diff --git a/AGENTS.md b/AGENTS.md index 12ace00..321013e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -374,6 +374,45 @@ Ayrıca UEFI modunda çift önyükleme ipucu (`bootloader_device_hint_dualboot`) gösterilir: hangi aygıtı seçmek gerektiği ekranda hiç yazmıyordu. +### ⭐ Önyükleyici varsayılanı: KÖRK diski değil, **mevcut OS diski** + +`steps/bootloader.rs::default_boot_device()` saf ve test edilebilir: + +``` +1. çıkarılabilir aygıt (ISO'nun USB'i) → ASLA hedef değil +2. kök diski dışında bölümü olan ilk dahili disk → mevcut OS diski +3. kök diski +4. selected_disk (son çare) +``` + +**Neden kök diski değil.** BIOS modunda `grub-install` seçilen +diskin **MBR'sine** yazar ve MBR'yi okuyan aygıt firmware'in açılış +sırasındaki ilk diski olmak ZORUNDA: + +``` +sda: Windows sdb: PisiLinux (kök burada) +GRUB → sdb firmware sdb'yi açmaz, PisiLinux HİÇ GÖRÜNMEZ +GRUB → sda firmware GRUB'a düşer, GRUB ikisini de listeler +``` + +"İşletim sistemi kurulu"nun en ucuz ve en güvenilir göstergesi: +**kök diski dışında hâlihazırda bölüm tablosu olan disk**. Tespit +`disk_partition_count()` ile `sysfs`'ten yapılır — **alt süreç yok** +(`lsblk` çalıştırmak `on_enter`'da bile gereksiz). + +Tek diskli kurulumda 3. adım çalışır, yani davranış kök diskiyle +aynıdır — geriye dönük uyum bozulmaz. + +Kullanıcı elle değiştirdiyse ipucu (`bootloader_device_why_existing`) +gösterilmez; ipucu yalnızca seçim bu kurala uyuyorsa çıkar, yani +sessiz bir sürpriz olmaz. + +> ⚠️ Bu, daha önceki iki varsayılanın da yerine geçti: +> `selected_disk` (manuel modda "aktif düzenleme diski" olduğu için +> yanlış diske gidiyordu) ve salt kök diski (dual-boot'ta yanlış). +> Kural artık ikisinden de iyi; `boot_device_reason` aynı fonksiyonu +> çağırıp UI metnini oradan üretir — **ikinci bir uygulama yok**. + ### `selected_disk` hâlâ 3 yerde kullanılıyor 1. Tablo hangi diski gösteriyor + `manual_dual_bars` 2. Diyaloğun varsayılan hedefi @@ -533,11 +572,11 @@ nedeni yorumla sabitlendi. Log bile "MiB" derken argüman "MB" gönderiyordu. ``` cargo check --all-targets : 0 hata, 0 uyarı -cargo test : 129/129 (test modülü sayısı arttıkça güncel tut) +cargo test : 137/137 (test modülü sayısı arttıkça güncel tut) cargo clippy : ~77 uyarı, tamamı stil (collapsed_if, replace zinciri) ``` -Bu oturumda 1 → 129 test. Test modülü olmayan dosyalar (`users.rs`, +Bu oturumda 1 → 137 test. Test modülü olmayan dosyalar (`users.rs`, `location.rs`, `rescue.rs`, `funct.rs`, `autoinstall/mod.rs`, `main.rs`) içine modüller eklendi. Çoklu test modülü paylaşılan yardımcıları `steps/partition.rs::testutil` altındadır (`disk()`, `part()`). diff --git a/locales/en.toml b/locales/en.toml index f39dad1..07d05de 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -380,6 +380,7 @@ bootloader_description = "Configure the bootloader (GRUB) settings." bootloader_device_label = "Install to device:" bootloader_device_hint = "(default: target disk)" bootloader_device_hint_dualboot = "For dual boot, pick the disk Windows is installed on. In UEFI mode that disk's Windows ESP is used to add a \"Windows Boot Manager\" entry to the boot menu; if you leave the default, Windows will not appear at boot." +bootloader_device_why_existing = "This device was picked because it already holds an operating system: the bootloader has to be written to the disk the firmware already boots." bootloader_timeout_label = "Timeout:" bootloader_timeout_suffix = "seconds" bootloader_password_label = "GRUB password (optional):" diff --git a/locales/tr.toml b/locales/tr.toml index 1b16d42..fbecbc7 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -388,6 +388,7 @@ bootloader_description = "Önyükleyici (GRUB) ayarlarını yapılandırın." bootloader_device_label = "Aygıta kur:" bootloader_device_hint = "(varsayılan: hedef disk)" bootloader_device_hint_dualboot = "Çift önyükleme yapacaksanız burada Windows'un kurulu olduğu diski seçin. UEFI modunda bu aygıtın bölümündeki Windows ESP'sinden \"Windows Boot Manager\" menüye eklenir; seçmezseniz açılışta Windows görünmez." +bootloader_device_why_existing = "Bu aygıt, üzerinde hâlihazırda işletim sistemi bulunduğu için seçildi: önyükleyici açılışta zaten okunan diske yazılmalıdır." bootloader_timeout_label = "Bekleme süresi:" bootloader_timeout_suffix = "saniye" bootloader_password_label = "GRUB parolası (isteğe bağlı):" diff --git a/src/steps/bootloader.rs b/src/steps/bootloader.rs index f97f388..9653acd 100644 --- a/src/steps/bootloader.rs +++ b/src/steps/bootloader.rs @@ -4,12 +4,99 @@ use crate::installer::{GlobalState, InstallerStep}; use crate::ui::theme; use crate::funct::{form_label, form_input_text, form_input_password}; +/// Bir diskin bölüm sayısını `sysfs`'ten okur (alt süreç yok). +/// +/// `lsblk` çalıştırmak kare başına olsaydı her karede süreç doğardu; +/// `on_enter`'da bile gereksiz. sysfs'te her bölüm +/// `/sys/block//`, `…/start` dosyasıyla listelenir. +fn disk_partition_count(disk: &str) -> usize { + let base = crate::steps::partition::base_disk_name(disk); + if base.is_empty() { + return 0; + } + let dir = format!("/sys/block/{}", base); + let Ok(rd) = std::fs::read_dir(&dir) else { return 0 }; + rd.flatten().filter(|e| { + let name = e.file_name().to_string_lossy().to_string(); + // Yalnızca "" biçimindeki girdiler bölümdür. + name.strip_prefix(&base) + .map(|rest| !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit())) + .unwrap_or(false) + }).count() +} + +/// Önyükleyici için varsayılan aygıtı seçer. +/// +/// Kural: **KÖR diski seçme; zaten işletim sistemi kurulu olan diski +/// seç.** Çünkü BIOS modunda `grub-install` seçilen diskin MBR'sine +/// yazar ve MBR'yi okuyan aygıt firmware'in açılış sırasındaki ilk +/// diski olmak ZORUNDA. Çift önyüklemeli bir makinede bu, mevcut +/// işletim sisteminin diski — kökün kurulduğu disk DEĞİL. +/// +/// Somut olarak: sda'da Windows, sdb'de PisiLinux kuruluyor. GRUB +/// sdb'nin MBR'sine yazılırsa firmware sdb'yi açmaz, PisiLinux hiç +/// görünmez. sda'nın MBR'sine yazılırsa firmware GRUB'a düşer, GRUB +/// da ikisini de listeler. +/// +/// "İşletim sistemi kurulu"nun en güvenilir ve en ucuz göstergesi: +/// **kök diski dışında, hâlihazırda bölüm tablosu olan bir disk** +/// (yani disk dolu değil, içinde tanım var). +/// +/// Sıralama: +/// 1. Çıkarılabilir aygıt (canlı ISO'nun USB'i) — ASLA hedef değil +/// 2. Kök diski dışında bölümü olan ilk dahili disk → mevcut OS diski +/// 3. Kök diski +/// 4. Aktif düzenleme diski (`selected_disk`) — son çare +/// +/// Tek diskli kurulumda 3. adım çalışır: başka disk yoktur, davranış +/// kök diskiyle aynıdır. +pub fn default_boot_device( + root_disk: Option<&str>, + disks: &[crate::installer::DiskInfo], + has_partitions: F, +) -> Option +where + F: Fn(&str) -> bool, +{ + // (1) çıkarılabilir aygıtlar ve (2) kök diski ele + disks.iter() + .filter(|d| !crate::steps::partition::is_removable(&d.name)) + .filter(|d| Some(d.name.as_str()) != root_disk) + .find(|d| has_partitions(&d.name)) + .map(|d| d.name.clone()) + .or_else(|| root_disk.map(|s| s.to_string())) +} + #[derive(Default)] pub struct BootloaderStep { show_password: bool, timeout_str: String, } +/// Seçili aygıt, "zaten işletim sistemi olan disk" kuralıyla mı +/// belirlendi? Değilse (kullanıcı elle değiştirdiyse ya da tek disk +/// varsa) `None` döner ve ipucu gösterilmez. +fn boot_device_reason(state: &GlobalState, current: &str) -> Option { + let root_disk = state + .custom_partitions + .iter() + .find(|p| p.mountpoint == "/" && !p.delete) + .map(|p| p.disk.clone()) + .or_else(|| state.partition_plan.as_ref().map(|p| p.disk.clone())); + + // Aynı kuralı bir kez daha çalıştırıp sonucun aygıtla örtüştüğünü + // doğruluyoruz. Kural tek kaynakta; burada yalnızca UI metni var. + let expected = default_boot_device(root_disk.as_deref(), &state.available_disks, |d| { + disk_partition_count(d) > 0 + })?; + + if expected == current { + Some(t!("bootloader_device_why_existing").to_string()) + } else { + None + } +} + impl InstallerStep for BootloaderStep { fn name(&self) -> String { t!("bootloader").to_string() @@ -19,22 +106,31 @@ impl InstallerStep for BootloaderStep { if self.timeout_str.is_empty() { self.timeout_str = state.bootloader_timeout.to_string(); } - // Varsayılan GRUB aygıtı, KÖKün olduğu disk olmalı. + // Varsayılan GRUB aygıtı: zaten işletim sistemi kurulu olan + // disk. Detaylı gerekçe `default_boot_device` yorumunda. // - // Önceden `state.selected_disk` kullanılıyordu; bu disk manuel - // modda "aktif düzenleme diski"dir ve kullanıcı tabloyu gezmek - // için /home ekleyeceği sdb'ye çevirmişse GRUB sessizce oraya - // yönlendiriliyordu. Doğru varsayılan kökün diski; kullanıcı - // ComboBox'tan değiştirebilir. + // Önceki davranışlar ve nedenleri: + // - `selected_disk` → manuel modda "aktif düzenleme diski"; + // /home eklemek için sdb'ye geçince GRUB sessizce oraya + // gidiyordu. + // - yalnızca kök diski → çift önyüklemeli makinede firmware'in + // açmadığı diske MBR yazılıyor, PisiLinux hiç görünmüyor. if state.bootloader_device.is_empty() { let root_disk = state .custom_partitions .iter() .find(|p| p.mountpoint == "/" && !p.delete) .map(|p| p.disk.clone()) - .or_else(|| state.partition_plan.as_ref().map(|p| p.disk.clone())) - .or_else(|| state.selected_disk.clone()); - if let Some(d) = root_disk { + .or_else(|| state.partition_plan.as_ref().map(|p| p.disk.clone())); + + let chosen = default_boot_device( + root_disk.as_deref(), + &state.available_disks, + |d| disk_partition_count(d) > 0, + ) + .or_else(|| state.selected_disk.clone()); + + if let Some(d) = chosen { state.bootloader_device = d; } } @@ -95,6 +191,17 @@ impl InstallerStep for BootloaderStep { } }); }); + // Varsayılanın NEDEN bu aygıt olduğunu söyle. Sessiz + // bir varsayılan sürpriz yaratmasın: kullanıcı + // nedenini anlamazsa dual-boot'ta yanlış aygıtta + // kalır ve açılışta Windows görünmez. + if let Some(why) = boot_device_reason(&state, &state.bootloader_device) { + ui.label( + egui::RichText::new(why) + .size(11.0) + .color(theme::c_accent()), + ); + } // Çift önyükleme ipucu — kullanıcı bu adımda en çok // zorlanan kuralı burada öğreniyor. if std::path::Path::new("/sys/firmware/efi").exists() { @@ -146,3 +253,118 @@ impl InstallerStep for BootloaderStep { !state.bootloader_device.is_empty() } } + +#[cfg(test)] +mod tests { + use super::default_boot_device; + use crate::installer::DiskInfo; + + fn d(name: &str) -> DiskInfo { + DiskInfo { + name: name.to_string(), + vendor: String::new(), + model: "TEST".to_string(), + drive_type: String::new(), + size_gb: 100, + size_bytes: 100 * 1_073_741_824, + } + } + + /// `has_partitions` verilen aygıtın bölümü var mı diye bakar + /// (test ortamında gerçek sysfs yok, bu yüzden dışarıdan verilir). + fn parts_on(disks_with_parts: &[&str]) -> impl Fn(&str) -> bool { + let v: Vec = disks_with_parts.iter().map(|s| s.to_string()).collect(); + move |name: &str| v.iter().any(|p| p == name) + } + + /// ⭐ Kullanıcının senaryosu: sda'da Windows, sdb'de PisiLinux + /// kuruluyor. GRUB sda'ya yazılmalı — çünkü firmware açılışta + /// sda'yı okuyor. Kök diski (sdb) seçilirse PisiLinux hiç görünmez. + #[test] + fn picks_the_disk_that_already_has_an_os() { + let disks = vec![d("/dev/sda"), d("/dev/sdb"), d("/dev/sdc")]; + let got = default_boot_device(Some("/dev/sdb"), &disks, parts_on(&["/dev/sda"])); + assert_eq!( + got.as_deref(), + Some("/dev/sda"), + "mevcut işletim sistemi olan disk seçilmeli" + ); + } + + /// Tek diskli kurulumda davranış KÖRK diskiyle aynıdır — başka + /// aday yoktur. + #[test] + fn single_disk_install_falls_back_to_the_root_disk() { + let disks = vec![d("/dev/sda")]; + assert_eq!( + default_boot_device(Some("/dev/sda"), &disks, parts_on(&["/dev/sda"])).as_deref(), + Some("/dev/sda") + ); + } + + /// Çok diskli ama HİÇBİRİ bölümlü değilse (tamamen boş makineler) + /// kök disk seçilir. + #[test] + fn no_existing_os_anywhere_falls_back_to_root() { + let disks = vec![d("/dev/sda"), d("/dev/sdb")]; + assert_eq!( + default_boot_device(Some("/dev/sdb"), &disks, parts_on(&[])).as_deref(), + Some("/dev/sdb") + ); + } + + /// Kök diskin kendisi bölümlü olsa bile "mevcut OS" sayılmaz — + /// kurulum oraya yapılıyor, oraya varsayılan yazmak eski davranış. + #[test] + fn root_disk_is_never_treated_as_the_existing_os() { + let disks = vec![d("/dev/sda"), d("/dev/sdb")]; + // sda'da kurulum, sdb boş → sda + assert_eq!( + default_boot_device(Some("/dev/sda"), &disks, parts_on(&["/dev/sda"])).as_deref(), + Some("/dev/sda") + ); + } + + /// Birden çok aday varsa SIRALAMA önemlidir: liste zaten + /// kararlı sıralıdır, ilki seçilir. + #[test] + fn first_candidate_in_sorted_order_wins() { + let disks = vec![d("/dev/sda"), d("/dev/sdb"), d("/dev/sdc")]; + assert_eq!( + default_boot_device(Some("/dev/sdd"), &disks, parts_on(&["/dev/sdb", "/dev/sdc"])).as_deref(), + Some("/dev/sdb"), + "sıralı listedeki ilk bölümlü disk seçilmeli" + ); + } + + /// Kök disk bilinmiyorsa (otomatik mod / henüz taranmamış) yine + /// bölümlü disk seçilir; hiçbiri yoksa `None`. + #[test] + fn unknown_root_disk_is_handled() { + let disks = vec![d("/dev/sda"), d("/dev/sdb")]; + assert_eq!( + default_boot_device(None, &disks, parts_on(&["/dev/sda"])).as_deref(), + Some("/dev/sda") + ); + assert_eq!(default_boot_device(None, &disks, parts_on(&[])), None); + } + + /// Disk listesi boşken bile KÖRK disk biliniyorsa o kullanılır — + /// liste yalnızca "mevcut işletim sistemi olan disk" arayışı için + /// gerekiyordu, son çare kök disktir. + #[test] + fn empty_disk_list_still_uses_the_root_disk() { + assert_eq!( + default_boot_device(Some("/dev/sda"), &[], parts_on(&["/dev/sda"])).as_deref(), + Some("/dev/sda"), + "liste boş olsa da bilinen kök disk kullanılmalı" + ); + } + + /// Ne disk ne de kök biliniyorsa `None` — `on_enter` bu durumda + /// `selected_disk`'e düşer, o da yoksa aygıt seçilemez. + #[test] + fn nothing_known_yields_none() { + assert_eq!(default_boot_device(None, &[], parts_on(&[])), None); + } +} diff --git a/src/steps/partition.rs b/src/steps/partition.rs index a045cc2..42d2bd4 100644 --- a/src/steps/partition.rs +++ b/src/steps/partition.rs @@ -247,7 +247,7 @@ fn sort_disks(disks: &mut [DiskInfo]) { /// /// Sonda `p` + rakam görmüyorsa NVMe/mmcbloğ için yolun tamamı disk /// adıdır — `nvme0n1` içinde ortada rakam vardır, sondakini atmak yanlıştır. -fn base_disk_name(device: &str) -> String { +pub fn base_disk_name(device: &str) -> String { let base = device.trim_start_matches("/dev/"); if base.starts_with("nvme") || base.starts_with("mmcblk") || base.starts_with("loop") { if let Some(pos) = base.rfind('p') {