Compare commits

...

5 Commits

6 changed files with 436 additions and 16 deletions
+224 -13
View File
@@ -18,9 +18,9 @@
Functions for working with disk layout Functions for working with disk layout
*/ */
use crate::kira_size::KiraSize;
use log; use log;
use std::collections::HashMap; use std::{collections::HashMap, io::Read, process::ExitStatus};
use crate::kira_size::{KiraSize};
/// ///
/// size - device size in bytes /// size - device size in bytes
/// sector size - default 4096 /// sector size - default 4096
@@ -38,6 +38,7 @@ impl std::fmt::Display for BlkDev {
} }
impl BlkDev { impl BlkDev {
/// Create new BlkDev struct with specifyed name and size
pub fn new(name: String, size: KiraSize) -> Self { pub fn new(name: String, size: KiraSize) -> Self {
Self { Self {
name: name, name: name,
@@ -45,6 +46,9 @@ impl BlkDev {
sector_size: KiraSize::new_b(4096), sector_size: KiraSize::new_b(4096),
} }
} }
/// Create new BlkDev struct with name and size parsed from hashmap
/// that contains String key val pairs with "NAME" and "SIZE" keys
pub fn from_hash_map(data: &HashMap<String, String>) -> Option<Self> { pub fn from_hash_map(data: &HashMap<String, String>) -> Option<Self> {
Some(Self { Some(Self {
name: data.get("NAME")?.clone(), name: data.get("NAME")?.clone(),
@@ -52,7 +56,13 @@ impl BlkDev {
sector_size: KiraSize::new_b(4096), sector_size: KiraSize::new_b(4096),
}) })
} }
/// returns dev name in a "/dev/name" form
pub fn full_name(&self) -> String {
format!("/dev/{}", self.name)
}
/// this is blocking function /// this is blocking function
/// Lists block devices in system thru "lsbk" command
pub fn list_sys_blk_dev() -> Result<Vec<Self>, String> { pub fn list_sys_blk_dev() -> Result<Vec<Self>, String> {
use std::process::Command; use std::process::Command;
@@ -139,6 +149,22 @@ impl FSType {
_ => Self::UNKNOWN, _ => Self::UNKNOWN,
} }
} }
/// return fs type that can be used with parted utility
pub fn to_parted_str(&self) -> &str {
match self {
Self::VFAT => "fat32",
Self::XFS => "xfs",
Self::LUKS => "luks",
Self::SWAP => "linux-swap",
Self::EXT4 => "ext4",
Self::BTRFS => "btrfs",
// workaroud as parted do not support bcachefs partition type
Self::BCACHEFS => "bcachefs",
// lets do something generic here instead of panic
Self::UNKNOWN => "linux-swap",
}
}
} }
// fn part_type_to_color(t: &str) -> Color { // fn part_type_to_color(t: &str) -> Color {
@@ -177,13 +203,7 @@ pub struct PartInfo {
impl std::fmt::Display for PartInfo { impl std::fmt::Display for PartInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!( write!(f, "{} {} {}", &self.name, &self.fs_type, self.size)
f,
"{} {} {}",
&self.name,
&self.fs_type,
self.size
)
} }
} }
@@ -291,7 +311,8 @@ pub fn align_part(size: u64, start: u64, align: u64) -> Option<(u64, u64, u64)>
if align_size < align { if align_size < align {
log::warn!( log::warn!(
"!!! align_size < align !!! align_size: {} align: {}", "!!! align_size < align !!! align_size: {} align: {}",
align_size, align align_size,
align
); );
return None; return None;
} }
@@ -316,7 +337,9 @@ impl PartLayout {
pub fn read_from_disk(dev: &BlkDev) -> Result<Self, String> { pub fn read_from_disk(dev: &BlkDev) -> Result<Self, String> {
let part_list = get_dev_part_blocking(&dev.name)?; let part_list = get_dev_part_blocking(&dev.name)?;
let parts_size = part_list.iter().fold(KiraSize::new_b(0), |acc, part| acc + part.size); let parts_size = part_list
.iter()
.fold(KiraSize::new_b(0), |acc, part| acc + part.size);
Ok(Self { Ok(Self {
dev: dev.clone(), dev: dev.clone(),
empty_space: dev.size - parts_size, empty_space: dev.size - parts_size,
@@ -392,7 +415,7 @@ impl PartLayout {
) )
.add_part_reserve( .add_part_reserve(
swap_size, swap_size,
FSType::XFS, FSType::BTRFS,
Some("root".into()), Some("root".into()),
None, None,
Some("/".into()), Some("/".into()),
@@ -415,6 +438,195 @@ impl PartLayout {
} }
} }
/// Executes parted command with given arguments
fn exec_parted(args: &Vec<&str>) -> Result<String, String> {
use std::process::Command;
match Command::new("parted").args(args).output() {
Err(ex) => Err(ex.to_string()),
Ok(out) => {
if out.status.success() {
String::from_utf8(out.stdout).map_err(|ex| ex.to_string())
} else {
Err(out.status.to_string())
}
}
}
}
fn parted_get_last_part_num(dev: &BlkDev) -> Result<u32, String> {
// parted -m /dev/loop0 print
match exec_parted(&vec!["-m", dev.full_name().as_str(), "print"]) {
Ok(res) => {
match res.lines().last().and_then(|last_line| {
last_line
.split_once(':')
.and_then(|(first_part, a)| u32::from_str_radix(first_part, 10).ok())
}) {
Some(n) => Ok(n),
None => Err("Error parsing parted output".to_string()),
}
}
Err(ex) => Err(ex),
}
}
/// init gpt partition table
/// parted --script $install_device mklabel gpt
/// Its blocking function
pub fn parted_init_gpt_part_table(dev: &BlkDev) -> Result<(), String> {
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<(), String> {
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: &BlkDev,
compression: Option<String>,
password: Option<String>,
) -> Result<(), String> {
// 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
use std::fs;
use std::process::Command;
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).map_err(|ex| ex.to_string())?;
arg.push("--encrypted");
arg.push("--passphrase_file=/tmp/bzzpsspass.txt");
}
// add dev name at the end
let dev_name = dev.full_name();
arg.push(dev_name.as_str());
let out = Command::new("bcachefs")
.args(arg)
.output()
.map_err(|ex| ex.to_string())?;
// ignore any errors while deleting pass tmp file
let _ = fs::remove_file("/tmp/bzzpsspass.txt");
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?)
}
}
/// unlok encrypted bcache volume
pub fn bcachefs_unlock(dev: &BlkDev, pass: String) -> Result<(), String> {
use std::fs;
use std::process::Command;
// bcachefs unlock --file=/tmp/bzzpsspass.txt /dev/loop0
let dev_name = dev.full_name();
let arg = vec!["unlock", "--file=/tmp/bzzpsspass.txt ", dev_name.as_str()];
// creating tmp file with password
fs::write("/tmp/bzzpsspass.txt", &pass).map_err(|ex| ex.to_string())?;
let out = Command::new("bcachefs")
.args(arg)
.output()
.map_err(|ex| ex.to_string())?;
// ignore any errors while deleting pass tmp file
let _ = fs::remove_file("/tmp/bzzpsspass.txt");
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?)
}
}
/// create subvolume
pub fn bcachefs_create_subvolume(vol_path: &str) -> Result<(), String> {
use std::process::Command;
// sudo bcachefs subvolume create /mnt/test/nyan
let out = Command::new("bcachefs")
.args(["subvolume", "create", vol_path])
.output()
.map_err(|ex| ex.to_string())?;
if out.status.success() {
Ok(())
} else {
Err(String::from_utf8(out.stderr).map_err(|ex| ex.to_string())?)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rand::RngExt; use rand::RngExt;
@@ -466,4 +678,3 @@ mod tests {
} }
} }
} }
+7
View File
@@ -46,6 +46,13 @@ impl std::fmt::Display for KiraSize {
impl KiraSize { impl KiraSize {
pub const SUFFIXS: [&str; 5] = ["KB", "MB", "GB", "TB", "PB"]; pub const SUFFIXS: [&str; 5] = ["KB", "MB", "GB", "TB", "PB"];
/// returns size in bytes suffixed with "B" character
pub fn to_parted_bytes_str(&self)-> String {
format!("{}B", self.size_bytes)
}
pub fn new_b(size:u64) -> Self { pub fn new_b(size:u64) -> Self {
Self { size_bytes: size } Self { size_bytes: size }
} }
+7 -1
View File
@@ -4,7 +4,10 @@
"button.next": "Next", "button.next": "Next",
"button.back": "Back", "button.back": "Back",
"button.cancel": "Cancel", "button.cancel": "Cancel",
"button.finish": "Finish",
"button.exit": "Exit", "button.exit": "Exit",
"button.yes": "Yes",
"button.no": "No",
"wellcome.text": "Welcome to Kira Installer!", "wellcome.text": "Welcome to Kira Installer!",
"wellcome.choose_language": "Please select language to use during istalation!", "wellcome.choose_language": "Please select language to use during istalation!",
"license.license": "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.\n\nThis 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.\n\nYou should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.", "license.license": "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.\n\nThis 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.\n\nYou should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.",
@@ -37,6 +40,7 @@
"partition.select_swapmode": "Select swap partition mode", "partition.select_swapmode": "Select swap partition mode",
"partition.use_zram": "Use zram technology", "partition.use_zram": "Use zram technology",
"partition.use_secure_boot": "Setup secure boot", "partition.use_secure_boot": "Setup secure boot",
"partition.encrypt_drive": "Encrypt drive",
"partition.current_dev_layout": "Current device layout", "partition.current_dev_layout": "Current device layout",
"partition.current_after_install": "After installation device will look like this", "partition.current_after_install": "After installation device will look like this",
"security.user_name": "User login name", "security.user_name": "User login name",
@@ -46,5 +50,7 @@
"security.root_password": "Root password", "security.root_password": "Root password",
"security.pass_low": "low security", "security.pass_low": "low security",
"security.pass_middle": "normal security", "security.pass_middle": "normal security",
"security.pass_hight": "hight security" "security.pass_hight": "hight security",
"install.caption": "Now we will beee install Bzz Linux to your PC",
"install.warning": "We a ready to start installation, this action will destroy all data on select drive!\nDo you want to proceed?"
} }
+181
View File
@@ -0,0 +1,181 @@
// <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/>.
/*
Actual installation script.
*/
///
/// So. I am goind to make dir structure lile this
/// /bzz/root
/// /bzz/home
/// Mount root from it and binmount home
///
use log;
use crate::{
kira_theming,
stage::{KiraConfig, StageAction, StageResult},
};
use iced::{Alignment, Task, futures, widget};
use rust_i18n::t;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum InstallationState {
UserConfirmDialog,
Partitionning,
CopyFiles,
Configuring,
Finish,
Error,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InstallStage {
state: InstallationState,
progress: f32,
progress_message: String,
}
#[derive(Debug, Clone)]
pub enum Message {
UserConfirms,
UserDenies,
StartPartitioning,
UpdateProgress(Result<(f32, String), String>),
PartitioningFinish(Result<(), String>),
Back,
Cancel,
Finish,
}
#[derive(Debug)]
pub enum Update {
StgAct(StageAction),
Task(Task<Message>),
}
impl InstallStage {
pub fn new(config: &KiraConfig) -> Self {
Self {
state: InstallationState::UserConfirmDialog,
progress: 0.0,
progress_message: String::new(),
}
}
fn gen_result(&self) -> StageResult {
StageResult::new("install")
}
pub fn update(&mut self, message: Message) -> Update {
match message {
Message::UserDenies => Update::StgAct(StageAction::Back),
Message::UserConfirms => {
// starting installation process here
self.state = InstallationState::Partitionning;
Update::Task(Task::run(disk_part::check_connected(), Message::UpdateProgress))
//Update::Task(Task::perform(, Message::PartitioningFinish))
}
Message::StartPartitioning => Update::StgAct(StageAction::None),
Message::PartitioningFinish(res) => Update::StgAct(StageAction::None),
Message::UpdateProgress(maybe_msg) => {
match maybe_msg {
Ok((progress, msg))=> {
self.progress = progress;
self.progress_message = msg;
Update::StgAct(StageAction::None)
}
Err(ex) => {
log::error!("{}", &ex);
self.state = InstallationState::Error;
Update::StgAct(StageAction::None)
}
}
//Update::StgAct(StageAction::None)
},
Message::Cancel => Update::StgAct(StageAction::Abort),
Message::Back => Update::StgAct(StageAction::Back),
Message::Finish => Update::StgAct(StageAction::None),
}
}
pub fn view(&self) -> iced::Element<'_, Message> {
let action_button = if self.state == InstallationState::Finish {
widget::button(widget::text(t!("button.finish"))).on_press(Message::Finish)
} else {
widget::button(widget::text(t!("button.cancel"))).on_press(Message::Cancel)
};
// Embed the image bytes into the executable
let welcom_logo_handle = widget::image::Handle::from_bytes(kira_theming::get_logo_bytes());
let main_content = widget::column![
widget::container(
widget::column![
widget::image(welcom_logo_handle)
.width(iced::Pixels(128.0))
.height(iced::Pixels(128.0)),
widget::text(t!("install.caption")),
widget::progress_bar(0.0..=100.0, self.progress),
widget::text(self.progress_message.clone()),
]
.padding(10)
.spacing(10)
.align_x(Alignment::Center)
)
.height(iced::Length::Fill)
.width(iced::Length::Fill)
.align_x(Alignment::Center)
.align_y(Alignment::Center),
widget::container(action_button)
.width(iced::Length::Fill)
.align_y(Alignment::End)
.padding(10),
]
.align_x(Alignment::Center)
.spacing(10);
let page_stack = widget::Stack::with_capacity(2).push(main_content);
if self.state == InstallationState::UserConfirmDialog {
page_stack
.push(
widget::column![
widget::text(t!("install.warning")),
widget::row![
widget::button(widget::text(t!("button.no")))
.on_press(Message::UserDenies),
widget::space::horizontal(), // Pushes the right button to the far right
widget::button(widget::text(t!("button.yes")))
.on_press(Message::UserConfirms)
]
.width(iced::Length::Fill)
.align_y(Alignment::End)
.padding(10)
]
.align_x(Alignment::Center),
)
.into()
} else {
page_stack.into()
}
}
}
+2 -1
View File
@@ -22,4 +22,5 @@ pub mod timezone;
pub mod locale; pub mod locale;
pub mod keyboard; pub mod keyboard;
pub mod partition; pub mod partition;
pub mod security; pub mod security;
pub mod install;
+15 -1
View File
@@ -68,6 +68,8 @@ fn part_type_to_color(t: &FSType) -> Color {
} }
} }
/// Converts PartInfo into data for ColorBar widget
///
fn part_to_ct_params(part: &PartInfo, dev_size: KiraSize) -> (String, u16, Color) { fn part_to_ct_params(part: &PartInfo, dev_size: KiraSize) -> (String, u16, Color) {
let fill_ratio: u16 = ((part.size.b() as f32 / dev_size.b() as f32) * 11.0) as u16; let fill_ratio: u16 = ((part.size.b() as f32 / dev_size.b() as f32) * 11.0) as u16;
let fill_ratio = fill_ratio.max(1); let fill_ratio = fill_ratio.max(1);
@@ -82,6 +84,7 @@ pub struct PartitionStage {
swap_mode: Option<SwapMode>, swap_mode: Option<SwapMode>,
use_zram: bool, use_zram: bool,
secure_boot: bool, secure_boot: bool,
encrypt_drive: bool,
secure_boot_in_setup_mode: bool, secure_boot_in_setup_mode: bool,
custom_swap_size_mb: u32, custom_swap_size_mb: u32,
custom_swap_size_str: String, custom_swap_size_str: String,
@@ -97,6 +100,7 @@ pub enum Message {
SelectSwapMode(SwapMode), SelectSwapMode(SwapMode),
ToggleZram(bool), ToggleZram(bool),
ToggleSecureBoot(bool), ToggleSecureBoot(bool),
ToggleEncrypDrive(bool),
SwapSizeFieldChange(String), SwapSizeFieldChange(String),
SwapSizeIncrement, SwapSizeIncrement,
SwapSizeDecrement, SwapSizeDecrement,
@@ -152,6 +156,7 @@ impl PartitionStage {
swap_mode: Some(SwapMode::SwapNoHibernate), swap_mode: Some(SwapMode::SwapNoHibernate),
use_zram: true, use_zram: true,
secure_boot: false, secure_boot: false,
encrypt_drive: false,
secure_boot_in_setup_mode: sec_boot_setup, secure_boot_in_setup_mode: sec_boot_setup,
custom_swap_size_mb: 1024, custom_swap_size_mb: 1024,
custom_swap_size_str: "1024MB".into(), custom_swap_size_str: "1024MB".into(),
@@ -233,6 +238,7 @@ impl PartitionStage {
.add_val_u64("swap_size_mb", swap_size.mb()) .add_val_u64("swap_size_mb", swap_size.mb())
.add_val_bool("use_zram", self.use_zram) .add_val_bool("use_zram", self.use_zram)
.add_val_bool("secure_boot", self.secure_boot) .add_val_bool("secure_boot", self.secure_boot)
.add_val_bool("encrypt_drive", self.encrypt_drive)
} }
pub fn update(&mut self, message: Message) -> StageAction { pub fn update(&mut self, message: Message) -> StageAction {
@@ -255,7 +261,7 @@ impl PartitionStage {
let swap_size: KiraSize = self.get_swap_size(); let swap_size: KiraSize = self.get_swap_size();
self.generated_disk_parts = Some( self.generated_disk_parts = Some(
PartLayout::gen_classic_layout(dev.clone(), KiraSize::new_mb(512), swap_size) PartLayout::gen_classic_layout(dev.clone(), KiraSize::new_mb(128), swap_size)
.part_list .part_list
.iter() .iter()
.map(|part_info| part_to_ct_params(part_info, dev.size)) .map(|part_info| part_to_ct_params(part_info, dev.size))
@@ -274,6 +280,10 @@ impl PartitionStage {
self.secure_boot = t; self.secure_boot = t;
StageAction::None StageAction::None
}, },
Message::ToggleEncrypDrive(t) => {
self.encrypt_drive = t;
StageAction::None
},
Message::ToggleZram(t) => { Message::ToggleZram(t) => {
self.use_zram = t; self.use_zram = t;
self.gen_layout(); self.gen_layout();
@@ -376,6 +386,10 @@ impl PartitionStage {
widget::rule::horizontal(2), widget::rule::horizontal(2),
use_sec_boot_checkbox, use_sec_boot_checkbox,
widget::rule::horizontal(2), widget::rule::horizontal(2),
widget::checkbox(self.encrypt_drive)
.label(t!("partition.encrypt_drive"))
.on_toggle(Message::ToggleEncrypDrive),
widget::rule::horizontal(2),
selected_dev_content, selected_dev_content,
dev_parts_content, dev_parts_content,
] ]