forked from pisilinux-rs/yali-rs
2602 lines
99 KiB
Rust
2602 lines
99 KiB
Rust
//! Kurulum iş kuyruğu.
|
||
//!
|
||
//! Mimari:
|
||
//! egui (ana iş parçacığı)
|
||
//! └─► std::thread::spawn
|
||
//! └─► tokio::runtime::Runtime::block_on
|
||
//! └─► JobQueue::run_all ─► std::sync::mpsc::Sender<InstallMessage>
|
||
//!
|
||
//! egui her frame'de `Receiver::try_recv` ile kuyruktan mesaj okur.
|
||
//! `egui::Context::request_repaint()` arka plan iş parçacığından çağrılarak
|
||
//! UI'nin hemen güncellenmesi sağlanır.
|
||
|
||
|
||
use std::sync::atomic::{AtomicBool, Ordering};
|
||
use std::sync::mpsc;
|
||
use std::time::Instant;
|
||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||
use tokio::process::Command;
|
||
use rust_i18n::t;
|
||
|
||
// ─────────────────────────────────────────────
|
||
// MESAJ TİPİ
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// Arka plan iş parçacığından UI'ye gönderilen mesajlar.
|
||
#[derive(Debug)]
|
||
pub enum InstallMessage {
|
||
/// Bir log satırı.
|
||
Log(String),
|
||
/// Genel ilerleme; 0.0–1.0 arasında.
|
||
Progress(f32),
|
||
/// Alt ilerleme (ör: rsync içindeki dosya kopyalama); 0.0–1.0 arasında.
|
||
SubProgress(f32),
|
||
/// Kopyalanmakta olan dosya adı.
|
||
CurrentFile(String),
|
||
/// Tüm işler başarıyla tamamlandı.
|
||
Done,
|
||
/// Bir iş başarısız oldu; kurulum durdu.
|
||
Error(String),
|
||
}
|
||
|
||
/// `mpsc::Sender<InstallMessage>` ile `egui::Context` birlikte taşır.
|
||
/// Mesaj gönderiminde `request_repaint()` çağrısı UI'yi uyandırır,
|
||
/// ancak en fazla saniyede ~30 kez (33ms aralıkla) olacak şekilde
|
||
/// throttle edilir. Bu, özellikle rsync gibi yoğun I/O işlemleri
|
||
/// sırasında Intel tümleşik GPU'lu (UMA) sistemlerde GPU belleği
|
||
/// rekabetini önler ve karakter bozulmasını engeller.
|
||
pub struct UiSender {
|
||
tx: mpsc::Sender<InstallMessage>,
|
||
ctx: eframe::egui::Context,
|
||
last_repaint: std::sync::Arc<std::sync::Mutex<Instant>>,
|
||
}
|
||
|
||
impl Clone for UiSender {
|
||
fn clone(&self) -> Self {
|
||
Self {
|
||
tx: self.tx.clone(),
|
||
ctx: self.ctx.clone(),
|
||
last_repaint: self.last_repaint.clone(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl UiSender {
|
||
pub fn new(tx: mpsc::Sender<InstallMessage>, ctx: eframe::egui::Context) -> Self {
|
||
Self {
|
||
tx,
|
||
ctx,
|
||
last_repaint: std::sync::Arc::new(std::sync::Mutex::new(Instant::now())),
|
||
}
|
||
}
|
||
|
||
pub fn send(&self, msg: InstallMessage) {
|
||
let _ = self.tx.send(msg);
|
||
let mut last = self.last_repaint.lock().unwrap();
|
||
if last.elapsed() >= std::time::Duration::from_millis(33) {
|
||
self.ctx.request_repaint();
|
||
*last = Instant::now();
|
||
}
|
||
}
|
||
|
||
pub fn log(&self, s: impl Into<String>) {
|
||
self.send(InstallMessage::Log(s.into()));
|
||
}
|
||
|
||
pub fn sub_progress(&self, p: f32) {
|
||
self.send(InstallMessage::SubProgress(p));
|
||
}
|
||
|
||
pub fn current_file(&self, f: impl Into<String>) {
|
||
self.send(InstallMessage::CurrentFile(f.into()));
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// JOB TRAIT
|
||
// ─────────────────────────────────────────────
|
||
|
||
#[async_trait::async_trait]
|
||
pub trait Job: Send + Sync {
|
||
fn name(&self) -> &str;
|
||
/// İşi asenkron olarak çalıştırır.
|
||
/// Hata durumunda `Err(açıklama)` döner; kuyruk durur.
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String>;
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// JOB QUEUE
|
||
// ─────────────────────────────────────────────
|
||
|
||
pub struct JobQueue {
|
||
jobs: Vec<Box<dyn Job>>,
|
||
}
|
||
|
||
impl JobQueue {
|
||
pub fn new(jobs: Vec<Box<dyn Job>>) -> Self {
|
||
Self { jobs }
|
||
}
|
||
|
||
/// Tüm işleri sırayla çalıştırır.
|
||
/// İlerleme ve loglar `ui` üzerinden UI'ye iletilir.
|
||
pub async fn run_all(&mut self, ui: UiSender) {
|
||
let total = self.jobs.len();
|
||
|
||
for (i, job) in self.jobs.iter().enumerate() {
|
||
let localized_name = t!(job.name());
|
||
ui.log(format!("▶ {} başlatıldı…", localized_name));
|
||
|
||
match job.run(&ui).await {
|
||
Ok(()) => {
|
||
ui.log(format!("✓ {} tamamlandı.", localized_name));
|
||
}
|
||
Err(e) => {
|
||
let msg = format!("✗ {} başarısız: {}", localized_name, e);
|
||
ui.log(msg.clone());
|
||
ui.send(InstallMessage::Error(msg));
|
||
return;
|
||
}
|
||
}
|
||
|
||
let progress = (i + 1) as f32 / total as f32;
|
||
ui.send(InstallMessage::Progress(progress));
|
||
}
|
||
|
||
ui.send(InstallMessage::Done);
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// SOMUT JOB'LAR
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// Sistem dosyalarını kopyala (rsync).
|
||
pub struct CopyFilesJob {
|
||
/// Canlı sistemin kök dizini; ör. "/run/livecd/squashfs-root/"
|
||
pub source: String,
|
||
/// Hedef bağlama noktası; ör. "/mnt"
|
||
pub target: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for CopyFilesJob {
|
||
fn name(&self) -> &str { "job_copy_files" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
ui.log(format!("[DEBUG] CopyFilesJob başladı"));
|
||
ui.log(format!("[DEBUG] Kaynak: '{}'", self.source));
|
||
ui.log(format!("[DEBUG] Hedef: '{}'", self.target));
|
||
|
||
// Kaynak dizini kontrol et
|
||
let source_path = std::path::Path::new(&self.source);
|
||
let source_exists = source_path.exists();
|
||
let source_readable = source_exists && source_path.is_dir();
|
||
ui.log(format!("[DEBUG] Kaynak mevcut: {}, dizin: {}", source_exists, source_readable));
|
||
|
||
if !source_exists {
|
||
ui.log(format!("[DEBUG] Kaynak dizin içeriği aranıyor..."));
|
||
// Kaynağın bir üst dizinini dene
|
||
let parent = source_path.parent();
|
||
if let Some(p) = parent {
|
||
if p.exists() {
|
||
if let Ok(entries) = std::fs::read_dir(p) {
|
||
for entry in entries.flatten().take(20) {
|
||
ui.log(format!("[DEBUG] /{:?}", entry.path()));
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
ui.log("[DEBUG] Kaynağın parent'ı yok (kök dizin olabilir)");
|
||
}
|
||
} else if source_readable {
|
||
// İlk 10 girdiyi listele
|
||
if let Ok(entries) = std::fs::read_dir(source_path) {
|
||
let mut count = 0;
|
||
for entry in entries.flatten().take(10) {
|
||
ui.log(format!("[DEBUG] {:?}", entry.path()));
|
||
count += 1;
|
||
}
|
||
ui.log(format!("[DEBUG] (ilk {} girdi listelendi)", count));
|
||
}
|
||
}
|
||
|
||
ui.log(format!("rsync {} → {}", self.source, self.target));
|
||
|
||
let is_root_source = self.source.trim_end_matches('/') == "";
|
||
|
||
// ── Önce find ile toplam dosya sayısını bul ──────────
|
||
let find_path = self.source.trim_end_matches('/');
|
||
let find_path = if find_path.is_empty() { "/" } else { find_path };
|
||
let total_files = {
|
||
let mut cmd = Command::new("find");
|
||
cmd.arg(find_path).arg("-type").arg("f");
|
||
if is_root_source {
|
||
cmd.arg("-xdev");
|
||
}
|
||
cmd.stdout(std::process::Stdio::piped());
|
||
cmd.stderr(std::process::Stdio::null());
|
||
match cmd.spawn() {
|
||
Ok(mut c) => {
|
||
let stdout = c.stdout.take()
|
||
.ok_or_else(|| "find stdout alınamadı".to_string())?;
|
||
let r = BufReader::new(stdout);
|
||
let mut lines = r.lines();
|
||
let mut n: u64 = 0;
|
||
while let Ok(Some(_)) = lines.next_line().await {
|
||
n += 1;
|
||
}
|
||
let _ = c.wait().await;
|
||
// if n > 0 {
|
||
// ui.log(format!("{} dosya bulundu", n));
|
||
// }
|
||
n
|
||
}
|
||
Err(_) => 0
|
||
}
|
||
};
|
||
|
||
let mut args: Vec<String> = vec![
|
||
"-aAXH".to_string(),
|
||
"--out-format=%n".to_string(),
|
||
"--exclude=/proc/**".to_string(),
|
||
"--exclude=/sys/**".to_string(),
|
||
"--exclude=/dev/**".to_string(),
|
||
"--exclude=/run/**".to_string(),
|
||
"--exclude=/tmp/**".to_string(),
|
||
"--exclude=/var/tmp/**".to_string(),
|
||
"--exclude=/var/cache/**".to_string(),
|
||
"--exclude=/home/**".to_string(),
|
||
"--exclude=/root/**".to_string(),
|
||
"--exclude=/lost+found".to_string(),
|
||
"--exclude=*.sqfs".to_string(),
|
||
];
|
||
|
||
// Kaynak canlı sistem köküyse ekstra exclude'ler
|
||
if is_root_source {
|
||
args.extend_from_slice(&[
|
||
"--exclude=/bootmnt/**".to_string(),
|
||
"--exclude=/ro_branch/**".to_string(),
|
||
"--exclude=/mnt/**".to_string(),
|
||
"--exclude=/media/**".to_string(),
|
||
"--exclude=/var/log/journal/**".to_string(),
|
||
"--exclude=/var/lib/pisi/cache/**".to_string(),
|
||
"--exclude=/var/lib/pisi/archives/**".to_string(),
|
||
"--exclude=/etc/adjtime".to_string(),
|
||
"--exclude=/etc/mtab".to_string(),
|
||
"--exclude=/etc/resolv.conf".to_string(),
|
||
"--exclude=/etc/hostname".to_string(),
|
||
"--exclude=/etc/machine-id".to_string(),
|
||
]);
|
||
}
|
||
|
||
if let (Ok(source_path), Ok(target_path)) = (
|
||
std::path::Path::new(&self.source).canonicalize(),
|
||
std::path::Path::new(&self.target).canonicalize(),
|
||
) {
|
||
if let Ok(relative_target) = target_path.strip_prefix(&source_path) {
|
||
if !relative_target.as_os_str().is_empty() {
|
||
args.push(format!("--exclude={}", relative_target.display()));
|
||
}
|
||
}
|
||
}
|
||
|
||
args.push(self.source.clone());
|
||
args.push(self.target.clone());
|
||
|
||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||
ui.log("rsync /run/livecd/squashfs-root/ → /mnt (DEMO)");
|
||
let steps = 20;
|
||
for i in 0..steps {
|
||
let p = (i + 1) as f32 / steps as f32;
|
||
ui.current_file(format!("/usr/share/doc/{}/README.md", (i * 137) % 50));
|
||
ui.sub_progress(p);
|
||
tokio::time::sleep(std::time::Duration::from_millis(150)).await;
|
||
}
|
||
return Ok(());
|
||
}
|
||
|
||
let mut child = Command::new("rsync")
|
||
.args(&args)
|
||
.stdout(std::process::Stdio::piped())
|
||
.stderr(std::process::Stdio::piped())
|
||
.spawn()
|
||
.map_err(|e| format!("rsync çalıştırılamadı: {}", e))?;
|
||
|
||
let stdout = child.stdout.take()
|
||
.ok_or_else(|| "rsync stdout alınamadı".to_string())?;
|
||
let stderr = child.stderr.take()
|
||
.ok_or_else(|| "rsync stderr alınamadı".to_string())?;
|
||
|
||
// Stderr'i arka planda oku (hata birikimi ve tampon taşmasını engelle)
|
||
let stderr_buf = std::sync::Arc::new(tokio::sync::Mutex::new(String::new()));
|
||
let stderr_buf_clone = stderr_buf.clone();
|
||
tokio::spawn(async move {
|
||
let r = BufReader::new(stderr);
|
||
let mut lines = r.lines();
|
||
while let Ok(Some(l)) = lines.next_line().await {
|
||
let mut b = stderr_buf_clone.lock().await;
|
||
b.push_str(&l);
|
||
b.push('\n');
|
||
}
|
||
});
|
||
|
||
// Stdout: --out-format=%n ile her satır bir dosya adı
|
||
let reader = BufReader::new(stdout);
|
||
let mut lines = reader.lines();
|
||
let mut file_count: u64 = 0;
|
||
// Son UI güncellemesi — çok sık güncelleme GPU (özellikle Intel UMA) baskısını azaltır
|
||
let mut last_update = Instant::now();
|
||
|
||
while let Some(line) = lines.next_line().await
|
||
.map_err(|e| format!("rsync çıktısı okunamadı: {}", e))?
|
||
{
|
||
let trimmed = line.trim().to_string();
|
||
if trimmed.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
file_count += 1;
|
||
|
||
let now = Instant::now();
|
||
if now.duration_since(last_update) >= std::time::Duration::from_millis(50)
|
||
|| file_count == 1
|
||
|| trimmed.contains("Packages") || trimmed.contains("package")
|
||
{
|
||
ui.current_file(trimmed);
|
||
last_update = now;
|
||
}
|
||
|
||
if total_files > 0 {
|
||
let p = (file_count as f32) / (total_files as f32);
|
||
ui.sub_progress(p.min(1.0));
|
||
}
|
||
}
|
||
|
||
let status = child.wait().await
|
||
.map_err(|e| format!("rsync beklenemedi: {}", e))?;
|
||
|
||
if !status.success() {
|
||
let err = stderr_buf.lock().await.clone();
|
||
return Err(format!(
|
||
"rsync çıkış kodu: {:?}. Hata: {}",
|
||
status.code(),
|
||
err.trim()
|
||
));
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// /proc, /sys, /dev, /dev/pts dizinlerini chroot hedefine bind-mount yapar.
|
||
/// Standart `chroot` için gereklidir; kurulum başında çağrılmalıdır.
|
||
pub struct MountBindJob {
|
||
pub mount: String, // ör. "/mnt"
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for MountBindJob {
|
||
fn name(&self) -> &str { "job_mount_bind" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let mt = &self.mount;
|
||
// (kaynak, hedef) — kaynak sistem üzerindeki gerçek path
|
||
let bind_dirs = [
|
||
("/proc", format!("{}/proc", mt)),
|
||
("/sys", format!("{}/sys", mt)),
|
||
("/dev", format!("{}/dev", mt)),
|
||
("/dev/pts", format!("{}/dev/pts", mt)),
|
||
("/run", format!("{}/run", mt)), // COMAR/dbus için gerekli
|
||
];
|
||
for (src, target) in &bind_dirs {
|
||
tokio::fs::create_dir_all(target).await
|
||
.map_err(|e| format!("{} oluşturulamadı: {}", target, e))?;
|
||
ui.log(format!("mount --bind {} {}", src, target));
|
||
run_cmd("mount", &["--bind", src, target]).await?;
|
||
}
|
||
|
||
// efivarfs — UEFI kurulumunda grub-install için zorunlu
|
||
let efivarfs_target = format!("{}/sys/firmware/efi/efivars", mt);
|
||
if std::path::Path::new("/sys/firmware/efi/efivars").exists() {
|
||
tokio::fs::create_dir_all(&efivarfs_target).await
|
||
.map_err(|e| format!("{} oluşturulamadı: {}", efivarfs_target, e))?;
|
||
ui.log(format!("mount --bind /sys/firmware/efi/efivars {}", efivarfs_target));
|
||
run_cmd("mount", &["--bind", "/sys/firmware/efi/efivars", &efivarfs_target]).await?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Bind-mount'ları temizler (kurulum sonunda çağrılır).
|
||
pub struct UnmountBindJob {
|
||
pub mount: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for UnmountBindJob {
|
||
fn name(&self) -> &str { "job_umount_bind" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let mt = &self.mount;
|
||
// Ters sırada umount — en derin path önce
|
||
let subdirs = [
|
||
"sys/firmware/efi/efivars",
|
||
"dev/pts",
|
||
"dev",
|
||
"run",
|
||
"sys",
|
||
"proc",
|
||
];
|
||
let mut errors: Vec<String> = Vec::new();
|
||
for subdir in &subdirs {
|
||
let target = format!("{}/{}", mt, subdir);
|
||
ui.log(format!("umount {}", target));
|
||
match tokio::process::Command::new("umount")
|
||
.arg("-lf").arg(&target).status().await
|
||
{
|
||
Ok(status) if status.success() => {}
|
||
Ok(status) => {
|
||
// "not mounted" veya "no such file" hataları beklenen durumlar —
|
||
// bunları uyarı olarak logla ama kritik hata sayma.
|
||
ui.log(format!("⚠ umount uyarısı (kritik değil, kod {}): {}", status, target));
|
||
}
|
||
Err(e) => {
|
||
// Komut çalıştırılamadı — bu gerçek bir sorun.
|
||
let msg = format!("umount komutu başlatılamadı ({}): {}", target, e);
|
||
ui.log(format!("✗ {}", msg));
|
||
errors.push(msg);
|
||
}
|
||
}
|
||
}
|
||
if !errors.is_empty() {
|
||
return Err(format!(
|
||
"Bind mount temizleme hatası — bazı sanal dosya sistemleri bağlı kalmış olabilir:\n{}",
|
||
errors.join("\n")
|
||
));
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// initramfs yeniler (GRUB öncesi zorunlu).
|
||
/// PisiLinux `dracut` kullanıyorsa `dracut -f --regenerate-all`,
|
||
/// mkinitcpio kullanıyorsa `mkinitcpio -P`.
|
||
pub struct GenerateInitramfsJob {
|
||
pub mount: String,
|
||
pub encrypt_root: bool,
|
||
pub lvm_active: bool,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for GenerateInitramfsJob {
|
||
fn name(&self) -> &str { "job_initramfs" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
// LVM için initramfs'e lvm modülü/hook'u ekleyen konfigürasyon
|
||
if self.lvm_active && !self.encrypt_root {
|
||
ui.log("LVM algılandı: initramfs LVM ayarlanıyor…");
|
||
|
||
// dracut: add lvm module
|
||
let dracut_conf = format!("{}/etc/dracut.conf.d/99-lvm.conf", self.mount);
|
||
let _ = tokio::fs::create_dir_all(format!("{}/etc/dracut.conf.d", self.mount)).await;
|
||
let _ = tokio::fs::write(&dracut_conf, "add_dracutmodules+=\" lvm \"\n").await;
|
||
|
||
// mkinitcpio: add lvm2 hook
|
||
// Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir.
|
||
let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount);
|
||
if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() {
|
||
if let Ok(content) = tokio::fs::read_to_string(&mkinitcpio_conf).await {
|
||
if content.contains("HOOKS=") && !content.contains("lvm2") {
|
||
let new_content = content
|
||
.replace("HOOKS=\"", "HOOKS=\"lvm2 ")
|
||
.replace("HOOKS=(", "HOOKS=(lvm2 ");
|
||
let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// LUKS için initramfs'e cryptsetup ekleyen konfigürasyon
|
||
if self.encrypt_root {
|
||
ui.log("LUKS algılandı: initramfs cryptsetup ayarlanıyor…");
|
||
|
||
// dracut: add crypt module
|
||
let dracut_conf = format!("{}/etc/dracut.conf.d/99-luks.conf", self.mount);
|
||
tokio::fs::create_dir_all(
|
||
format!("{}/etc/dracut.conf.d", self.mount)
|
||
).await.map_err(|e| format!("dracut.conf.d oluşturulamadı: {}", e))?;
|
||
tokio::fs::write(&dracut_conf, "add_dracutmodules+=\" crypt lvm \"\n")
|
||
.await.map_err(|e| format!("dracut luks conf yazılamadı: {}", e))?;
|
||
|
||
// mkinitcpio: add encrypt hook
|
||
// Hem HOOKS="..." hem HOOKS=(... ) sözdizimi desteklenir.
|
||
let mkinitcpio_conf = format!("{}/etc/mkinitcpio.conf", self.mount);
|
||
if tokio::fs::metadata(&mkinitcpio_conf).await.is_ok() {
|
||
let content = tokio::fs::read_to_string(&mkinitcpio_conf)
|
||
.await.unwrap_or_default();
|
||
if content.contains("HOOKS=") && !content.contains("encrypt") {
|
||
let new_content = content
|
||
.replace("HOOKS=\"", "HOOKS=\"encrypt ")
|
||
.replace("HOOKS=(", "HOOKS=(encrypt ");
|
||
let _ = tokio::fs::write(&mkinitcpio_conf, new_content).await;
|
||
}
|
||
}
|
||
}
|
||
|
||
// PisiLinux /sbin/mkinitramfs, dracut, mkinitcpio sırasıyla dene
|
||
let cmds: &[&[&str]] = &[
|
||
&["mkinitramfs", "-f"],
|
||
&["dracut", "-f", "--regenerate-all"],
|
||
&["mkinitcpio", "-P"],
|
||
];
|
||
for cmd in cmds {
|
||
if run_chroot(&self.mount, cmd).await.is_ok() {
|
||
ui.log(format!("initramfs oluşturuldu ({})", cmd[0]));
|
||
return Ok(());
|
||
}
|
||
}
|
||
Err("initramfs oluşturulamadı: mkinitramfs, dracut, mkinitcpio denendi".to_string())
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/// GRUB bootloader kurulumu (UEFI veya BIOS).
|
||
pub struct InstallGrubJob {
|
||
pub disk: String,
|
||
pub mount: String,
|
||
pub is_uefi: bool,
|
||
pub boot_device: String,
|
||
pub timeout: u32,
|
||
pub password: String,
|
||
pub kernel_options: String,
|
||
pub encrypt_root: bool,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for InstallGrubJob {
|
||
fn name(&self) -> &str { "job_bootloader" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
// Ana dizinleri oluştur (demo modunda rsync çalışmaz)
|
||
let etc_default = format!("{}/etc/default", self.mount);
|
||
let _ = tokio::fs::create_dir_all(&etc_default).await;
|
||
let grub_d = format!("{}/etc/grub.d", self.mount);
|
||
let _ = tokio::fs::create_dir_all(&grub_d).await;
|
||
|
||
// GRUB şifresi (varsa) — grub-mkpasswd-pbkdf2 ile hash üretilir,
|
||
// düz metin asla diske yazılmaz.
|
||
if !self.password.is_empty() {
|
||
ui.log("GRUB şifresi PBKDF2 ile hashleniyor…");
|
||
|
||
// grub-mkpasswd-pbkdf2 şifreyi stdin'den iki kez okur (şifre + doğrulama)
|
||
use tokio::io::AsyncWriteExt;
|
||
let pw_input = format!("{}\n{}\n", self.password, self.password);
|
||
let mut child = tokio::process::Command::new("grub-mkpasswd-pbkdf2")
|
||
.stdin(std::process::Stdio::piped())
|
||
.stdout(std::process::Stdio::piped())
|
||
.stderr(std::process::Stdio::null())
|
||
.spawn()
|
||
.map_err(|e| format!("grub-mkpasswd-pbkdf2 başlatılamadı: {}", e))?;
|
||
|
||
if let Some(mut stdin) = child.stdin.take() {
|
||
stdin.write_all(pw_input.as_bytes()).await
|
||
.map_err(|e| format!("grub-mkpasswd-pbkdf2 stdin hatası: {}", e))?;
|
||
}
|
||
|
||
let output = child.wait_with_output().await
|
||
.map_err(|e| format!("grub-mkpasswd-pbkdf2 bekleme hatası: {}", e))?;
|
||
|
||
// Çıktıdan "grub.pbkdf2.sha512...." token'ını ayrıştır
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
let hash = stdout
|
||
.lines()
|
||
.find_map(|l| {
|
||
let pos = l.find("grub.pbkdf2")?;
|
||
Some(l[pos..].trim().to_string())
|
||
})
|
||
.ok_or("grub-mkpasswd-pbkdf2 çıktısında hash bulunamadı")?;
|
||
|
||
let custom_grub = format!(
|
||
"#!/bin/sh\nset superusers=\"root\"\npassword_pbkdf2 root {}\n",
|
||
hash
|
||
);
|
||
let pw_path = format!("{}/etc/grub.d/40_custom", self.mount);
|
||
tokio::fs::write(&pw_path, custom_grub)
|
||
.await
|
||
.map_err(|e| format!("GRUB şifre dosyası yazılamadı: {}", e))?;
|
||
run_cmd("chmod", &["+x", &pw_path]).await?;
|
||
ui.log("GRUB şifresi PBKDF2 hash olarak yazıldı.");
|
||
}
|
||
|
||
// GRUB timeout ayarı
|
||
ui.log(format!("GRUB timeout ayarlanıyor: {}s", self.timeout));
|
||
let default_path = format!("{}/etc/default/grub", self.mount);
|
||
let mut grub_default = tokio::fs::read_to_string(&default_path)
|
||
.await
|
||
.unwrap_or_default();
|
||
// timeout satırını bul/değiştir
|
||
let timeout_line = format!("GRUB_TIMEOUT={}", self.timeout);
|
||
let found_timeout = grub_default.lines()
|
||
.any(|l| l.trim_start().starts_with("GRUB_TIMEOUT="));
|
||
if found_timeout {
|
||
let mut lines: Vec<String> = grub_default.lines()
|
||
.map(|l| {
|
||
if l.trim_start().starts_with("GRUB_TIMEOUT=") {
|
||
timeout_line.clone()
|
||
} else {
|
||
l.to_string()
|
||
}
|
||
})
|
||
.collect();
|
||
lines.push(String::new());
|
||
grub_default = lines.join("\n");
|
||
} else {
|
||
grub_default.push_str(&format!("\n{}\n", timeout_line));
|
||
}
|
||
// kernel cmdline
|
||
let mut cmdline = self.kernel_options.clone();
|
||
if self.encrypt_root {
|
||
// Kök bölüm UUID'sini blkid'den al (LUKS başlık UUID'si)
|
||
let (root_num, _swap_num): (u32, u32) = if self.is_uefi { (3, 2) } else { (2, 1) };
|
||
let root_part = part_path(&self.disk, root_num);
|
||
let root_uuid = blkid_uuid(&root_part).await.unwrap_or_default();
|
||
let crypt_arg = format!("cryptdevice=UUID={}:cryptroot root=/dev/mapper/cryptroot", root_uuid);
|
||
if cmdline.is_empty() {
|
||
cmdline = crypt_arg;
|
||
} else {
|
||
cmdline = format!("{} {}", crypt_arg, cmdline);
|
||
}
|
||
}
|
||
if !cmdline.is_empty() {
|
||
let cmdline_line = format!("GRUB_CMDLINE_LINUX=\"{}\"", cmdline);
|
||
let found_cmdline = grub_default.lines()
|
||
.any(|l| l.trim_start().starts_with("GRUB_CMDLINE_LINUX="));
|
||
if found_cmdline {
|
||
let mut lines: Vec<String> = grub_default.lines()
|
||
.map(|l| {
|
||
if l.trim_start().starts_with("GRUB_CMDLINE_LINUX=") {
|
||
cmdline_line.clone()
|
||
} else {
|
||
l.to_string()
|
||
}
|
||
})
|
||
.collect();
|
||
lines.push(String::new());
|
||
grub_default = lines.join("\n");
|
||
} else {
|
||
grub_default.push_str(&format!("{}\n", cmdline_line));
|
||
}
|
||
}
|
||
tokio::fs::write(&default_path, grub_default)
|
||
.await
|
||
.map_err(|e| format!("/etc/default/grub yazılamadı: {}", e))?;
|
||
|
||
// Chroot içinde grub2-install çalıştır (UEFI veya BIOS)
|
||
let mut grub_args = vec!["grub2-install"];
|
||
let efi_dir_arg;
|
||
let disk_arg;
|
||
|
||
if self.is_uefi {
|
||
ui.log("GRUB UEFI modu kuruluyor (chroot içinde)…");
|
||
efi_dir_arg = "--efi-directory=/boot/efi".to_string();
|
||
grub_args.extend_from_slice(&[
|
||
"--target=x86_64-efi",
|
||
&efi_dir_arg,
|
||
"--bootloader-id=PisiLinux",
|
||
"--recheck",
|
||
]);
|
||
} else {
|
||
let install_dev = if self.boot_device.is_empty() { &self.disk } else { &self.boot_device };
|
||
ui.log(format!("GRUB BIOS modu {} aygıtına kuruluyor (chroot içinde)…", install_dev));
|
||
disk_arg = install_dev.clone();
|
||
grub_args.extend_from_slice(&[
|
||
"--target=i386-pc",
|
||
&disk_arg,
|
||
"--recheck",
|
||
]);
|
||
}
|
||
|
||
run_chroot(&self.mount, &grub_args).await?;
|
||
|
||
ui.log("grub2-mkconfig çalıştırılıyor…");
|
||
run_chroot(&self.mount, &[
|
||
"grub2-mkconfig", "-o", "/boot/grub2/grub.cfg",
|
||
]).await?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// YARDIMCI: KOMUT ÇALIŞTIRICI
|
||
// ─────────────────────────────────────────────
|
||
|
||
static DEMO_MODE: AtomicBool = AtomicBool::new(false);
|
||
|
||
/// Disk adı ve bölüm numarasından tam aygıt yolunu oluşturur.
|
||
///
|
||
/// - SATA/SCSI: `/dev/sda` + 1 → `/dev/sda1`
|
||
/// - NVMe: `/dev/nvme0n1` + 1 → `/dev/nvme0n1p1`
|
||
/// - MMC: `/dev/mmcblk0` + 1 → `/dev/mmcblk0p1`
|
||
fn part_path(disk: &str, num: u32) -> String {
|
||
crate::installer::part_path(disk, num)
|
||
}
|
||
|
||
|
||
/// Başarısızlık durumunda stderr'i hata mesajı olarak döner.
|
||
async fn run_cmd(program: &str, args: &[&str]) -> Result<(), String> {
|
||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||
tokio::time::sleep(std::time::Duration::from_millis(3000)).await;
|
||
return Ok(());
|
||
}
|
||
|
||
let output = Command::new(program)
|
||
.args(args)
|
||
.output()
|
||
.await
|
||
.map_err(|e| format!("{} çalıştırılamadı: {}", program, e))?;
|
||
|
||
if output.status.success() {
|
||
Ok(())
|
||
} else {
|
||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||
Err(if stderr.is_empty() { stdout } else { stderr })
|
||
}
|
||
}
|
||
|
||
/// `parted` komutlarını çalıştırmak için özel sarmalayıcı.
|
||
///
|
||
/// `parted -s` canlı sistemde kullanımda olan bir disk üzerinde bölüm
|
||
/// tablosunu kernel'e bildiremeyince exit 1 döndürür; ancak değişiklikler
|
||
/// diske **yazılmıştır** — kernel güncellemesi `sync_partition_table` ile
|
||
/// yapılır. Bu fonksiyon yalnızca gerçekten kritik hataları (disk bulunamadı
|
||
/// vb.) yakalar; "kernel haberdar edilemedi" uyarısını yok sayar.
|
||
async fn run_parted(args: &[&str]) -> Result<(), String> {
|
||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||
return Ok(());
|
||
}
|
||
|
||
let output = Command::new("parted")
|
||
.args(args)
|
||
.output()
|
||
.await
|
||
.map_err(|e| format!("parted çalıştırılamadı: {}", e))?;
|
||
|
||
if output.status.success() {
|
||
return Ok(());
|
||
}
|
||
|
||
// Parted, kernel güncelleyemediğinde exit 1 verir ama disk yazımı
|
||
// başarılıdır. Bu spesifik uyarıyı hata saymıyoruz.
|
||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||
let combined = format!("{}{}", stderr, stdout);
|
||
|
||
let is_kernel_notify_warning = combined.contains("kernel değişiklikten haberdar edilemedi")
|
||
|| combined.contains("kernel was not notified")
|
||
|| combined.contains("unable to inform the kernel")
|
||
|| combined.contains("still in use");
|
||
|
||
if is_kernel_notify_warning {
|
||
// Uyarıyı yok say; sync_partition_table kernel'i güncelleyecek
|
||
return Ok(());
|
||
}
|
||
|
||
// Gerçek bir hata
|
||
Err(combined.trim().to_string())
|
||
}
|
||
|
||
/// Standart `chroot` ile verilen komutu hedef dizinde çalıştırır.
|
||
/// Bind-mount'ların önceden yapılmış olduğu varsayılır (MountBindJob).
|
||
async fn run_chroot(mount: &str, cmd: &[&str]) -> Result<(), String> {
|
||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||
tokio::time::sleep(std::time::Duration::from_millis(2000)).await;
|
||
return Ok(());
|
||
}
|
||
|
||
// chroot <mount> <cmd[0]> [cmd[1..]...]
|
||
let mut args = vec![mount];
|
||
args.extend_from_slice(cmd);
|
||
|
||
let output = Command::new("chroot")
|
||
.args(&args)
|
||
.output()
|
||
.await
|
||
.map_err(|e| format!("chroot çalıştırılamadı: {}", e))?;
|
||
|
||
if output.status.success() {
|
||
Ok(())
|
||
} else {
|
||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||
Err(if stderr.is_empty() { stdout } else { stderr })
|
||
}
|
||
}
|
||
|
||
/// LUKS bölümünü güvenli şekilde formatlar.
|
||
///
|
||
/// Şifre, `sh -c` + string interpolasyon yerine doğrudan `stdin` pipe'ı
|
||
/// üzerinden iletilir; böylece shell injection saldırıları önlenir.
|
||
async fn run_luks_format(device: &str, password: &str) -> Result<(), String> {
|
||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
||
return Ok(());
|
||
}
|
||
|
||
use tokio::io::AsyncWriteExt;
|
||
|
||
let mut child = tokio::process::Command::new("cryptsetup")
|
||
.args([
|
||
"luksFormat", "-q", "--type", "luks2",
|
||
"--key-file", "-",
|
||
device,
|
||
])
|
||
.stdin(std::process::Stdio::piped())
|
||
.stdout(std::process::Stdio::null())
|
||
.stderr(std::process::Stdio::piped())
|
||
.spawn()
|
||
.map_err(|e| format!("cryptsetup luksFormat başlatılamadı: {}", e))?;
|
||
|
||
if let Some(mut stdin) = child.stdin.take() {
|
||
stdin.write_all(password.as_bytes()).await
|
||
.map_err(|e| format!("cryptsetup stdin yazma hatası: {}", e))?;
|
||
}
|
||
|
||
let output = child.wait_with_output().await
|
||
.map_err(|e| format!("cryptsetup bekleme hatası: {}", e))?;
|
||
|
||
if output.status.success() {
|
||
Ok(())
|
||
} else {
|
||
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
||
}
|
||
}
|
||
|
||
/// LUKS bölümünü güvenli şekilde açar (open).
|
||
///
|
||
/// Şifre, shell interpolasyon yerine `stdin` pipe'ı üzerinden iletilir.
|
||
async fn run_luks_open(device: &str, name: &str, password: &str) -> Result<(), String> {
|
||
if DEMO_MODE.load(Ordering::Relaxed) {
|
||
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||
return Ok(());
|
||
}
|
||
|
||
use tokio::io::AsyncWriteExt;
|
||
|
||
let mut child = tokio::process::Command::new("cryptsetup")
|
||
.args(["open", "--key-file", "-", device, name])
|
||
.stdin(std::process::Stdio::piped())
|
||
.stdout(std::process::Stdio::null())
|
||
.stderr(std::process::Stdio::piped())
|
||
.spawn()
|
||
.map_err(|e| format!("cryptsetup open başlatılamadı: {}", e))?;
|
||
|
||
if let Some(mut stdin) = child.stdin.take() {
|
||
stdin.write_all(password.as_bytes()).await
|
||
.map_err(|e| format!("cryptsetup open stdin yazma hatası: {}", e))?;
|
||
}
|
||
|
||
let output = child.wait_with_output().await
|
||
.map_err(|e| format!("cryptsetup open bekleme hatası: {}", e))?;
|
||
|
||
if output.status.success() {
|
||
Ok(())
|
||
} else {
|
||
Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
||
}
|
||
}
|
||
|
||
/// Bölümlendirme öncesi diski hazırlar:
|
||
/// mevcut bağlamaları ve swap'ı ayırır, ardından kernel'e yeni
|
||
/// bölüm tablosunu duyurarak aygıt düğümlerinin hazır olmasını bekler.
|
||
async fn prepare_disk(disk: &str, ui: &UiSender) {
|
||
if DEMO_MODE.load(Ordering::Relaxed) { return; }
|
||
|
||
// 1. Diskteki aktif swap'ları kapat
|
||
ui.log(format!("Swap ayrılıyor: {}…", disk));
|
||
let lsblk = tokio::process::Command::new("lsblk")
|
||
.args(["-o", "NAME,TYPE", "-l", "--noheadings", disk])
|
||
.output().await;
|
||
if let Ok(out) = lsblk {
|
||
let text = String::from_utf8_lossy(&out.stdout);
|
||
for line in text.lines() {
|
||
if line.contains("part") {
|
||
if let Some(name) = line.split_whitespace().next() {
|
||
let dev = format!("/dev/{}", name);
|
||
let _ = tokio::process::Command::new("swapoff")
|
||
.arg(&dev).status().await;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Disk bölümlerini umount et (hata görmezden gelinir)
|
||
ui.log(format!("Bağlantılar ayrılıyor: {}…", disk));
|
||
for n in 1..=32u32 {
|
||
let part = crate::installer::part_path(disk, n);
|
||
if !std::path::Path::new(&part).exists() { break; }
|
||
let _ = tokio::process::Command::new("umount")
|
||
.args(["-lf", &part]).status().await;
|
||
}
|
||
|
||
// 3. İlk senkronizasyon
|
||
let _ = tokio::process::Command::new("partprobe")
|
||
.arg(disk).status().await;
|
||
let _ = tokio::process::Command::new("udevadm")
|
||
.args(["settle", "--timeout=10"]).status().await;
|
||
}
|
||
|
||
/// Bölümlendirme sonrası kernel'e yeni tabloyu bildirir ve aygıt
|
||
/// düğümlerinin hazır olmasını bekler. `parted`'ın uyarı vermesi
|
||
/// normaldir; bu fonksiyon hata döndürmez.
|
||
async fn sync_partition_table(disk: &str) {
|
||
if DEMO_MODE.load(Ordering::Relaxed) { return; }
|
||
let _ = tokio::process::Command::new("partprobe")
|
||
.arg(disk).status().await;
|
||
let _ = tokio::process::Command::new("blockdev")
|
||
.args(["--rereadpt", disk]).status().await;
|
||
let _ = tokio::process::Command::new("udevadm")
|
||
.args(["settle", "--timeout=10"]).status().await;
|
||
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
|
||
}
|
||
|
||
/// ISO ortamında canlı sistemin kaynak dizinini otomatik tespit eder.
|
||
///
|
||
/// Sırasıyla:
|
||
/// 1. `/proc/mounts` içinde squashfs/overlay mount noktalarını tara
|
||
/// 2. Sabit aday listesini dene (her adayda bin/ + etc/ varlığı aranır)
|
||
/// 3. Hiçbiri bulunamazsa `/` (canlı sistem kökü) kullan — rsync
|
||
/// zaten /proc /sys /dev /run'u exclude eder.
|
||
fn has_root_fs(path: &std::path::Path) -> bool {
|
||
(path.join("bin").exists() || path.join("usr/bin").exists()) && path.join("etc").exists()
|
||
}
|
||
|
||
fn is_iso_mount(path: &str) -> bool {
|
||
let sqfs_check = [
|
||
"pisi/pisi.sqfs",
|
||
"live/pisi.sqfs",
|
||
"live/filesystem.squashfs",
|
||
"casper/filesystem.squashfs",
|
||
"live squashfs",
|
||
];
|
||
for pattern in &sqfs_check {
|
||
if std::path::Path::new(path).join(pattern).exists() {
|
||
return true;
|
||
}
|
||
}
|
||
false
|
||
}
|
||
|
||
pub fn detect_source_dir() -> String {
|
||
// 1. /proc/mounts içinde squashfs veya overlay mount noktalarını tara
|
||
if let Ok(mounts) = std::fs::read_to_string("/proc/mounts") {
|
||
for line in mounts.lines() {
|
||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||
if parts.len() < 3 { continue; }
|
||
let fs_type = parts[2];
|
||
let mount_pt = parts[1];
|
||
|
||
match fs_type {
|
||
"squashfs" => {
|
||
if mount_pt != "/" {
|
||
let p = std::path::Path::new(mount_pt);
|
||
if has_root_fs(p) {
|
||
return format!("{}/", mount_pt.trim_end_matches('/'));
|
||
}
|
||
}
|
||
}
|
||
"overlay" => {
|
||
// lowerdir parametresini ayıkla
|
||
let opts = parts.get(3).unwrap_or(&"");
|
||
for opt in opts.split(',') {
|
||
if let Some(lower) = opt.strip_prefix("lowerdir=") {
|
||
for dir in lower.split(':') {
|
||
let p = std::path::Path::new(dir);
|
||
if p.exists() && has_root_fs(p) {
|
||
return format!("{}/", dir.trim_end_matches('/'));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 2. Sabit aday listesi (her adayda bin/ + etc/ kontrolü)
|
||
// NOT: /bootmnt ISO kökü olabilir; altında pisi.sqfs varsa atlanır.
|
||
for path in &[
|
||
"/run/livecd/squashfs-root",
|
||
"/run/live/rootfs",
|
||
"/run/live/medium",
|
||
"/lib/live/mount/rootfs",
|
||
"/mnt/sqfs",
|
||
"/mnt/livecd",
|
||
"/mnt/cdrom",
|
||
"/bootmnt/live",
|
||
"/bootmnt/livecd",
|
||
] {
|
||
let p = std::path::Path::new(path);
|
||
if p.exists() && has_root_fs(p) {
|
||
return format!("{}/", path);
|
||
}
|
||
}
|
||
|
||
// 2b. /bootmnt son çare; ama ISO köküyse (pisi.sqfs varsa) atla
|
||
let bootmnt = std::path::Path::new("/bootmnt");
|
||
if bootmnt.exists() && !is_iso_mount("/bootmnt") && has_root_fs(bootmnt) {
|
||
return "/bootmnt/".to_string();
|
||
}
|
||
|
||
// 3. Hiçbiri bulunamazsa canlı sistem kökü
|
||
"/".to_string()
|
||
}
|
||
|
||
/// Kurulum kuyruğu için ortak yapılandırma.
|
||
pub struct JobQueueConfig {
|
||
pub mount: String,
|
||
pub source: String,
|
||
pub locale: String,
|
||
pub timezone: String,
|
||
pub username: String,
|
||
pub password: String,
|
||
pub hostname: String,
|
||
pub kb_layout: String,
|
||
pub kb_variant: String,
|
||
pub demo_mode: bool,
|
||
pub boot_device: String,
|
||
pub bootloader_timeout: u32,
|
||
pub bootloader_password: String,
|
||
pub kernel_options: String,
|
||
pub use_ntp: bool,
|
||
pub manual_year: i32,
|
||
pub manual_month: u32,
|
||
pub manual_day: u32,
|
||
pub manual_hour: u32,
|
||
pub manual_minute: u32,
|
||
pub root_password: String,
|
||
pub display_manager: String,
|
||
pub autologin: bool,
|
||
pub desktop_environment: String,
|
||
pub selected_package_groups: Vec<String>,
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// FABRİKA: STANDART KURULUM KUYRUĞU
|
||
// ─────────────────────────────────────────────
|
||
|
||
// ─────────────────────────────────────────────
|
||
// PARTITION JOB (otomatik bölümleme)
|
||
// ─────────────────────────────────────────────
|
||
|
||
use crate::installer::{PartitionPlan, PartitionTableType};
|
||
|
||
/// `parted` ve `mkfs` araçlarını kullanarak diski otomatik bölümlendirir.
|
||
/// Plan, PartitionStep'te hesaplanmış ve GlobalState'te saklanmıştır.
|
||
pub struct PartitionJob {
|
||
pub plan: PartitionPlan,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for PartitionJob {
|
||
fn name(&self) -> &str { "job_partition" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let disk = &self.plan.disk;
|
||
let is_gpt = self.plan.table_type == PartitionTableType::Gpt;
|
||
|
||
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
|
||
prepare_disk(disk, ui).await;
|
||
|
||
// 1. Bölüm tablosu oluştur
|
||
let table_label = if is_gpt { "gpt" } else { "msdos" };
|
||
ui.log(format!("parted {} mktable {}", disk, table_label));
|
||
run_parted(&["-s", disk, "mktable", table_label]).await?;
|
||
|
||
let mut next_mb: u64 = 1; // İlk MB'ı boot kayıtları için bırak
|
||
|
||
// 2. EFI bölümü (yalnızca GPT/UEFI)
|
||
if self.plan.efi_mb > 0 {
|
||
let end_mb = next_mb + self.plan.efi_mb;
|
||
ui.log(format!("EFI bölümü oluşturuluyor: {}–{} MB", next_mb, end_mb));
|
||
run_parted(&[
|
||
"-s", disk, "mkpart", "fat32",
|
||
&format!("{}MB", next_mb), &format!("{}MB", end_mb),
|
||
]).await?;
|
||
run_parted(&["-s", disk, "set", "1", "esp", "on"]).await?;
|
||
next_mb = end_mb;
|
||
}
|
||
|
||
// 3. Swap bölümü
|
||
let swap_end = next_mb + self.plan.swap_mb;
|
||
let swap_num: u32 = if self.plan.efi_mb > 0 { 2 } else { 1 };
|
||
ui.log(format!("Swap bölümü oluşturuluyor: {}–{} MB", next_mb, swap_end));
|
||
run_parted(&[
|
||
"-s", disk, "mkpart", "linux-swap",
|
||
&format!("{}MB", next_mb), &format!("{}MB", swap_end),
|
||
]).await?;
|
||
next_mb = swap_end;
|
||
|
||
// 4. Kök bölümü (kalan alan)
|
||
let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 };
|
||
ui.log(format!("Kök bölümü oluşturuluyor: {}MB – disk sonu", next_mb));
|
||
run_parted(&[
|
||
"-s", disk, "mkpart", "ext4",
|
||
&format!("{}MB", next_mb), "100%",
|
||
]).await?;
|
||
|
||
// 4.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle
|
||
sync_partition_table(disk).await;
|
||
|
||
// 5. Biçimlendirme
|
||
let efi_part = part_path(disk, 1);
|
||
let swap_part = part_path(disk, swap_num);
|
||
let root_part = part_path(disk, root_num);
|
||
|
||
if self.plan.efi_mb > 0 {
|
||
ui.log(format!("mkfs.fat -F32 {}", efi_part));
|
||
run_cmd("mkfs.fat", &["-F32", &efi_part]).await?;
|
||
}
|
||
|
||
ui.log(format!("mkswap {}", swap_part));
|
||
run_cmd("mkswap", &[&swap_part]).await?;
|
||
|
||
ui.log(format!("swapon {}", swap_part));
|
||
run_cmd("swapon", &[&swap_part]).await?;
|
||
|
||
// LUKS şifreleme (kök bölümü)
|
||
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
|
||
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
|
||
let root_device = if self.plan.encrypt_root && !self.plan.luks_password.is_empty() {
|
||
let luks_name = "cryptroot";
|
||
ui.log(format!("cryptsetup luksFormat {}…", root_part));
|
||
run_luks_format(&root_part, &self.plan.luks_password).await
|
||
.map_err(|e| format!("LUKS format hatası: {}", e))?;
|
||
|
||
ui.log(format!("cryptsetup open {} {}…", root_part, luks_name));
|
||
run_luks_open(&root_part, luks_name, &self.plan.luks_password).await
|
||
.map_err(|e| format!("LUKS open hatası: {}", e))?;
|
||
|
||
format!("/dev/mapper/{}", luks_name)
|
||
} else {
|
||
root_part.clone()
|
||
};
|
||
|
||
ui.log(format!("mkfs.ext4 -F {}", root_device));
|
||
run_cmd("mkfs.ext4", &["-F", &root_device]).await?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Bölüm yolunu bağla.
|
||
pub struct MountPartitionsJob {
|
||
pub plan: PartitionPlan,
|
||
pub mount: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for MountPartitionsJob {
|
||
fn name(&self) -> &str { "job_mount" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let disk = &self.plan.disk;
|
||
let mount = &self.mount;
|
||
let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 };
|
||
let root_part = part_path(disk, root_num);
|
||
|
||
let root_device = if self.plan.encrypt_root {
|
||
"/dev/mapper/cryptroot".to_string()
|
||
} else {
|
||
root_part.clone()
|
||
};
|
||
|
||
ui.log(format!("mount {} {}", root_device, mount));
|
||
run_cmd("mount", &[&root_device, mount]).await?;
|
||
|
||
if self.plan.efi_mb > 0 {
|
||
let efi_dir = format!("{}/boot/efi", mount);
|
||
ui.log(format!("mkdir -p {}", efi_dir));
|
||
tokio::fs::create_dir_all(&efi_dir).await
|
||
.map_err(|e| format!("{} oluşturulamadı: {}", efi_dir, e))?;
|
||
let efi_part = part_path(disk, 1);
|
||
ui.log(format!("mount {} {}", efi_part, efi_dir));
|
||
run_cmd("mount", &[&efi_part, &efi_dir]).await?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Tam kurulum iş kuyruğu.
|
||
///
|
||
/// Sıra:
|
||
/// 1. Disk bölümlendirme (parted + mkfs)
|
||
/// 2. Bölümleri bağlama
|
||
/// 3. /proc /sys /dev bind-mount (chroot hazırlığı)
|
||
/// 4. Sistem dosyalarını kopyalama (rsync)
|
||
/// 5. UUID'leri oku → fstab yaz
|
||
/// 6. Hostname + /etc/hosts
|
||
/// 7. Locale yapılandırması + locale-gen
|
||
/// 8. Zaman dilimi + hwclock
|
||
/// 9. Klavye (vconsole + X11)
|
||
/// 10. Kullanıcı oluşturma + şifre (chpasswd)
|
||
/// 11. initramfs yenile (mkinitramfs / dracut / mkinitcpio)
|
||
/// 12. GRUB kurulumu (chroot içinde)
|
||
/// 13. COMAR / pisi configure-pending
|
||
/// 14. ldconfig
|
||
/// 15. update-environment (PisiLinux init)
|
||
/// 16. Live ortam temizliği
|
||
/// 17. Bind-mount'ları temizle
|
||
pub fn build_full_job_queue(
|
||
plan: PartitionPlan,
|
||
cfg: JobQueueConfig,
|
||
) -> JobQueue {
|
||
DEMO_MODE.store(cfg.demo_mode, std::sync::atomic::Ordering::Relaxed);
|
||
|
||
let is_uefi = plan.efi_mb > 0;
|
||
let encrypt_root = plan.encrypt_root;
|
||
let disk = plan.disk.clone();
|
||
let mt = cfg.mount.clone();
|
||
let plan2 = plan.clone();
|
||
let plan3 = plan.clone();
|
||
|
||
// Kaynak dizini: parametre boşsa otomatik tespit et
|
||
let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source };
|
||
|
||
let mut jobs: Vec<Box<dyn Job>> = vec![
|
||
// 1. Bölümlendir
|
||
Box::new(PartitionJob { plan }),
|
||
// 2. Bağla
|
||
Box::new(MountPartitionsJob { plan: plan2, mount: mt.clone() }),
|
||
// 3. /proc /sys /dev bind-mount
|
||
Box::new(MountBindJob { mount: mt.clone() }),
|
||
// 4. Kopyala
|
||
Box::new(CopyFilesJob { source: src, target: mt.clone() }),
|
||
// 5. UUID oku + fstab yaz
|
||
Box::new(FstabFromBlkidJob { plan: plan3, mount: mt.clone() }),
|
||
// 6. Hostname
|
||
Box::new(ConfigureHostnameJob {
|
||
mount: mt.clone(),
|
||
hostname: cfg.hostname.clone(),
|
||
}),
|
||
// 7. Locale
|
||
Box::new(ConfigureLocaleJob {
|
||
mount: mt.clone(),
|
||
locale: cfg.locale.clone(),
|
||
}),
|
||
// 8. Zaman dilimi
|
||
Box::new(ConfigureTimezoneJob {
|
||
mount: mt.clone(),
|
||
timezone: cfg.timezone.clone(),
|
||
}),
|
||
// 9. Tarih/Saat (NTP)
|
||
Box::new(ConfigureDateTimeJob {
|
||
mount: mt.clone(),
|
||
use_ntp: cfg.use_ntp,
|
||
year: cfg.manual_year,
|
||
month: cfg.manual_month,
|
||
day: cfg.manual_day,
|
||
hour: cfg.manual_hour,
|
||
minute: cfg.manual_minute,
|
||
}),
|
||
// 10. Klavye
|
||
Box::new(ConfigureKeyboardJob {
|
||
mount: mt.clone(),
|
||
layout: cfg.kb_layout.clone(),
|
||
variant: cfg.kb_variant.clone(),
|
||
}),
|
||
// 11. Kullanıcı
|
||
Box::new(CreateUserJob {
|
||
mount: mt.clone(),
|
||
username: cfg.username.clone(),
|
||
password: cfg.password.clone(),
|
||
root_password: cfg.root_password.clone(),
|
||
}),
|
||
// 12. initramfs
|
||
Box::new(GenerateInitramfsJob { mount: mt.clone(), encrypt_root, lvm_active: false }),
|
||
];
|
||
|
||
// 13. Bootloader (GRUB)
|
||
jobs.push(Box::new(InstallGrubJob {
|
||
disk: disk.clone(),
|
||
mount: mt.clone(),
|
||
is_uefi,
|
||
boot_device: cfg.boot_device.clone(),
|
||
timeout: cfg.bootloader_timeout,
|
||
password: cfg.bootloader_password.clone(),
|
||
kernel_options: cfg.kernel_options.clone(),
|
||
encrypt_root,
|
||
}));
|
||
|
||
// 14. Ekran yöneticisi
|
||
jobs.push(Box::new(ConfigureDisplayManagerJob {
|
||
mount: mt.clone(),
|
||
display_manager: cfg.display_manager.clone(),
|
||
autologin: cfg.autologin,
|
||
username: cfg.username.clone(),
|
||
desktop_environment: cfg.desktop_environment.clone(),
|
||
}));
|
||
|
||
// 15. COMAR / pisi configure-pending
|
||
jobs.push(Box::new(RunComarJob { mount: mt.clone() }));
|
||
|
||
// 16. ldconfig
|
||
jobs.push(Box::new(RunLdconfigJob { mount: mt.clone() }));
|
||
|
||
// 17. update-environment (PisiLinux init yapılandırması)
|
||
jobs.push(Box::new(UpdateEnvironmentJob { mount: mt.clone() }));
|
||
|
||
// 18. Ek paket grupları (netinstall)
|
||
if !cfg.selected_package_groups.is_empty() {
|
||
jobs.push(Box::new(InstallExtraPackagesJob {
|
||
mount: mt.clone(),
|
||
package_groups: cfg.selected_package_groups.clone(),
|
||
}));
|
||
}
|
||
|
||
// 19. Live ortam izlerini temizle
|
||
jobs.push(Box::new(CleanupLiveJob { mount: mt.clone() }));
|
||
|
||
// 20. Bind-mount temizle
|
||
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
|
||
|
||
JobQueue::new(jobs)
|
||
}
|
||
|
||
/// Ekran yöneticisi (SDDM/LightDM/GDM) yapılandırması.
|
||
pub struct ConfigureDisplayManagerJob {
|
||
pub mount: String,
|
||
pub display_manager: String,
|
||
pub autologin: bool,
|
||
pub username: String,
|
||
pub desktop_environment: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for ConfigureDisplayManagerJob {
|
||
fn name(&self) -> &str { "job_display_manager" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let dm = &self.display_manager;
|
||
ui.log(format!("Ekran yöneticisi yapılandırılıyor: {}", dm));
|
||
|
||
match dm.as_str() {
|
||
"sddm" => {
|
||
let conf = format!("{}/usr/lib/sddm/sddm.conf.d/sddm.conf", self.mount);
|
||
let session = match self.desktop_environment.as_str() {
|
||
"plasma" => "plasmax11.desktop",
|
||
"xfce" => "xfce.desktop",
|
||
"gnome" => "gnome.desktop",
|
||
"cinnamon" => "cinnamon.desktop",
|
||
"budgie" => "budgie-desktop.desktop",
|
||
"lxde" => "LXDE.desktop",
|
||
"lxqt" => "lxqt.desktop",
|
||
"mate" => "mate.desktop",
|
||
"lumina" => "lumina-desktop.desktop",
|
||
_ => "plasmax11.desktop",
|
||
};
|
||
let content = if self.autologin {
|
||
format!(
|
||
"[Autologin]\nUser={}\nSession={}\n\n[Theme]\nCurrent=pisilinux24\n",
|
||
self.username, session
|
||
)
|
||
} else {
|
||
"[Autologin]\nUser=\nSession=\n\n[Theme]\nCurrent=pisilinux24\n".to_string()
|
||
};
|
||
tokio::fs::write(&conf, content)
|
||
.await
|
||
.map_err(|e| format!("sddm.conf yazılamadı: {}", e))?;
|
||
}
|
||
"lightdm" => {
|
||
let conf = format!("{}/etc/lightdm/lightdm.conf", self.mount);
|
||
// Mevcut dosyayı oku veya baştan yaz
|
||
let mut content = String::new();
|
||
if let Ok(existing) = tokio::fs::read_to_string(&conf).await {
|
||
content = existing;
|
||
}
|
||
if self.autologin {
|
||
let auto_lines = format!(
|
||
"[Seat:*]\nautologin-user={}\nautologin-session={}\n",
|
||
self.username, self.desktop_environment
|
||
);
|
||
content.push_str(&auto_lines);
|
||
}
|
||
tokio::fs::write(&conf, content)
|
||
.await
|
||
.map_err(|e| format!("lightdm.conf yazılamadı: {}", e))?;
|
||
}
|
||
"gdm" => {
|
||
let dir = format!("{}/etc/gdm", self.mount);
|
||
let _ = tokio::fs::create_dir_all(&dir).await;
|
||
let conf = format!("{}/etc/gdm/custom.conf", self.mount);
|
||
let session = match self.desktop_environment.as_str() {
|
||
"plasma" => "plasmax11.desktop",
|
||
"xfce" => "xfce.desktop",
|
||
"gnome" => "gnome.desktop",
|
||
"cinnamon" => "cinnamon.desktop",
|
||
"budgie" => "budgie-desktop.desktop",
|
||
"lxde" => "LXDE.desktop",
|
||
"lxqt" => "lxqt.desktop",
|
||
"mate" => "mate.desktop",
|
||
"lumina" => "lumina-desktop.desktop",
|
||
_ => "gnome.desktop",
|
||
};
|
||
let content = if self.autologin {
|
||
format!(
|
||
"[daemon]\nAutomaticLoginEnable=true\nAutomaticLogin={}\nDefaultSession={}\n",
|
||
self.username, session
|
||
)
|
||
} else {
|
||
"[daemon]\n".to_string()
|
||
};
|
||
tokio::fs::write(&conf, content)
|
||
.await
|
||
.map_err(|e| format!("gdm/custom.conf yazılamadı: {}", e))?;
|
||
}
|
||
_ => {
|
||
ui.log(format!("⚠ Bilinmeyen ekran yöneticisi: {}", dm));
|
||
}
|
||
}
|
||
|
||
ui.log("✓ Ekran yöneticisi yapılandırması tamamlandı");
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Ek paket gruplarını chroot içinde kurar.
|
||
pub struct InstallExtraPackagesJob {
|
||
pub mount: String,
|
||
pub package_groups: Vec<String>,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for InstallExtraPackagesJob {
|
||
fn name(&self) -> &str { "job_netinstall" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
use crate::steps::netinstall::PACKAGE_GROUPS;
|
||
|
||
let mut packages: Vec<&str> = Vec::new();
|
||
for (key, _, pkgs) in PACKAGE_GROUPS {
|
||
if self.package_groups.contains(&key.to_string()) {
|
||
packages.extend_from_slice(pkgs);
|
||
}
|
||
}
|
||
|
||
if packages.is_empty() {
|
||
ui.log("Ek paket seçilmedi, atlanıyor.");
|
||
return Ok(());
|
||
}
|
||
|
||
let pkg_list = packages.join(" ");
|
||
ui.log(format!("Ek paketler kuruluyor: {}", pkg_list));
|
||
match run_chroot(&self.mount, &["pisi", "install", &pkg_list]).await {
|
||
Ok(_) => ui.log("✓ Ek paketler kuruldu"),
|
||
Err(e) => {
|
||
ui.log(format!("⚠ pisi install uyarısı: {}", e));
|
||
ui.log("⚠ Bu hata kritik değilse kurulum devam edecek.");
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// COMAR servislerini ve tetikleyicilerini yapılandırır.
|
||
pub struct RunComarJob {
|
||
pub mount: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for RunComarJob {
|
||
fn name(&self) -> &str { "job_comar" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
// pisi configure-pending: paket post-install scriptlerini çalıştırır.
|
||
// Yapılandırılacak paket yoksa çıkış kodu sıfır dışı olabilir — fatal değil.
|
||
ui.log("pisi configure-pending çalıştırılıyor (COMAR)…");
|
||
match run_chroot(&self.mount, &["pisi", "configure-pending"]).await {
|
||
Ok(_) => {
|
||
ui.log("✓ pisi configure-pending tamamlandı");
|
||
}
|
||
Err(e) => {
|
||
// "no packages to configure" gibi durumlar hata değil
|
||
if e.contains("no packages") || e.contains("Nothing to do") {
|
||
ui.log("⚠ configure-pending: yapılandırılacak paket yok, atlanıyor");
|
||
} else {
|
||
// Gerçek hata — uyar ama kurulumu durdurma
|
||
ui.log(format!("⚠ pisi configure-pending uyarısı: {}", e));
|
||
ui.log("⚠ Bu uyarı kritik değilse kurulum devam edecek.");
|
||
}
|
||
}
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// ldconfig: paylaşımlı kütüphane önbelleğini yeniden oluşturur.
|
||
pub struct RunLdconfigJob {
|
||
pub mount: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for RunLdconfigJob {
|
||
fn name(&self) -> &str { "job_ldconfig" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
ui.log("ldconfig çalıştırılıyor…");
|
||
match run_chroot(&self.mount, &["ldconfig"]).await {
|
||
Ok(_) => ui.log("✓ ldconfig tamamlandı"),
|
||
Err(e) => ui.log(format!("⚠ ldconfig uyarısı: {}", e)),
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// update-environment: PisiLinux init/betiklerini yeniden yapılandırır.
|
||
pub struct UpdateEnvironmentJob {
|
||
pub mount: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for UpdateEnvironmentJob {
|
||
fn name(&self) -> &str { "job_update_env" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
ui.log("update-environment çalıştırılıyor…");
|
||
match run_chroot(&self.mount, &["update-environment"]).await {
|
||
Ok(_) => ui.log("✓ update-environment tamamlandı"),
|
||
Err(e) => ui.log(format!("⚠ update-environment uyarısı: {}", e)),
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Live ortam izlerini hedef sistemden temizler.
|
||
pub struct CleanupLiveJob {
|
||
pub mount: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for CleanupLiveJob {
|
||
fn name(&self) -> &str { "job_cleanup_live" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let mt = &self.mount;
|
||
ui.log("Live ortam izleri temizleniyor…");
|
||
|
||
// run/livemedia
|
||
let _ = tokio::fs::remove_dir_all(format!("{}/run/livemedia", mt)).await;
|
||
// var/cache/pisi/packages
|
||
let _ = tokio::fs::remove_dir_all(format!("{}/var/cache/pisi/packages", mt)).await;
|
||
// YALI desktop dosyaları
|
||
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali.desktop", mt)).await;
|
||
//let _ = tokio::fs::remove_file(format!("{}/usr/share/applications/yali-bin.desktop", mt)).await;
|
||
let _ = tokio::fs::remove_dir_all(format!("{}/bootmnt", mt)).await;
|
||
//pisi kullanıcısını sil (ev diziniyle birlikte), YALI paketlerini kaldır
|
||
let _ = run_chroot(&self.mount, &["userdel", "-r", "pisi"]).await;
|
||
let _ = run_chroot(&self.mount, &["pisi", "rm", "yali-rs"]).await;
|
||
// live paket deposunu kaldır
|
||
let _ = run_chroot(&self.mount, &["pisi", "rr", "live"]).await;
|
||
// depolarları yeniden ekle (stable2 + contrib)
|
||
ui.log("Depolar Ekleniyor…");
|
||
let _ = run_chroot(&self.mount, &["pisi", "ar", "stable2", "https://stable2.pisilinux.org/pisi-index.xml.xz"]).await;
|
||
let _ = run_chroot(&self.mount, &["pisi", "ar", "contrib", "https://contrib.pisilinux.org/pisi-index.xml.xz"]).await;
|
||
// depoları aktif et
|
||
ui.log("Depolar aktif ediliyor...");
|
||
let _ = run_chroot(&self.mount, &["pisi", "er", "contrib", "stable2"]).await;
|
||
// paket listelerini güncelle
|
||
ui.log("Depolar Güncelleniyor...");
|
||
let _ = run_chroot(&self.mount, &["pisi", "ur"]).await;
|
||
|
||
ui.log("✓ Live temizlik tamamlandı");
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// blkid ile UUID okuyup fstab'ı doğrudan yazan job.
|
||
/// (async closure Rust'ta trait object olarak kullanılamaz;
|
||
/// bu nedenle ayrı bir Job struct'ı gerekli.)
|
||
struct FstabFromBlkidJob {
|
||
plan: PartitionPlan,
|
||
mount: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for FstabFromBlkidJob {
|
||
fn name(&self) -> &str { "job_fstab" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
ui.log("blkid ile UUID'ler okunuyor…");
|
||
let (root_uuid, efi_uuid, swap_uuid) = read_partition_uuids(&self.plan).await?;
|
||
ui.log(format!("Kök UUID: {}", root_uuid));
|
||
|
||
let job = GenerateFstabJob {
|
||
mount: self.mount.clone(),
|
||
root_uuid,
|
||
efi_uuid,
|
||
swap_uuid,
|
||
encrypt_root: self.plan.encrypt_root,
|
||
};
|
||
job.run(ui).await
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// CHROOT YAPILANDIRMA JOB'LARI
|
||
// ─────────────────────────────────────────────
|
||
|
||
/// /etc/hostname ve /etc/hosts dosyalarını yazar.
|
||
pub struct ConfigureHostnameJob {
|
||
pub mount: String,
|
||
pub hostname: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for ConfigureHostnameJob {
|
||
fn name(&self) -> &str { "job_hostname" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let etc_dir = format!("{}/etc", self.mount);
|
||
let _ = tokio::fs::create_dir_all(&etc_dir).await;
|
||
|
||
// /etc/hostname
|
||
let hostname_path = format!("{}/etc/hostname", self.mount);
|
||
ui.log(format!("Yazılıyor: {}", hostname_path));
|
||
tokio::fs::write(&hostname_path, format!("{}\n", self.hostname))
|
||
.await
|
||
.map_err(|e| format!("/etc/hostname yazılamadı: {}", e))?;
|
||
|
||
// /etc/hosts
|
||
let hosts_path = format!("{}/etc/hosts", self.mount);
|
||
let hosts_content = format!(
|
||
"127.0.0.1\tlocalhost\n\
|
||
::1\t\tlocalhost\n\
|
||
127.0.1.1\t{hostname}\n",
|
||
hostname = self.hostname
|
||
);
|
||
ui.log(format!("Yazılıyor: {}", hosts_path));
|
||
tokio::fs::write(&hosts_path, hosts_content)
|
||
.await
|
||
.map_err(|e| format!("/etc/hosts yazılamadı: {}", e))?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// /etc/locale.conf ve /etc/locale.gen'i yazar, sonra locale-gen çalıştırır.
|
||
pub struct ConfigureLocaleJob {
|
||
pub mount: String,
|
||
pub locale: String, // ör. "tr_TR.UTF-8"
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for ConfigureLocaleJob {
|
||
fn name(&self) -> &str { "job_locale" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
// Locale değeri doğrulama: yalnızca alfanumerik, '.', '_', '-', '@'
|
||
// karakterlerine izin ver. Yol manipülasyonu ve injection önlenir.
|
||
let locale_valid = !self.locale.is_empty()
|
||
&& !self.locale.contains("..")
|
||
&& !self.locale.contains('/')
|
||
&& !self.locale.contains('\n')
|
||
&& self.locale.chars().all(|c| c.is_alphanumeric() || "._-@".contains(c));
|
||
if !locale_valid {
|
||
return Err(format!("Geçersiz locale değeri: '{}'", self.locale));
|
||
}
|
||
|
||
let etc_dir = format!("{}/etc", self.mount);
|
||
let _ = tokio::fs::create_dir_all(&etc_dir).await;
|
||
let _ = tokio::fs::create_dir_all(format!("{}/etc/env.d", self.mount)).await;
|
||
|
||
// /etc/locale.conf (systemd-style)
|
||
let conf_path = format!("{}/etc/locale.conf", self.mount);
|
||
let conf = format!(
|
||
"LANG={locale}\nLC_ALL={locale}\n",
|
||
locale = self.locale
|
||
);
|
||
ui.log(format!("Yazılıyor: {}", conf_path));
|
||
let _ = tokio::fs::write(&conf_path, conf).await;
|
||
|
||
// /etc/env.d/03locale (PisiLinux native)
|
||
let pisi_locale_path = format!("{}/etc/env.d/03locale", self.mount);
|
||
let pisi_locale = format!(
|
||
"LANG={locale}\nLC_ALL={locale}\n",
|
||
locale = self.locale
|
||
);
|
||
ui.log(format!("Yazılıyor: {}", pisi_locale_path));
|
||
let _ = tokio::fs::write(&pisi_locale_path, pisi_locale).await;
|
||
|
||
// /etc/locale.gen — satırın başındaki # kaldırılır
|
||
let gen_path = format!("{}/etc/locale-gen", self.mount);
|
||
ui.log(format!("Güncelleniyor: {}", gen_path));
|
||
let content = tokio::fs::read_to_string(&gen_path).await.unwrap_or_default();
|
||
let updated = content
|
||
.lines()
|
||
.map(|line| {
|
||
let stripped = line.trim_start_matches('#').trim_start();
|
||
if stripped.starts_with(&self.locale) {
|
||
stripped.to_string() // yorumu kaldır
|
||
} else {
|
||
line.to_string()
|
||
}
|
||
})
|
||
.collect::<Vec<_>>()
|
||
.join("\n");
|
||
tokio::fs::write(&gen_path, updated + "\n")
|
||
.await
|
||
.map_err(|e| format!("/etc/locale-gen yazılamadı: {}", e))?;
|
||
|
||
// locale-gen çalıştır — kurulum sonrası locale desteği için zorunlu
|
||
ui.log("locale-gen çalıştırılıyor…");
|
||
run_chroot(&self.mount, &["locale-gen"]).await?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Zaman dilimini ayarlar ve hwclock'u senkronize eder.
|
||
pub struct ConfigureTimezoneJob {
|
||
pub mount: String,
|
||
pub timezone: String, // ör. "Europe/Istanbul"
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for ConfigureTimezoneJob {
|
||
fn name(&self) -> &str { "job_timezone" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
// Path traversal koruması: timezone değeri yalnızca alfanumerik,
|
||
// '/', '_', '-', '+' karakterlerine izin verir; ".." asla geçemez.
|
||
let tz = &self.timezone;
|
||
let tz_valid = !tz.is_empty()
|
||
&& !tz.contains("..")
|
||
&& tz.chars().all(|c| c.is_alphanumeric() || "/._-+".contains(c));
|
||
if !tz_valid {
|
||
return Err(format!("Geçersiz timezone değeri: '{}'", tz));
|
||
}
|
||
|
||
// Sembolik link: /etc/localtime → /usr/share/zoneinfo/<tz>
|
||
let link_target = format!("/usr/share/zoneinfo/{}", self.timezone);
|
||
let link_path = format!("{}/etc/localtime", self.mount);
|
||
|
||
ui.log(format!("ln -sf {} {}", link_target, link_path));
|
||
// Önce varsa eski linki sil
|
||
let _ = tokio::fs::remove_file(&link_path).await;
|
||
// tokio'da Unix symlink için spawn_blocking kullan
|
||
let lt = link_target.clone();
|
||
let lp = link_path.clone();
|
||
tokio::task::spawn_blocking(move || {
|
||
std::os::unix::fs::symlink(<, &lp)
|
||
}).await
|
||
.map_err(|e| format!("symlink spawn hatası: {}", e))?
|
||
.map_err(|e| format!("localtime symlink: {}", e))?;
|
||
|
||
// hwclock --systohc
|
||
ui.log("hwclock --systohc çalıştırılıyor…");
|
||
// Hata kritik değil, uyar ve devam et
|
||
match run_chroot(&self.mount, &["hwclock", "--systohc"]).await {
|
||
Ok(_) => ui.log("hwclock senkronize edildi."),
|
||
Err(e) => ui.log(format!("⚠ hwclock uyarısı (kritik değil): {}", e)),
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// NTP ve manuel tarih/saat yapılandırması.
|
||
pub struct ConfigureDateTimeJob {
|
||
pub mount: String,
|
||
pub use_ntp: bool,
|
||
pub year: i32,
|
||
pub month: u32,
|
||
pub day: u32,
|
||
pub hour: u32,
|
||
pub minute: u32,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for ConfigureDateTimeJob {
|
||
fn name(&self) -> &str { "job_datetime" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
if self.use_ntp {
|
||
ui.log("NTP etkinleştiriliyor…");
|
||
// systemd-timesyncd veya ntpd'yi etkinleştir
|
||
let _ = run_chroot(&self.mount, &["systemctl", "enable", "systemd-timesyncd"]).await;
|
||
let _ = run_chroot(&self.mount, &["systemctl", "start", "systemd-timesyncd"]).await;
|
||
} else {
|
||
ui.log("Manuel tarih/saat ayarlanıyor…");
|
||
let date_str = format!(
|
||
"{:04}-{:02}-{:02} {:02}:{:02}:00",
|
||
self.year, self.month, self.day, self.hour, self.minute
|
||
);
|
||
let _ = run_chroot(&self.mount, &["date", "-s", &date_str]).await;
|
||
let _ = run_chroot(&self.mount, &["hwclock", "--systohc"]).await;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Klavye düzenini /etc/vconsole.conf ve X11 yapılandırmasına yazar.
|
||
pub struct ConfigureKeyboardJob {
|
||
pub mount: String,
|
||
pub layout: String, // ör. "tr"
|
||
pub variant: String, // ör. "f" veya ""
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for ConfigureKeyboardJob {
|
||
fn name(&self) -> &str { "job_keyboard" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
// /etc/vconsole.conf (systemd-style TTY)
|
||
let vconsole_path = format!("{}/etc/vconsole.conf", self.mount);
|
||
let keymap = if self.variant.is_empty() {
|
||
self.layout.clone()
|
||
} else {
|
||
format!("{}-{}", self.layout, self.variant)
|
||
};
|
||
let vconsole = format!("KEYMAP={}\nFONT=ter-v16n\n", keymap);
|
||
ui.log(format!("Yazılıyor: {}", vconsole_path));
|
||
let _ = tokio::fs::write(&vconsole_path, vconsole).await;
|
||
|
||
// /etc/conf.d/mudur (PisiLinux native init)
|
||
let mudur_path = format!("{}/etc/conf.d/mudur", self.mount);
|
||
let mudur_conf = format!("keymap={}\nfont=ter-v16n\n", keymap);
|
||
ui.log(format!("Yazılıyor: {}", mudur_path));
|
||
let _ = tokio::fs::write(&mudur_path, mudur_conf).await;
|
||
|
||
// /etc/X11/xorg.conf.d/00-keyboard.conf (X11 için)
|
||
let x11_dir = format!("{}/etc/X11/xorg.conf.d", self.mount);
|
||
let x11_path = format!("{}/00-keyboard.conf", x11_dir);
|
||
tokio::fs::create_dir_all(&x11_dir).await
|
||
.map_err(|e| format!("{} oluşturulamadı: {}", x11_dir, e))?;
|
||
|
||
let options_line = if self.variant.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!("\n\t\tOption \"XkbVariant\" \"{}\"", self.variant)
|
||
};
|
||
|
||
let x11_conf = format!(
|
||
"Section \"InputClass\"\n\
|
||
\tIdentifier \"system-keyboard\"\n\
|
||
\tMatchIsKeyboard \"on\"\n\
|
||
\tOption \"XkbLayout\" \"{layout}\"{options}\n\
|
||
EndSection\n",
|
||
layout = self.layout,
|
||
options = options_line,
|
||
);
|
||
ui.log(format!("Yazılıyor: {}", x11_path));
|
||
tokio::fs::write(&x11_path, x11_conf)
|
||
.await
|
||
.map_err(|e| format!("xorg keyboard conf yazılamadı: {}", e))?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Kullanıcı hesabı oluşturur ve şifresini ayarlar.
|
||
pub struct CreateUserJob {
|
||
pub mount: String,
|
||
pub username: String,
|
||
pub password: String,
|
||
pub root_password: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for CreateUserJob {
|
||
fn name(&self) -> &str { "job_user" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
// wheel grubunun varlığını garantile (PisiLinux'ta olmayabilir)
|
||
let group_file = format!("{}/etc/group", self.mount);
|
||
let group_content = tokio::fs::read_to_string(&group_file)
|
||
.await.unwrap_or_default();
|
||
if !group_content.lines().any(|l| l.starts_with("wheel:")) {
|
||
ui.log("wheel grubu oluşturuluyor…");
|
||
run_chroot(&self.mount, &["groupadd", "-r", "wheel"]).await
|
||
.unwrap_or_default(); // hata olsa bile devam et
|
||
}
|
||
|
||
// useradd: kullanıcıyı temel gruplarla oluştur
|
||
// PisiLinux'ta "storage" grubu yoksa useradd hata verir; -M flag ile grub yoksa atla
|
||
// Güvenli yöntem: önce her grubu tek tek ekle
|
||
let groups = ["users", "pnp", "disk", "audio", "video", "power", "dialout", "lp", "lpadmin", "cdrom", "floppy"];
|
||
for grp in &groups {
|
||
if group_content.lines().any(|l| l.starts_with(&format!("{}:", grp))) {
|
||
// grup zaten var
|
||
} else {
|
||
// grup yoksa oluşturma (hata görmezden gel)
|
||
let _ = run_chroot(&self.mount, &["groupadd", grp]).await;
|
||
}
|
||
}
|
||
|
||
// Kullanıcıyı oluştur
|
||
let groups_str = groups.join(",");
|
||
ui.log(format!("useradd -m -G {} -s /bin/bash {}", groups_str, self.username));
|
||
run_chroot(&self.mount, &[
|
||
"useradd",
|
||
"-m",
|
||
"-G", &groups_str,
|
||
"-s", "/bin/bash",
|
||
&self.username,
|
||
]).await?;
|
||
|
||
// // chpasswd ile şifre ayarla (stdin üzerinden güvenli)
|
||
// ui.log(format!("Şifre ayarlanıyor: {}", self.username));
|
||
// let chpasswd_input = format!("{}:{}", self.username, self.password);
|
||
// set_password_via_chpasswd(&self.mount, &chpasswd_input).await?;
|
||
|
||
// // Root şifresi: belirlenmişse ayarla, yoksa kilitle
|
||
// if self.root_password.is_empty() {
|
||
// ui.log("Root hesabı kilitleniyor…");
|
||
// run_chroot(&self.mount, &["passwd", "-l", "root"]).await?;
|
||
// } else {
|
||
// ui.log("Root şifresi ayarlanıyor…");
|
||
// let root_input = format!("root:{}", self.root_password);
|
||
// set_password_via_chpasswd(&self.mount, &root_input).await?;
|
||
// }
|
||
|
||
// 2. chpasswd ile şifre ayarla (YENİ BASİTLEŞTİRİLMİŞ VE KESİN YÖNTEM)
|
||
ui.log(format!("Şifre ayarlanıyor: {}", self.username));
|
||
// sh -c kullanarak echo ile chpasswd'e şifreyi güvenli bir şekilde üflüyoruz
|
||
let user_cmd = format!("echo '{}:{}' | chpasswd -c SHA512", self.username, self.password);
|
||
run_chroot(&self.mount, &["sh", "-c", &user_cmd]).await?;
|
||
|
||
// 3. Root şifresi: belirlenmişse ayarla, yoksa kilitle
|
||
if self.root_password.is_empty() {
|
||
ui.log("Root hesabı kilitleniyor…");
|
||
run_chroot(&self.mount, &["passwd", "-l", "root"]).await?;
|
||
} else {
|
||
ui.log("Root şifresi ayarlanıyor…");
|
||
let root_cmd = format!("echo 'root:{}' | chpasswd -c SHA512", self.root_password);
|
||
run_chroot(&self.mount, &["sh", "-c", &root_cmd]).await?;
|
||
}
|
||
|
||
|
||
// sudoers: wheel grubu sudo yetkisi alsın
|
||
let sudoers_dir = format!("{}/etc/sudoers.d", self.mount);
|
||
tokio::fs::create_dir_all(&sudoers_dir).await
|
||
.map_err(|e| format!("{} oluşturulamadı: {}", sudoers_dir, e))?;
|
||
let sudoers_path = format!("{}/wheel", sudoers_dir);
|
||
ui.log(format!("Yazılıyor: {}", sudoers_path));
|
||
tokio::fs::write(&sudoers_path, "%wheel ALL=(ALL:ALL) ALL\n")
|
||
.await
|
||
.map_err(|e| format!("sudoers yazılamadı: {}", e))?;
|
||
|
||
// Dosya izinleri: sudoers 440 olmalı
|
||
run_cmd("chmod", &["440", &sudoers_path]).await?;
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
// /// `chpasswd` komutuna stdin üzerinden kullanıcı:şifre gönderir.
|
||
// async fn set_password_via_chpasswd(mount: &str, input: &str) -> Result<(), String> {
|
||
// if DEMO_MODE.load(std::sync::atomic::Ordering::Relaxed) {
|
||
// tokio::time::sleep(std::time::Duration::from_millis(2500)).await;
|
||
// return Ok(());
|
||
// }
|
||
|
||
// use tokio::io::AsyncWriteExt;
|
||
// use tokio::process::Command;
|
||
|
||
// let mut child = Command::new("chroot")
|
||
// .args([mount, "chpasswd"])
|
||
// .stdin(std::process::Stdio::piped())
|
||
// .stdout(std::process::Stdio::null())
|
||
// .stderr(std::process::Stdio::piped())
|
||
// .spawn()
|
||
// .map_err(|e| format!("chpasswd başlatılamadı: {}", e))?;
|
||
|
||
// if let Some(mut stdin) = child.stdin.take() {
|
||
// let mut data = input.as_bytes().to_vec();
|
||
// if !data.ends_with(b"\n") {
|
||
// data.push(b'\n');
|
||
// }
|
||
// stdin.write_all(&data).await
|
||
// .map_err(|e| format!("chpasswd stdin yazma hatası: {}", e))?;
|
||
// }
|
||
|
||
// let output = child.wait_with_output().await
|
||
// .map_err(|e| format!("chpasswd bekleme hatası: {}", e))?;
|
||
|
||
// if output.status.success() {
|
||
// Ok(())
|
||
// } else {
|
||
// Err(String::from_utf8_lossy(&output.stderr).trim().to_string())
|
||
// }
|
||
// }
|
||
|
||
|
||
/// fstab'ı doğrudan dosyaya yazar (genfstab yerine; ISO ortamında daha güvenilir).
|
||
pub struct GenerateFstabJob {
|
||
pub mount: String,
|
||
pub root_uuid: String, // blkid ile alınır
|
||
pub efi_uuid: Option<String>,
|
||
pub swap_uuid: String,
|
||
pub encrypt_root: bool,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for GenerateFstabJob {
|
||
fn name(&self) -> &str { "job_fstab" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let etc_dir = format!("{}/etc", self.mount);
|
||
let _ = tokio::fs::create_dir_all(&etc_dir).await;
|
||
|
||
let fstab_path = format!("{}/etc/fstab", self.mount);
|
||
ui.log(format!("Yazılıyor: {}", fstab_path));
|
||
|
||
let mut fstab = String::from(
|
||
"# /etc/fstab — Yali tarafından otomatik oluşturuldu\n\
|
||
# <device> <mountpoint> <fs> <options> <dump> <pass>\n\n"
|
||
);
|
||
|
||
// Kök bölümü
|
||
if self.encrypt_root {
|
||
fstab.push_str(
|
||
"/dev/mapper/cryptroot / ext4 defaults,noatime 0 1\n"
|
||
);
|
||
} else {
|
||
fstab.push_str(&format!(
|
||
"UUID={} / ext4 defaults,noatime 0 1\n",
|
||
self.root_uuid
|
||
));
|
||
}
|
||
|
||
// EFI bölümü
|
||
if let Some(ref efi) = self.efi_uuid {
|
||
fstab.push_str(&format!(
|
||
"UUID={} /boot/efi vfat umask=0077 0 2\n",
|
||
efi
|
||
));
|
||
}
|
||
|
||
// Swap
|
||
fstab.push_str(&format!(
|
||
"UUID={} none swap sw 0 0\n",
|
||
self.swap_uuid
|
||
));
|
||
|
||
tokio::fs::write(&fstab_path, fstab)
|
||
.await
|
||
.map_err(|e| format!("fstab yazılamadı: {}", e))?;
|
||
|
||
// crypttab (LUKS için)
|
||
if self.encrypt_root {
|
||
let crypttab_path = format!("{}/etc/crypttab", self.mount);
|
||
let crypttab = format!(
|
||
"cryptroot UUID={} none luks\n",
|
||
self.root_uuid
|
||
);
|
||
ui.log(format!("Yazılıyor: {}", crypttab_path));
|
||
tokio::fs::write(&crypttab_path, crypttab)
|
||
.await
|
||
.map_err(|e| format!("crypttab yazılamadı: {}", e))?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Disk bölümlerinin UUID'lerini `blkid` ile okur.
|
||
pub async fn read_partition_uuids(
|
||
plan: &PartitionPlan,
|
||
) -> Result<(String, Option<String>, String), String> {
|
||
let (root_num, swap_num): (u32, u32) = if plan.efi_mb > 0 { (3, 2) } else { (2, 1) };
|
||
|
||
let root_part = part_path(&plan.disk, root_num);
|
||
let swap_part = part_path(&plan.disk, swap_num);
|
||
|
||
// Şifreli kök: blkid'den LUKS UUID'sini al (crypttab için)
|
||
let root_uuid = blkid_uuid(&root_part).await?;
|
||
let swap_uuid = blkid_uuid(&swap_part).await?;
|
||
let efi_uuid = if plan.efi_mb > 0 {
|
||
Some(blkid_uuid(&part_path(&plan.disk, 1)).await?)
|
||
} else {
|
||
None
|
||
};
|
||
|
||
Ok((root_uuid, efi_uuid, swap_uuid))
|
||
}
|
||
|
||
async fn blkid_uuid(device: &str) -> Result<String, String> {
|
||
if DEMO_MODE.load(std::sync::atomic::Ordering::Relaxed) {
|
||
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
|
||
return Ok(format!("demo-uuid-{}", device.replace("/", "-")));
|
||
}
|
||
|
||
let output = tokio::process::Command::new("blkid")
|
||
.args(["-s", "UUID", "-o", "value", device])
|
||
.output()
|
||
.await
|
||
.map_err(|e| format!("blkid çalıştırılamadı: {}", e))?;
|
||
|
||
let uuid = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||
if uuid.is_empty() {
|
||
Err(format!("{} için UUID bulunamadı", device))
|
||
} else {
|
||
Ok(uuid)
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// MANUEL BÖLÜMLEME İÇİN DESTEKLEYİCİ JOB'LAR
|
||
// ─────────────────────────────────────────────
|
||
|
||
use crate::installer::{CustomPartition, FsType};
|
||
|
||
/// Manuel bölümleme ayarlarını disk üzerinde uygulayan ve biçimlendiren Job.
|
||
pub struct CustomPartitionJob {
|
||
pub disk: String,
|
||
pub custom_partitions: Vec<CustomPartition>,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for CustomPartitionJob {
|
||
fn name(&self) -> &str { "job_custom_partition" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let is_uefi = std::path::Path::new("/sys/firmware/efi").exists();
|
||
|
||
// 0. Diski hazırla: bağlamaları ayır, kernel tablosunu temizle
|
||
prepare_disk(&self.disk, ui).await;
|
||
|
||
let is_gpt = is_uefi; // Genellikle UEFI ise GPT kullanılır
|
||
// MBR'de "primary" zorunludur, GPT'de ise bu alan isim (label) alanıdır.
|
||
let part_type_or_name = if is_gpt { "" } else { "primary" };
|
||
|
||
// 1. Bölüm tablosu oluştur
|
||
let table_label = if is_uefi { "gpt" } else { "msdos" };
|
||
ui.log(format!("parted {} mktable {}", self.disk, table_label));
|
||
run_parted(&["-s", &self.disk, "mktable", table_label]).await?;
|
||
|
||
let mut next_mb: u64 = 1; // İlk MB boot sektörü
|
||
|
||
// 2. Bölümleri parted ile oluştur
|
||
for (i, part) in self.custom_partitions.iter().enumerate() {
|
||
let part_num = (i + 1) as u32;
|
||
let end_str = if part.size_mb == 0 {
|
||
"100%".to_string()
|
||
} else {
|
||
format!("{}MB", next_mb + part.size_mb)
|
||
};
|
||
let start_str = format!("{}MB", next_mb);
|
||
|
||
let fs_label = match part.fstype {
|
||
FsType::Fat32 => "fat32",
|
||
FsType::Swap => "linux-swap",
|
||
FsType::Ext4 => "ext4",
|
||
FsType::Ext3 => "ext3",
|
||
FsType::Btrfs => "btrfs",
|
||
FsType::Xfs => "xfs",
|
||
FsType::Ntfs => "ntfs",
|
||
// LVM PV için parted "ext4" ile işaretle, pvcreate sonra
|
||
FsType::Lvm => "ext4",
|
||
// LVM LV doğrudan parted ile oluşturulmaz
|
||
FsType::LvmLv => continue,
|
||
};
|
||
|
||
ui.log(format!(
|
||
"Bölüm {} oluşturuluyor: {} - {} ({})",
|
||
part_num, start_str, end_str, fs_label
|
||
));
|
||
run_parted(&[
|
||
"-s", &self.disk, "mkpart", part_type_or_name, fs_label,
|
||
&start_str, &end_str
|
||
]).await?;
|
||
|
||
if part.mountpoint == "/boot/efi" || part.fstype == FsType::Fat32 {
|
||
run_parted(&["-s", &self.disk, "set", &part_num.to_string(), "esp", "on"]).await?;
|
||
}
|
||
|
||
if part.size_mb > 0 {
|
||
next_mb += part.size_mb;
|
||
}
|
||
}
|
||
|
||
// 2.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle
|
||
sync_partition_table(&self.disk).await;
|
||
|
||
// 3. Biçimlendir (LUKS ile şifreleme desteklenir)
|
||
for part in &self.custom_partitions {
|
||
let path = &part.device;
|
||
|
||
// LUKS şifreleme (kök veya isteğe bağlı bölümler)
|
||
// Şifre, shell injection'ı önlemek için doğrudan stdin pipe üzerinden
|
||
// iletilir; `sh -c` + string interpolasyon kullanılmaz.
|
||
let fs_device = if part.encrypt && !part.luks_password.is_empty() {
|
||
let luks_name = if part.luks_name.is_empty() {
|
||
format!("crypt_{}", part.mountpoint.replace("/", "_"))
|
||
} else {
|
||
part.luks_name.clone()
|
||
};
|
||
let luks_name = luks_name.trim_start_matches('_').to_string();
|
||
|
||
ui.log(format!("cryptsetup luksFormat {}…", path));
|
||
run_luks_format(path, &part.luks_password).await
|
||
.map_err(|e| format!("LUKS format hatası ({}): {}", path, e))?;
|
||
|
||
ui.log(format!("cryptsetup open {} {}…", path, luks_name));
|
||
run_luks_open(path, &luks_name, &part.luks_password).await
|
||
.map_err(|e| format!("LUKS open hatası ({}): {}", path, e))?;
|
||
|
||
format!("/dev/mapper/{}", luks_name)
|
||
} else {
|
||
path.clone()
|
||
};
|
||
|
||
match part.fstype {
|
||
FsType::Fat32 => {
|
||
ui.log(format!("mkfs.fat -F32 {}", fs_device));
|
||
run_cmd("mkfs.fat", &["-F32", &fs_device]).await?;
|
||
}
|
||
FsType::Swap => {
|
||
ui.log(format!("mkswap {}", path));
|
||
run_cmd("mkswap", &[path]).await?;
|
||
ui.log(format!("swapon {}", path));
|
||
run_cmd("swapon", &[path]).await?;
|
||
}
|
||
FsType::Ext4 => {
|
||
ui.log(format!("mkfs.ext4 -F {}", fs_device));
|
||
run_cmd("mkfs.ext4", &["-F", &fs_device]).await?;
|
||
}
|
||
FsType::Ext3 => {
|
||
ui.log(format!("mkfs.ext3 -F {}", fs_device));
|
||
run_cmd("mkfs.ext3", &["-F", &fs_device]).await?;
|
||
}
|
||
FsType::Btrfs => {
|
||
ui.log(format!("mkfs.btrfs -f {}", fs_device));
|
||
run_cmd("mkfs.btrfs", &["-f", &fs_device]).await?;
|
||
}
|
||
FsType::Xfs => {
|
||
ui.log(format!("mkfs.xfs -f {}", fs_device));
|
||
run_cmd("mkfs.xfs", &["-f", &fs_device]).await?;
|
||
}
|
||
FsType::Ntfs => {
|
||
ui.log(format!("mkfs.ntfs -f {}", fs_device));
|
||
run_cmd("mkfs.ntfs", &["-f", &fs_device]).await?;
|
||
}
|
||
FsType::Lvm => {
|
||
ui.log(format!("pvcreate {}", path));
|
||
run_cmd("pvcreate", &[path]).await?;
|
||
}
|
||
// LV ayrı bir job'da oluşturulur
|
||
FsType::LvmLv => {}
|
||
}
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// LVM Volume Group ve Logical Volume'leri oluşturur.
|
||
pub struct SetupLvmJob {
|
||
pub volume_groups: Vec<crate::installer::VolumeGroup>,
|
||
pub logical_volumes: Vec<crate::installer::LogicalVolume>,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for SetupLvmJob {
|
||
fn name(&self) -> &str { "job_setup_lvm" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
for vg in &self.volume_groups {
|
||
let pv_str = vg.pv_devices.join(" ");
|
||
ui.log(format!("vgcreate {} {}", vg.name, pv_str));
|
||
run_cmd("vgcreate", &[&vg.name, &pv_str]).await?;
|
||
}
|
||
|
||
for lv in &self.logical_volumes {
|
||
let size_str = if lv.size_mb == 0 {
|
||
"100%FREE".to_string()
|
||
} else {
|
||
format!("{}M", lv.size_mb)
|
||
};
|
||
let lv_path = format!("/dev/{}/{}", lv.vg_name, lv.name);
|
||
ui.log(format!("lvcreate -L {} -n {} {}", size_str, lv.name, lv.vg_name));
|
||
run_cmd("lvcreate", &["-L", &size_str, "-n", &lv.name, &lv.vg_name]).await?;
|
||
|
||
let fs_cmd = match lv.fstype {
|
||
crate::installer::FsType::Ext4 => "mkfs.ext4",
|
||
crate::installer::FsType::Ext3 => "mkfs.ext3",
|
||
crate::installer::FsType::Btrfs => "mkfs.btrfs",
|
||
crate::installer::FsType::Xfs => "mkfs.xfs",
|
||
_ => continue,
|
||
};
|
||
ui.log(format!("{} {}", fs_cmd, lv_path));
|
||
run_cmd(fs_cmd, &[&lv_path]).await?;
|
||
}
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Manuel bölümleri ve mantıksal hacimleri kök dizinden başlayarak hiyerarşik olarak hedef yola bağlayan Job.
|
||
pub struct MountCustomPartitionsJob {
|
||
pub custom_partitions: Vec<CustomPartition>,
|
||
pub logical_volumes: Vec<crate::installer::LogicalVolume>,
|
||
pub mount: String,
|
||
}
|
||
|
||
impl MountCustomPartitionsJob {
|
||
/// Şifreli bölümler için mapper yolunu döndürür.
|
||
fn device_path(&self, part: &CustomPartition) -> String {
|
||
if part.encrypt {
|
||
let luks_name = if part.luks_name.is_empty() {
|
||
format!("crypt_{}", part.mountpoint.replace("/", "_").trim_start_matches('_'))
|
||
} else {
|
||
part.luks_name.clone()
|
||
};
|
||
format!("/dev/mapper/{}", luks_name)
|
||
} else {
|
||
part.device.clone()
|
||
}
|
||
}
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for MountCustomPartitionsJob {
|
||
fn name(&self) -> &str { "job_mount_custom" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
// 1. Kök (/) bölümünü (fiziksel partition veya LVM LV) bul ve bağla
|
||
let root_device = if let Some(root_part) = self.custom_partitions.iter().find(|p| p.mountpoint == "/") {
|
||
self.device_path(root_part)
|
||
} else if let Some(root_lv) = self.logical_volumes.iter().find(|lv| lv.mountpoint == "/") {
|
||
format!("/dev/{}/{}", root_lv.vg_name, root_lv.name)
|
||
} else {
|
||
return Err("Kök (/) bölümü manuel listede veya mantıksal bölümlerde bulunamadı.".to_string());
|
||
};
|
||
|
||
ui.log(format!("Kök dizin bağlanıyor: {} ➔ {}", root_device, self.mount));
|
||
run_cmd("mount", &[&root_device, &self.mount]).await?;
|
||
|
||
// 2. Diğer bölümleri ve mantıksal hacimleri topla, sırala ve bağla (derinliğe göre artan sırada)
|
||
struct MountItem {
|
||
device: String,
|
||
mountpoint: String,
|
||
}
|
||
|
||
let mut mount_items = Vec::new();
|
||
|
||
for part in &self.custom_partitions {
|
||
if part.mountpoint != "/" && part.fstype != FsType::Swap && !part.mountpoint.is_empty() {
|
||
mount_items.push(MountItem {
|
||
device: self.device_path(part),
|
||
mountpoint: part.mountpoint.clone(),
|
||
});
|
||
}
|
||
}
|
||
|
||
for lv in &self.logical_volumes {
|
||
if lv.mountpoint != "/" && lv.fstype != FsType::Swap && !lv.mountpoint.is_empty() {
|
||
mount_items.push(MountItem {
|
||
device: format!("/dev/{}/{}", lv.vg_name, lv.name),
|
||
mountpoint: lv.mountpoint.clone(),
|
||
});
|
||
}
|
||
}
|
||
|
||
mount_items.sort_by_key(|item| item.mountpoint.len());
|
||
|
||
for item in mount_items {
|
||
let target_path = format!("{}{}", self.mount, item.mountpoint);
|
||
ui.log(format!("Bağlama noktası dizini oluşturuluyor: {}", target_path));
|
||
tokio::fs::create_dir_all(&target_path).await
|
||
.map_err(|e| format!("{} oluşturulamadı: {}", target_path, e))?;
|
||
ui.log(format!("Bağlanıyor: {} ➔ {}", item.device, target_path));
|
||
run_cmd("mount", &[&item.device, &target_path]).await?;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Manuel bölümleme planına göre fstab dosyası üreten Job.
|
||
pub struct FstabFromCustomPartitionsJob {
|
||
pub custom_partitions: Vec<CustomPartition>,
|
||
pub logical_volumes: Vec<crate::installer::LogicalVolume>,
|
||
pub mount: String,
|
||
}
|
||
|
||
#[async_trait::async_trait]
|
||
impl Job for FstabFromCustomPartitionsJob {
|
||
fn name(&self) -> &str { "job_fstab" }
|
||
|
||
async fn run(&self, ui: &UiSender) -> Result<(), String> {
|
||
let etc_dir = format!("{}/etc", self.mount);
|
||
let _ = tokio::fs::create_dir_all(&etc_dir).await;
|
||
|
||
let fstab_path = format!("{}/etc/fstab", self.mount);
|
||
ui.log(format!("Yazılıyor: {}", fstab_path));
|
||
|
||
let mut fstab = String::from(
|
||
"# /etc/fstab — Yali tarafından otomatik (manuel mod) oluşturuldu\n\
|
||
# <device> <mountpoint> <fs> <options> <dump> <pass>\n\n"
|
||
);
|
||
|
||
let mut crypttab = String::from(
|
||
"# /etc/crypttab — Yali tarafından otomatik oluşturuldu\n"
|
||
);
|
||
|
||
for part in &self.custom_partitions {
|
||
if part.mountpoint.is_empty() { continue; }
|
||
|
||
let is_encrypted = part.encrypt && !part.luks_password.is_empty();
|
||
|
||
let device_field = if is_encrypted {
|
||
let luks_uuid = blkid_uuid(&part.device).await?;
|
||
let luks_name = if part.luks_name.is_empty() {
|
||
format!("crypt_{}", part.mountpoint.replace("/", "_").trim_start_matches('_'))
|
||
} else {
|
||
part.luks_name.clone()
|
||
};
|
||
crypttab.push_str(&format!(
|
||
"{} UUID={} none luks\n",
|
||
luks_name, luks_uuid
|
||
));
|
||
format!("/dev/mapper/{:<30}", luks_name)
|
||
} else {
|
||
ui.log(format!("{} için UUID okunuyor…", part.device));
|
||
let uuid = blkid_uuid(&part.device).await?;
|
||
format!("UUID={:<36}", uuid)
|
||
};
|
||
|
||
let (mp, options, dump, pass) = match part.fstype {
|
||
FsType::Swap => ("none", "sw", "0", "0"),
|
||
FsType::Fat32 => ("/boot/efi", "umask=0077", "0", "2"),
|
||
FsType::Ext4 | FsType::Ext3 if part.mountpoint == "/" => ("/", "defaults,noatime", "0", "1"),
|
||
_ => {
|
||
let pass_val = if part.mountpoint == "/" { "1" } else { "2" };
|
||
(part.mountpoint.as_str(), "defaults,noatime", "0", pass_val)
|
||
}
|
||
};
|
||
|
||
let fs_name = match part.fstype {
|
||
FsType::Ext4 => "ext4",
|
||
FsType::Ext3 => "ext3",
|
||
FsType::Btrfs => "btrfs",
|
||
FsType::Xfs => "xfs",
|
||
FsType::Fat32 => "vfat",
|
||
FsType::Ntfs => "ntfs-3g",
|
||
FsType::Swap => "swap",
|
||
FsType::Lvm | FsType::LvmLv => "ext4",
|
||
};
|
||
|
||
fstab.push_str(&format!(
|
||
"{} {:<12} {:<6} {:<18} {} {}\n",
|
||
device_field, mp, fs_name, options, dump, pass
|
||
));
|
||
}
|
||
|
||
// LVM mantıksal bölümlerini fstab'a ekle
|
||
for lv in &self.logical_volumes {
|
||
if lv.mountpoint.is_empty() { continue; }
|
||
|
||
let device_field = format!("/dev/{}/{}", lv.vg_name, lv.name);
|
||
|
||
let (mp, options, dump, pass) = match lv.fstype {
|
||
FsType::Swap => ("none", "sw", "0", "0"),
|
||
FsType::Fat32 => ("/boot/efi", "umask=0077", "0", "2"),
|
||
FsType::Ext4 | FsType::Ext3 if lv.mountpoint == "/" => ("/", "defaults,noatime", "0", "1"),
|
||
_ => {
|
||
let pass_val = if lv.mountpoint == "/" { "1" } else { "2" };
|
||
(lv.mountpoint.as_str(), "defaults,noatime", "0", pass_val)
|
||
}
|
||
};
|
||
|
||
let fs_name = match lv.fstype {
|
||
FsType::Ext4 => "ext4",
|
||
FsType::Ext3 => "ext3",
|
||
FsType::Btrfs => "btrfs",
|
||
FsType::Xfs => "xfs",
|
||
FsType::Fat32 => "vfat",
|
||
FsType::Ntfs => "ntfs-3g",
|
||
FsType::Swap => "swap",
|
||
_ => "ext4",
|
||
};
|
||
|
||
fstab.push_str(&format!(
|
||
"{:<42} {:<12} {:<6} {:<18} {} {}\n",
|
||
device_field, mp, fs_name, options, dump, pass
|
||
));
|
||
}
|
||
|
||
tokio::fs::write(&fstab_path, fstab)
|
||
.await
|
||
.map_err(|e| format!("fstab yazılamadı: {}", e))?;
|
||
|
||
// crypttab (sadece şifreli bölümler için)
|
||
let crypttab_device_count = crypttab.lines().count().saturating_sub(1); // header satırını sayma
|
||
if crypttab_device_count > 0 {
|
||
let crypttab_path = format!("{}/etc/crypttab", self.mount);
|
||
ui.log(format!("Yazılıyor: {}", crypttab_path));
|
||
let _ = tokio::fs::write(&crypttab_path, crypttab).await;
|
||
}
|
||
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
/// Manuel bölümleme kullanıldığında derlenecek kurulum iş kuyruğu.
|
||
pub fn build_custom_job_queue(
|
||
disk: String,
|
||
custom_partitions: Vec<CustomPartition>,
|
||
volume_groups: Vec<crate::installer::VolumeGroup>,
|
||
logical_volumes: Vec<crate::installer::LogicalVolume>,
|
||
cfg: JobQueueConfig,
|
||
) -> JobQueue {
|
||
DEMO_MODE.store(cfg.demo_mode, std::sync::atomic::Ordering::Relaxed);
|
||
|
||
let is_uefi = std::path::Path::new("/sys/firmware/efi").exists();
|
||
let encrypt_root = custom_partitions.iter().any(|p| p.mountpoint == "/" && p.encrypt);
|
||
let mt = cfg.mount.clone();
|
||
|
||
let src = if cfg.source.is_empty() { detect_source_dir() } else { cfg.source };
|
||
|
||
let mut jobs: Vec<Box<dyn Job>> = vec![
|
||
Box::new(CustomPartitionJob { disk: disk.clone(), custom_partitions: custom_partitions.clone() }),
|
||
Box::new(SetupLvmJob { volume_groups: volume_groups.clone(), logical_volumes: logical_volumes.clone() }),
|
||
Box::new(MountCustomPartitionsJob { custom_partitions: custom_partitions.clone(), logical_volumes: logical_volumes.clone(), mount: mt.clone() }),
|
||
Box::new(MountBindJob { mount: mt.clone() }),
|
||
Box::new(CopyFilesJob { source: src, target: mt.clone() }),
|
||
Box::new(FstabFromCustomPartitionsJob { custom_partitions: custom_partitions.clone(), logical_volumes: logical_volumes.clone(), mount: mt.clone() }),
|
||
Box::new(ConfigureHostnameJob { mount: mt.clone(), hostname: cfg.hostname.clone() }),
|
||
Box::new(ConfigureLocaleJob { mount: mt.clone(), locale: cfg.locale.clone() }),
|
||
Box::new(ConfigureTimezoneJob { mount: mt.clone(), timezone: cfg.timezone.clone() }),
|
||
Box::new(ConfigureDateTimeJob {
|
||
mount: mt.clone(),
|
||
use_ntp: cfg.use_ntp,
|
||
year: cfg.manual_year,
|
||
month: cfg.manual_month,
|
||
day: cfg.manual_day,
|
||
hour: cfg.manual_hour,
|
||
minute: cfg.manual_minute,
|
||
}),
|
||
Box::new(ConfigureKeyboardJob { mount: mt.clone(), layout: cfg.kb_layout.clone(), variant: cfg.kb_variant.clone() }),
|
||
Box::new(CreateUserJob { mount: mt.clone(), username: cfg.username.clone(), password: cfg.password.clone(), root_password: cfg.root_password.clone() }),
|
||
Box::new(GenerateInitramfsJob { mount: mt.clone(), encrypt_root, lvm_active: !volume_groups.is_empty() }),
|
||
];
|
||
|
||
jobs.push(Box::new(InstallGrubJob {
|
||
disk: disk.clone(),
|
||
mount: mt.clone(),
|
||
is_uefi,
|
||
boot_device: cfg.boot_device.clone(),
|
||
timeout: cfg.bootloader_timeout,
|
||
password: cfg.bootloader_password.clone(),
|
||
kernel_options: cfg.kernel_options.clone(),
|
||
encrypt_root,
|
||
}));
|
||
|
||
jobs.push(Box::new(ConfigureDisplayManagerJob {
|
||
mount: mt.clone(),
|
||
display_manager: cfg.display_manager.clone(),
|
||
autologin: cfg.autologin,
|
||
username: cfg.username.clone(),
|
||
desktop_environment: cfg.desktop_environment.clone(),
|
||
}));
|
||
jobs.push(Box::new(RunComarJob { mount: mt.clone() }));
|
||
jobs.push(Box::new(RunLdconfigJob { mount: mt.clone() }));
|
||
jobs.push(Box::new(UpdateEnvironmentJob { mount: mt.clone() }));
|
||
if !cfg.selected_package_groups.is_empty() {
|
||
jobs.push(Box::new(InstallExtraPackagesJob {
|
||
mount: mt.clone(),
|
||
package_groups: cfg.selected_package_groups.clone(),
|
||
}));
|
||
}
|
||
jobs.push(Box::new(CleanupLiveJob { mount: mt.clone() }));
|
||
jobs.push(Box::new(UnmountBindJob { mount: mt.clone() }));
|
||
|
||
JobQueue::new(jobs)
|
||
}
|