diff --git a/AGENTS.md b/AGENTS.md index f28665f..3b7a6a0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -95,9 +95,35 @@ dizini. Bu iş `CopyFilesJob`'dan sonra çalıştığı için `mount` doludur. > `lightdm.conf`'a dokunulmuyor, `sddm.conf` yazılıyor ve otomatik giriş > sessizce çalışmıyordu. +### ⭐ Canlı ISO autologin'i + ters görünen checkbox (29 Eyl 2026) + +ISO **canlı kullanıcı için çalışan bir autologin config'i** taşır +(kullanıcı doğruladı: canlı açılış otomatik giriyor). Bu iki arızayı +birlikte üretiyordu: + +| Kutu | Olan | Neden | +|---|---|---| +| tikli | login ekranı | bizim config `/usr/lib/...` altına yazılıyordu → `/etc` altındaki canlı config'e **öncelikle yeniliyordu**; bir de `Session=plasmax11.desktop` **sabit** yazılıyordu, hedefte yoksa SDDM oturumu bulamıyor | +| boş | "oturum açıyor" | kutu boşken hiçbir şeye dokunulmuyordu → canlı config kopyalanıyor, kurulu sistem **canlı kullanıcıyla** (parolasız!) açılıyordu | + +Kurallar: +- sddm config'i **`/etc/sddm.conf.d/10-yali-autologin.conf`** kanonik + dosyaya yazılır (en yüksek öncelik, deterministik kazanır). + ISO dosyaları **silinmez**, öncelikle ezilir. +- Oturum adı hedeften tespit edilir: `resolve_session()` → + `/usr/share/{wayland-sessions,xsessions}/*.desktop` taranır, + `pick_session_file()` seçer (saf + testli). Sabit eşleme yalnızca + **son çaredir**. +- Kutu boşken de kanonik dosya yazılır ama **`User=` boş** bırakılır — + SDDM bunu "kapalı" sayar, canlı config ezilir. lightdm'de + `autologin-user=`/`autologin-session=` satırları ayıklanır. +- DM bulunamazsa **yalnızca kutu tikliyken hata**; kapalıyken sessiz + geçilir (DM'siz sunucu kurulumunu öldürmemek için). + `ConfigureAutologinJob` ayrıca: -- sddm config dizini: `/usr/lib/sddm/sddm.conf.d` yoksa `/etc/sddm.conf.d` - denir; ikisi de yoksa **hata** (sessizce yanlış yere yazmak yerine) +- sddm config'i daima `/etc/sddm.conf.d/10-yali-autologin.conf` dosyasına + yazılır (dizin yoksa oluşturulur); eski `/usr/lib/...` + fallback + mantığı **kaldırıldı** (öncelik kaybı + yanlış dizin riski vardı) - `session_desktop_file()` eşlemesini **tüm DM'lerde** kullanır (lightdm `autologin-session=plasma` yazıyordu; `.desktop` dosyası adı bekler) @@ -495,6 +521,27 @@ mountpoint'i olan bölümü düzenlerken) liste **tamamen kilitlenmişti**. > (removable gibi) sorgulanmadığından emin ol. Sorgulanıyorsa probu > dışarıdan al. +### ⭐ Yerelleşmiş CLI çıktısı ayrıştırılmaz (29 Eyl 2026, P0 veri kaybı) + +`disk_label_is_gpt` parted çıktısındaki İngilizce `Partition Table` +satırını arıyordu. Ama parted yerelleşmiştir — canlı ISO'da satır +`Disk bölümü Tablosu: msdos` gelir. Ayrıştırıcı hiçbir diski +tanıyamadı → her disk "boş" sanıldı → `mktable` dolu diski **komple +sildi**. Boş disklerde zararsız olduğu için iki kurulum geçti, üçüncüde +(dolu diske ekleme) patladı. + +Kurallar: +- İnsan-dili CLI çıktısı **daima `LC_ALL=C` ile** okunur + (`env LC_ALL=C parted …`). Makine-dili alternatif varsa (blkid + `PTTYPE` gibi) o tercih edilir. +- Türkçe başlık çekimlidir (`bölümü`, `bölümü`nün…): tam kalıp + eşleştirme ıskalar. Anahtar kelimeyle (`tablosu`) eşleştir + test + verisi olarak GERÇEK yerelleşmiş çıktıyı kullan. +- **Yıkıcı komutun kilidi olur:** `mktable` öncesi sysfs'ten fiziksel + bölüm sayılır; bölüm varken etiket okunamazsa **hata verilir**, + tabloya dokunulmaz. Ayrıştırıcı + locale + kilit üçü birlikte + kapatır; hiçbiri tek başına yeterli değildir. + ### ⭐ "Biçimlendir" kutusu — olumlu çerçeve Kullanıcı isteği: *"düzenle penceresindeki biçimlendirmeyi atla diff --git a/locales/en.toml b/locales/en.toml index 82c6847..bb18531 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -141,7 +141,7 @@ display_manager_title = "Display Manager Configuration" display_manager_description = "Select your display manager and desktop environment." display_manager_select = "Display manager:" display_manager_de = "Desktop environment:" -display_manager_autologin = "Automatic login" +display_manager_autologin = "Log in automatically" display_manager_autologin_user = "Autologin user:" display_manager_autologin_hint = "(first user)" display_manager_de_not_found = "(not detected)" diff --git a/locales/tr.toml b/locales/tr.toml index 90eaa0b..e08fb94 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -303,7 +303,7 @@ display_manager_title = "Görüntü Yöneticisi Yapılandırması" display_manager_description = "Görüntü yöneticinizi ve masaüstü ortamınızı seçin." display_manager_select = "Görüntü yöneticisi:" display_manager_de = "Masaüstü ortamı:" -display_manager_autologin = "Otomatik oturum açma" +display_manager_autologin = "Otomatik oturum aç" display_manager_autologin_user = "Otomatik oturum açılacak kullanıcı:" display_manager_autologin_hint = "(ilk kullanıcı)" display_manager_de_not_found = "(algılanamadı)" diff --git a/src/jobs/autologin.rs b/src/jobs/autologin.rs index 079910c..55b0272 100644 --- a/src/jobs/autologin.rs +++ b/src/jobs/autologin.rs @@ -4,8 +4,22 @@ use super::{Job, UiSender}; /// /// Ekran yöneticisi (sddm/lightdm/gdm) yapılandırması ISO'da hazır olduğu /// için burada değiştirilmez; yalnızca kullanıcının seçtiği "otomatik oturum -/// açma" bilgisi mevcut konfigürasyona eklenir. Autologin kapalıysa hiçbir -/// şeye dokunulmaz (ISO varsayılanında otomatik giriş yoktur). +/// açma" bilgisi mevcut konfigürasyona eklenir. +/// +/// ## Neden `/etc/sddm.conf.d/10-yali-autologin.conf` +/// +/// ISO canlı kullanıcı için çalışan bir autologin config'i taşıyabilir. +/// `/usr/lib/...` altına yazılan drop-in, `/etc/...` altındaki canlı +/// config'e öncelik olarak YENİLİR — kutu tikliyken bile canlı config +/// kazanırdı. `/etc` altındaki kanonik dosya deterministik olarak kazanır. +/// +/// ## Kapalıyken neden boş `User=` yazılır +/// +/// Kutu boşken hiçbir şeye dokunulmamak, canlı ISO'nun autologin +/// config'ini olduğu gibi kopyalamak demekti: kurulu sistem canlı +/// kullanıcıyla (parolasız!) otomatik açılıyordu. Bu yüzden kapalı +/// durumda da kanonik dosya yazılır ama `User=` boş bırakılır — SDDM +/// bunu "autologin yok" sayar ve canlı config'i ezer. pub struct ConfigureAutologinJob { pub mount: String, pub display_manager: String, @@ -19,19 +33,12 @@ impl Job for ConfigureAutologinJob { fn name(&self) -> &str { "job_autologin" } async fn run(&self, ui: &UiSender) -> Result<(), String> { - if !self.autologin { - ui.log("Otomatik oturum açma kapalı, değişiklik yapılmıyor."); - return Ok(()); - } - // Ekran yöneticisini SEÇMEK yerine TESPİT ediyoruz. // - // YALI kurulu sistemi squashfs'ten kopyalar; ekran yöneticisi ISO'da - // hazırdır. `state.display_manager` ise GlobalState'in "sddm" - // varsayılanıdır ve kullanıcı tarafından değiştirilemez. Önceden - // bu sabite göre dallanılıyordu: ISO lightdm kullanıyorsa - // lightdm.conf'a hiç dokunulmaz, sddm.conf yazılır ve otomatik - // giriş sessizce çalışmazdı. + // YALI kurulu sistemi squashfs'ten kopyalar; ekran yöneticisi ISO'nun + // kendisidir; kullanıcı seçmez. Tespit çalışma zamanında yapılır çünkü + // `mount` dizini rsync'ten sonra tamdır (bu iş `CopyFilesJob`'dan + // sonra çalışır). let dm = detect_display_manager(&self.mount).await; match &dm { Some(found) if found != &self.display_manager => { @@ -44,6 +51,11 @@ impl Job for ConfigureAutologinJob { ui.log(format!("Ekran yöneticisi: {}", found)); } None => { + // Kapalıyken DM yoksa etkisizleştirilecek bir şey de yoktur. + if !self.autologin { + ui.log("Otomatik oturum açma kapalı, değişiklik yapılmıyor."); + return Ok(()); + } return Err( "Hedef sistemde sddm/lightdm/gdm bulunamadı. \ Otomatik oturum açma uygulanamadı; kurulumda seçilen \ @@ -53,42 +65,46 @@ impl Job for ConfigureAutologinJob { } } - ui.log(format!( - "Otomatik oturum açma uygulanıyor: {} ({})", - self.username, self.username - )); - - let session = session_desktop_file(&self.desktop_environment); + // Oturum adı HEDEFTEN tespit edilir (aşağıya bak). Eskiden sabit + // `plasmax11.desktop` yazılıyordu; hedefte bu dosya yoksa SDDM + // oturumu bulamaz ve kutu tikli olmasına rağmen login ekranına + // düşerdi — üstelik kutu boşken ISO'nun (doğru oturumlu) canlı + // config'i çalıştığı için davranış tam TERS görünüyordu. + let session = resolve_session(&self.mount, &self.desktop_environment).await; + ui.log(format!("Oturum dosyası: {}", session)); match dm.as_deref() { Some("sddm") => { - // ISO'nun /usr/lib/sddm/sddm.conf.d içeriğine dokunmadan - // ayrı bir drop-in dosyası ile autologin eklenir. - let dir = format!("{}/usr/lib/sddm/sddm.conf.d", self.mount); - // Bu dizin yoksa ISO sddm kullanmıyordur demektir; sddm - // genelde /etc/sddm.conf.d kullanır. İkisini de dene. - let dir = if std::path::Path::new(&dir).is_dir() { - dir - } else { - let alt = format!("{}/etc/sddm.conf.d", self.mount); - if std::path::Path::new(&alt).is_dir() { - alt - } else { - return Err(format!( - "sddm yapılandırma dizini bulunamadı ({}/usr/lib/sddm/sddm.conf.d, {}/etc/sddm.conf.d)", - self.mount, self.mount - )); + let conf = format!("{}/etc/sddm.conf.d/10-yali-autologin.conf", self.mount); + if !self.autologin { + // Canlı config'i ezen, autologin'i kapatan kanonik dosya. + // ISO dosyaları SİLİNMEZ, yalnızca öncelikle ezilir. + let content = "# Yali: otomatik oturum açma kapalı (canlı config'i ezer)\n\ + [Autologin]\nUser=\nSession=\n"; + if let Some(parent) = std::path::Path::new(&conf).parent() { + let _ = tokio::fs::create_dir_all(parent).await; } - }; - tokio::fs::create_dir_all(&dir).await - .map_err(|e| format!("{} oluşturulamadı: {}", dir, e))?; + tokio::fs::write(&conf, content) + .await + .map_err(|e| format!("{} yazılamadı: {}", conf, e))?; + ui.log("Otomatik oturum açma kapalı, canlı config etkisizleştirildi."); + return Ok(()); + } + ui.log(format!( + "Otomatik oturum açma uygulanıyor: {} ({})", + self.username, self.username + )); + if let Some(parent) = std::path::Path::new(&conf).parent() { + tokio::fs::create_dir_all(parent).await + .map_err(|e| format!("{} oluşturulamadı: {}", parent.display(), e))?; + } let content = format!( - "[Autologin]\nUser={}\nSession={}\n", + "# Yali tarafından oluşturuldu\n[Autologin]\nUser={}\nSession={}\n", self.username, session ); - tokio::fs::write(format!("{}/autologin.conf", dir), content) + tokio::fs::write(&conf, content) .await - .map_err(|e| format!("autologin.conf yazılamadı: {}", e))?; + .map_err(|e| format!("{} yazılamadı: {}", conf, e))?; } Some("lightdm") => { let dir = format!("{}/etc/lightdm", self.mount); @@ -97,21 +113,40 @@ impl Job for ConfigureAutologinJob { let mut content = tokio::fs::read_to_string(&conf) .await .unwrap_or_default(); - if !content.contains("[Seat:*]") { - content.push_str("[Seat:*]\n"); + // Önce bayat autologin satırlarını temizle (canlı config + // kalıntısı da, kendi önceki yazımımız da). + content = content + .lines() + .filter(|l| { + let t = l.trim_start(); + !(t.starts_with("autologin-user=") + || t.starts_with("autologin-session=")) + }) + .collect::>() + .join("\n"); + if !content.ends_with('\n') { + content.push('\n'); } - if !content.contains("autologin-user=") { + if self.autologin { + if !content.contains("[Seat:*]") { + content.push_str("[Seat:*]\n"); + } // lightdm oturumu .desktop dosyası adı olarak bekler. - // Ham DE adını ("plasma") yazmak, sddm/gdm'nin kullandığı - // eşlemenin aynısı değildi ve oturumu bulamazdı. content.push_str(&format!("autologin-user={}\n", self.username)); content.push_str(&format!("autologin-session={}\n", session)); + ui.log(format!("Otomatik oturum açma uygulanıyor: {}", self.username)); + } else { + ui.log("Otomatik oturum açma kapalı, canlı config etkisizleştirildi."); } tokio::fs::write(&conf, content) .await .map_err(|e| format!("lightdm.conf yazılamadı: {}", e))?; } Some("gdm") => { + if !self.autologin { + ui.log("Otomatik oturum açma kapalı, değişiklik yapılmıyor."); + return Ok(()); + } let dir = format!("{}/etc/gdm", self.mount); let _ = tokio::fs::create_dir_all(&dir).await; let conf = format!("{}/custom.conf", dir); @@ -159,6 +194,67 @@ fn session_desktop_file(de: &str) -> &'static str { } } +/// Hedef sistemde GERÇEKTEN var olan oturum dosyasını seçer (saf). +/// +/// `wayland` ve `x11`, hedefteki `/usr/share/wayland-sessions` ve +/// `/usr/share/xsessions` içerik listesidir. Önce DE anahtarını içeren +/// dosya aranır (wayland öncelikli — ISO varsayılanı genelde odur), +/// bulunamazsa alfabetik ilk dosya, liste boşsa sabit eşleme döner. +/// +/// Sabit eşleme tek başına YETERSİZDİR: hedefte `plasmax11.desktop` +/// yoksa (yalnızca wayland kurulu ISO) SDDM oturumu bulamaz ve kutu +/// tikli olmasına rağmen login ekranına düşer. +fn pick_session_file(de: &str, wayland: &[String], x11: &[String]) -> String { + let key = match de { + "plasma" => "plasma", + "xfce" => "xfce", + "gnome" => "gnome", + "cinnamon" => "cinnamon", + "budgie" => "budgie", + "lxde" => "lxde", + "lxqt" => "lxqt", + "mate" => "mate", + "lumina" => "lumina", + _ => "plasma", + }; + for list in [wayland, x11] { + let mut sorted = list.to_vec(); + sorted.sort(); + for f in &sorted { + if f.to_lowercase().contains(key) { + return f.clone(); + } + } + } + let mut all: Vec<&String> = wayland.iter().chain(x11.iter()).collect(); + all.sort(); + if let Some(f) = all.first() { + return (*f).clone(); + } + session_desktop_file(de).to_string() +} + +/// Hedef `mount` altındaki oturum dizinlerini okuyup dosya adı seçer. +async fn resolve_session(mount: &str, de: &str) -> String { + async fn list(mount: &str, sub: &str) -> Vec { + let mut out = Vec::new(); + let mut rd = match tokio::fs::read_dir(format!("{}/usr/share/{}", mount, sub)).await { + Ok(rd) => rd, + Err(_) => return out, + }; + while let Ok(Some(e)) = rd.next_entry().await { + let name = e.file_name().to_string_lossy().to_string(); + if name.ends_with(".desktop") { + out.push(name); + } + } + out + } + let wayland = list(mount, "wayland-sessions").await; + let x11 = list(mount, "xsessions").await; + pick_session_file(de, &wayland, &x11) +} + /// Hedef sistemde hangi ekran yöneticisinin KURULU olduğunu tespit eder. /// /// YALI squashfs'ten kopyaladığı için ekran yöneticisi ISO'nun kendisidir; @@ -219,4 +315,37 @@ mod tests { ); } } + + fn s(v: &[&str]) -> Vec { + v.iter().map(|x| x.to_string()).collect() + } + + #[test] + fn picker_prefers_real_wayland_session() { + // Yalnızca wayland kurulu ISO: sabit `plasmax11.desktop` yok. + // Eskiden bu dosya adı körü körüne yazılıyor, SDDM oturumu + // bulamayıp login ekranına düşüyordu. + let got = pick_session_file("plasma", &s(&["plasma.desktop"]), &s(&[])); + assert_eq!(got, "plasma.desktop"); + } + + #[test] + fn picker_matches_de_key_case_insensitively() { + let got = pick_session_file( + "xfce", + &s(&[]), + &s(&["xfce.desktop", "plasma.desktop"]), + ); + assert_eq!(got, "xfce.desktop"); + } + + #[test] + fn picker_falls_back_to_first_file_then_static_map() { + // DE'ye uyan yoksa alfabetik ilk dosya. + let got = pick_session_file("mate", &s(&[]), &s(&["zebra.desktop", "aaa.desktop"])); + assert_eq!(got, "aaa.desktop"); + // Hiç dosya yoksa sabit eşleme (son çare). + let got = pick_session_file("plasma", &[], &[]); + assert_eq!(got, "plasmax11.desktop"); + } } diff --git a/src/jobs/partition.rs b/src/jobs/partition.rs index 44e8891..5632371 100644 --- a/src/jobs/partition.rs +++ b/src/jobs/partition.rs @@ -298,7 +298,145 @@ fn read_physical_extents(disk: &str) -> Result, String> { Ok(out) } -/// `parted` komutlarını çalıştırmak için özel sarmalayıcı. +/// `parted print` çıktısından disk etiketini çözer (saf, test edilebilir). +/// +/// `parted -s /dev/sdx print` çıktısı `Partition Table: gpt` veya +/// `Partition Table: msdos` satırı içerir. Etiketsiz/boş diskte bu satır +/// yerine `unrecognised disk label` yazar → `None` döner, çağıran yeni +/// etiket oluşturur. +/// +/// ⚠️ parted çıktısı YERELLEŞTİRİLMİŞTİR (tr: `Disk bölümü Tablosu`)! +/// Üretim kodu parted'i `LC_ALL=C` ile çalıştırır; bu ayrıştırıcı yine +/// de Türkçe anahtar kelimeleri de tanır (kemer + askı). +pub fn parse_parted_label(output: &str) -> Option { + for line in output.lines() { + let l = line.to_lowercase(); + // Türkçe başlık çekimlidir (`Disk bölümü Tablosu`) — tam kalıp + // aramak ek yüzünden ıskalar, `tablosu` yeterlidir. + let is_label_line = l.contains("partition table") || l.contains("tablosu"); + if is_label_line { + // Değer anahtarları (gpt/msdos) yerelleşmez; yalnızca + // satır başlığı (`Partition Table` / `Disk bölümü Tablosu`) + // dile göre değişir. + if l.contains("gpt") { + return Some(true); + } + if l.contains("msdos") || l.contains("dos") || l.contains("mbr") { + return Some(false); + } + // Etiket satırı var ama tür tanınmıyor (örn. "loop"): + // temkinli None. + return None; + } + if l.contains("unrecognised disk label") || l.contains("unrecognized disk label") { + return None; + } + } + None +} + +/// Diskin bölüm tablosu etiketini okur: `Some(true)` = GPT, `Some(false)` = +/// msdos/MBR, `None` = etiketsiz/boş/okunamadı. +/// +/// parted **daima `LC_ALL=C` ile** çalıştırılır: çıktısı yerelleşmiştir +/// (tr: `Disk bölümü Tablosu`) ve İngilizce ayrıştırıcı Türkçe satırı +/// tanıyamaz. 29 Eyl 2026'da bu, dolu diskin "boş" sanılıp `mktable` +/// ile SİLİNMESİNE yol açtı (P0 veri kaybı). +async fn disk_label_is_gpt(disk: &str) -> Option { + if DEMO_MODE.load(Ordering::Relaxed) { + return None; + } + let out = Command::new("env") + .args(["LC_ALL=C", "parted", "-s", disk, "print"]) + .output() + .await + .ok()?; + let combined = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + parse_parted_label(&combined) +} + +/// `mkpart`'ın parted söz dizimi tablo etiketine göre değişir: +/// +/// - GPT: `mkpart START END` (NAME serbest metin, boş OLAMAZ) +/// - msdos: `mkpart START END` (TYPE = primary|logical) +/// +/// Eskiden firmware (`/sys/firmware/efi`) tablo türü sanılıyordu: UEFI +/// makinede msdos diskli kurulum `mkpart "" ext4 …` çalıştırıp +/// "Bir disk bölümü türü bekleniyor" hatasıyla düşüyordu. +pub fn mkpart_first_arg(is_gpt: bool, slot_num: u32) -> &'static str { + if is_gpt { + // GPT'de bu alan bölüm ADI'dır; çağıran `gpt_part_name` kullanır. + // Buraya düşülmemeli — imza simetrisi için korunuyor. + "yali" + } else if slot_num >= 5 { + "logical" + } else { + "primary" + } +} + +/// GPT bölüm adı: mountpoint'ten türetilir. +/// +/// `/` için ad BOŞ bırakılır (kullanıcı isteği — `root` adı `/root` +/// ile karışıyordu). parted CLI boş ada izin vermez (boş argümanı +/// sessizce atlayıp fs ipucunu ad yapar!), bu yüzden `/` önce `/` +/// adıyla oluşturulup `sfdisk --part-label ""` ile +/// sıfırlanır (`should_blank_gpt_name`). +pub fn gpt_part_name(mountpoint: &str, fstype: &FsType, slot_num: u32) -> String { + if mountpoint == "/boot/efi" { + return "EFI".to_string(); + } + match mountpoint { + "/" => "/".to_string(), + "/home" => "home".to_string(), + "[SWAP]" | "swap" => "swap".to_string(), + m if !m.is_empty() => { + let s: String = m + .trim_start_matches('/') + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect(); + if s.is_empty() { + format!("yali{}", slot_num) + } else { + s.chars().take(32).collect() + } + } + _ => match fstype { + FsType::Swap => "swap".to_string(), + FsType::Fat32 => "EFI".to_string(), + _ => format!("yali{}", slot_num), + }, + } +} + +/// Adı boş bırakılacak GPT bölümleri: yalnızca `/`. +/// +/// Diğerleri (`/home` → `home`, takas → `swap`…) adını korur; boş ad +/// yalnızca `/root` karışıklığını önlemek için `/`'ye özeldir. +pub fn should_blank_gpt_name(mountpoint: &str) -> bool { + mountpoint == "/" +} + +/// parted'ın anladığı dosya sistemi ipucu. +pub fn parted_fs_label(fstype: &FsType) -> &'static str { + match 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 ipucu + ardından `set N lvm on`. + FsType::Lvm => "ext4", + FsType::LvmLv => "ext4", + } +} /// /// `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 @@ -521,10 +659,15 @@ impl Job for PartitionJob { if self.plan.efi_mb > 0 { let end_mb = next_mb + self.plan.efi_mb; ui.log(format!("EFI bölümü oluşturuluyor: {}–{} MiB", next_mb, end_mb)); - run_parted(&[ - "-s", disk, "mkpart", "fat32", - &format!("{}MiB", next_mb), &format!("{}MiB", end_mb), - ]).await?; + let start_s = format!("{}MiB", next_mb); + let end_s = format!("{}MiB", end_mb); + if is_gpt { + // GPT: mkpart START END + run_parted(&["-s", disk, "mkpart", "EFI", "fat32", &start_s, &end_s]).await?; + } else { + // msdos: mkpart START END + run_parted(&["-s", disk, "mkpart", "primary", "fat32", &start_s, &end_s]).await?; + } run_parted(&["-s", disk, "set", "1", "esp", "on"]).await?; next_mb = end_mb; } @@ -533,10 +676,13 @@ impl Job for PartitionJob { 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: {}–{} MiB", next_mb, swap_end)); - run_parted(&[ - "-s", disk, "mkpart", "linux-swap", - &format!("{}MiB", next_mb), &format!("{}MiB", swap_end), - ]).await?; + { + let start_s = format!("{}MiB", next_mb); + let end_s = format!("{}MiB", swap_end); + // Otomatik planda en fazla 3 bölüm var → msdos'ta daima primary. + let (first, fs) = if is_gpt { ("swap", "linux-swap") } else { ("primary", "linux-swap") }; + run_parted(&["-s", disk, "mkpart", first, fs, &start_s, &end_s]).await?; + } next_mb = swap_end; // 4. Kök bölümü (kalan alan) @@ -547,10 +693,19 @@ impl Job for PartitionJob { // 2048 sektörün katıdır, hizalama sorunu olmaz. let root_num: u32 = if self.plan.efi_mb > 0 { 3 } else { 2 }; ui.log(format!("Kök bölümü oluşturuluyor: {}MiB – disk sonu", next_mb)); - run_parted(&[ - "-s", disk, "mkpart", "ext4", - &format!("{}MiB", next_mb), "100%", - ]).await?; + { + let start_s = format!("{}MiB", next_mb); + let (first, fs) = if is_gpt { ("/", "ext4") } else { ("primary", "ext4") }; + run_parted(&["-s", disk, "mkpart", first, fs, &start_s, "100%"]).await?; + // GPT'de `/` adı boş kalır (sfdisk ile; parted boş ada izin + // vermez). Kozmetiktir — başarısız olursa ad `/` kalır. + if is_gpt { + let slot = root_num.to_string(); + if run_cmd("sfdisk", &["--part-label", disk, &slot, ""]).await.is_err() { + ui.log("Bölüm adı temizlenemedi (ad '/' kalıyor)."); + } + } + } // 4.5. Tüm mkpart bitti; kernel'e bildir ve aygıt düğümlerini bekle sync_partition_table(disk).await; @@ -786,9 +941,7 @@ 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(); - // MBR'de "primary" zorunludur, GPT'de ise bu alan isim (label) alanıdır. - let part_type_or_name = if is_uefi { "" } else { "primary" }; + let firmware_uefi = std::path::Path::new("/sys/firmware/efi").exists(); // UI'ın hesapladığı aygıt yolu → kurulum sonrası GERÇEK aygıt yolu. // @@ -836,6 +989,45 @@ impl Job for CustomPartitionJob { // Bağlamaları ayır, swap kapat prepare_disk(disk, ui).await; + // ── TABLO ETİKETİ: her disk kendi etiketine göre yazılır. + // + // REGRESYON (29 Eyl 2026): etiket firmware'den tahmin ediliyordu + // (UEFI → GPT varsayımı). UEFI makinede msdos etiketli veya + // TAMAMEN BOŞ (etiketsiz) disk `mkpart "" ext4 …` çalıştırıp + // "Bir disk bölümü türü bekleniyor" ile düşüyordu. + // Boş diskte parted "unrecognised disk label" verir → burada + // firmware'e göre taze etiket oluşturulur. + // + // ⭐ GÜVENLİK KİLİDİ (29 Eyl 2026, P0): `mktable` YALNIZCA + // fiziksel bölümü OLMAYAN diske uygulanır. Etiket + // okunamadı (`None`) ama diskte bölüm VARSA tabloyu + // sıfırlamak yerine HATA verilir — veri kaybı engellenir. + // (Türkçe parted çıktısı İngilizce ayrıştırıcıyı atlatıp + // dolu diski "boş" göstermişti; `LC_ALL=C` + bu kilit + // birlikte kapatır.) + let is_gpt = match disk_label_is_gpt(disk).await { + Some(v) => v, + None => { + let physical_check = read_physical_extents(disk).unwrap_or_default(); + if !physical_check.is_empty() { + return Err(format!( + "{}: bölüm tablosu okunamadı ama diskte {} bölüm var; \ + veri kaybını önlemek için diske dokunulmuyor", + disk, + physical_check.len() + )); + } + let label = if firmware_uefi { "gpt" } else { "msdos" }; + ui.log(format!( + "{} üzerinde bölüm tablosu yok, yeni {} tablosu oluşturuluyor", + disk, label + )); + run_parted(&["-s", disk, "mktable", label]).await?; + sync_partition_table(disk).await; + firmware_uefi + } + }; + // ── FAZ A: SİLMEK İŞARETLİ BÖLÜMLER (büyükten küçüğe numara) ── let mut del_sorted = destroys.clone(); del_sorted.sort_unstable_by(|a, b| b.cmp(a)); @@ -885,39 +1077,54 @@ impl Job for CustomPartitionJob { let part = creates[plan.part_index]; // Yeni bölüm: UI'ın tahmin ettiği yol → gerçek yol. device_map.insert(part.device.clone(), plan.device.clone()); - 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", - FsType::LvmLv => continue, - }; + let fs_label = parted_fs_label(&part.fstype); // DİKKAT: parted "MB" sonekini onalık (10^6) okur; // hesaplarımız binary (2^20) olduğu için "MiB" zorunlu. // Tamsayı MiB = 2048 sektörün katı → parted hizalama // hatası vermez. + // + // Söz dizimi etikete göre değişir: + // GPT: mkpart START END (AD boş olamaz) + // msdos: mkpart START END (primary|logical) let start_str = format!("{}MiB", plan.start_mb); let end_str = format!("{}MiB", plan.end_mb); let slot_str = plan.num.to_string(); + let first_arg: String = if is_gpt { + gpt_part_name(&part.mountpoint, &part.fstype, plan.num) + } else { + mkpart_first_arg(false, plan.num).to_string() + }; ui.log(format!( "{} üzerinde yeni bölüm oluşturuluyor: {} - {} ({}, slot {})", disk, start_str, end_str, fs_label, plan.num )); run_parted(&[ - "-s", disk, "mkpart", part_type_or_name, fs_label, + "-s", disk, "mkpart", &first_arg, fs_label, &start_str, &end_str ]).await?; if part.mountpoint == "/boot/efi" || part.fstype == FsType::Fat32 { run_parted(&["-s", disk, "set", &slot_str, "esp", "on"]).await?; } + if part.fstype == FsType::Lvm { + run_parted(&["-s", disk, "set", &slot_str, "lvm", "on"]).await?; + } + // `/` adı boş kalır: parted boş ada izin vermez, sfdisk + // ile sıfırlanır. Kozmetik adımdır — başarısız olursa + // kurulum DURMAZ, ad `/` olarak kalır. + if is_gpt && should_blank_gpt_name(&part.mountpoint) { + match run_cmd("sfdisk", &["--part-label", disk, &slot_str, ""]).await { + Ok(()) => ui.log(format!( + "{} bölüm adı temizlendi (slot {})", plan.device, plan.num + )), + Err(e) => ui.log(format!( + "Bölüm adı temizlenemedi ({}, ad '/' kalıyor): {}", + plan.device, e + )), + } + } } sync_partition_table(disk).await; } @@ -1489,4 +1696,62 @@ mod layout_tests { .expect("planlanmalı"); assert_eq!(planned[0].device, "/dev/nvme0n1p2"); } + + // ── mkpart söz dizimi (29 Eyl 2026 regresyonu) ── + + #[test] + fn msdos_first_arg_is_primary_then_logical() { + assert_eq!(mkpart_first_arg(false, 1), "primary"); + assert_eq!(mkpart_first_arg(false, 4), "primary"); + assert_eq!(mkpart_first_arg(false, 5), "logical"); + } + + #[test] + fn gpt_names_are_never_empty() { + use crate::installer::FsType; + assert_eq!(gpt_part_name("/boot/efi", &FsType::Fat32, 1), "EFI"); + assert_eq!(gpt_part_name("/", &FsType::Ext4, 2), "/"); + assert_eq!(gpt_part_name("/home", &FsType::Ext4, 1), "home"); + // Kullanıcının senaryosu: boş diske /home + ext4. + assert_eq!(gpt_part_name("/home", &FsType::Ext4, 1), "home"); + assert!(!gpt_part_name("", &FsType::Ext4, 3).is_empty()); + assert!(!gpt_part_name("", &FsType::Swap, 3).is_empty()); + } + + #[test] + fn only_root_gets_blanked_name() { + assert!(should_blank_gpt_name("/")); + assert!(!should_blank_gpt_name("/home")); + assert!(!should_blank_gpt_name("/boot/efi")); + assert!(!should_blank_gpt_name("swap")); + assert!(!should_blank_gpt_name("")); + } + + #[test] + fn parted_label_parsing() { + assert_eq!( + parse_parted_label("Model: ATA\nPartition Table: gpt\n"), + Some(true) + ); + assert_eq!( + parse_parted_label("Partition Table: msdos\n"), + Some(false) + ); + // GERÇEK Türkçe parted çıktısı (canlı ISO'dan): satır başlığı + // yerelleşir, değerler (gpt/msdos) yerelleşmez. + assert_eq!( + parse_parted_label("Disk /tmp/opencode/parttest/t.img: 67,1MB\nDisk bölümü Tablosu: gpt\n"), + Some(true) + ); + assert_eq!( + parse_parted_label("Disk bölümü Tablosu: msdos\n"), + Some(false) + ); + assert_eq!( + parse_parted_label("Error: /dev/sdc: unrecognised disk label\n"), + None + ); + // Boş disk: etiket satırı yok → None (çağıran mktable yapar). + assert_eq!(parse_parted_label(""), None); + } }