From c6a1d0fb6fa84a6884b1e1bf73417ec20a8656a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erkan=20I=C5=9EIK?= Date: Sun, 27 Sep 2026 18:48:44 +0300 Subject: [PATCH] =?UTF-8?q?fix(bootloader):=20disk=20listesini=20s=C4=B1ra?= =?UTF-8?q?la,=20boot=20medyas=C4=B1n=C4=B1=20hedeften=20ele?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kullanıcı sorusu: "önyükleme adımında ilk disk neyse o seçili olması daha mantıklı değil mi." ## "İlk disk" neden doğru bir kural DEĞİL — iki bulgu ### 1. Liste sıralanmıyor `list_block_devices` ne `lsblk` çıktısını ne `/sys/block` çıktısını sıralıyor. Fallback yolunda liste `read_dir` sırasıyla geliyor, yani **önyüklemeden önyüklemeye değişebilir**. "İlk disk" böylece ne sıralı bir kavram ne de kararlı bir varsayılan. ### 2. ⭐ Boot medyası listede — ve hiçbir yerde filtrelenmiyor `detect_drive_type` "USB" döndürüyor ama o değer **tek bir yerde bile okunmuyor**. Canlı ISO'yu boot ettiğiniz USB `available_disks`'te yer alıyordu. "İlk disk" çok yaygın bir kurulumda boot medyasına denk gelir. Önyükleyici hedefi olarak seçilirse BIOS modunda `grub-install --target=i386-pc ` MBR'yi oraya yazar: kurulum biter, sistem AÇILMAZ. ## Düzeltme - `sort_disks()`: çıkarılabilir aygıtlar SONA, dahililer ada göre. Artık "ilk disk" her zaman bir iç disk. - `disk_sort_key`: bölüm numarası sayısal ayrıştırılır; ada göre sıralama `sda10 < sda2` derdi. - Önyükleyici hedef listesinden çıkarılabilir aygıtlar **elenir**. Liste tamamen çıkarılabilir aygıt olursa eski davranışa düşülür (ComboBox boş kalmasın). - UEFI modunda `bootloader_device_hint_dualboot` ipucu gösterilir: çift önyüklemede hangi aygıtı seçmek gerektiği ekranda hiç yazmıyordu. ## Test sırasında çıkan hata İlk `base_disk_name` yazımı `trim_end_matches(is_ascii_digit)`'ı her yola uyguluyordu: `nvme0n1` → `nvme0n`. Sonuç `/sys/block/nvme0n/ removable` aranıyor, NVMe disk çıkarılabilir sayılmıyordu — yani filtre NVMe'de sessizce çalışmıyordu. Aynı hata `installer::device_to_disk` içinde de var ama o işlev BÖLÜM YOLU içindir ve şu an yalnızca test yardımcısından çağrılıyor; canlı bir hatası yok. Yine de `base_disk_name` kendi ayrıştırıcısını kullanıyor ve bu ayrım notla sabitlendi — "basitleştirilirse" NVMe'ler çıkarılabilir sanılır. ## Testler (124 → 129) - diskler ada göre sıralanır - bölüm numaraları sayısal sıralanır (sda10 > sda2) - NVMe/mmcblk/vda isimleri doğru ayrışır (çıplak + bölümlü) - sıralama anahtarı her iki biçimi de doğru verir - çıkarılabilir aygıtlar sona gider Varsayılan seçimin KÖK diski olarak kalması korundu — bu konuda kullanıcıyla konuşulacak, aşağıdaki soru gönderildi. cargo check --all-targets : 0 hata, 0 uyarı cargo test : 129/129 Co-Authored-By: Claude Opus 4.8 (1M context) --- AGENTS.md | 31 +++++++- locales/en.toml | 1 + locales/tr.toml | 1 + src/steps/bootloader.rs | 25 ++++++ src/steps/partition.rs | 165 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 221 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e78eb6f..12ace00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -347,6 +347,33 @@ Diyalogda NTFS seçilince: 1. geçersiz öneri kalıntısı (`/`, `/boot/efi`, `swap`…) **temizlenir** 2. `mp_mountpoint_optional` ipucu gösterilir +### ⭐ Disk listesi kararlı sıralanır, boot medyası hariç + +`list_block_devices` ne `lsblk` çıktısını ne `/sys/block` çıktısını +sıralıyordu. `/sys/block` fallback yolunda liste **önyüklemeden +önyüklemeye değişebiliyor** — "ilk disk" kavramı anlamsızlaşıyor, +varsayılan seçimler tutarsızlaşıyor. + +`sort_disks()`: **çıkarılabilir aygıtlar sona**, dahililer ada göre +(`disk_sort_key` — bölüm numarası SAYISAL ayrıştırılır, yoksa +`sda10 < sda2` olurdu). + +**Çıkarılabilir aygıtlar listeden de ÖNYÜKLEYİCİ hedefinde de +elenir.** `detect_drive_type` "USB" döndürüyor ama o değer hiç +okunmuyordu. Canlı ISO'nun USB'i `available_disks`'teydi ve önyükleyici +hedefi olarak seçilirse BIOS modunda `grub-install --target=i386-pc` +MBR'yi **boot medyasına** yazar → kurulum biter, sistem açılmaz. + +> `is_removable()` kendi ayrıştırıcısını kullanır, `device_to_disk`'i +> DEĞİL: o işlev **bölüm yolu** içindir ve çıplak NVMe disk adında +> hatalıdır (`/dev/nvme0n1` → `nvme0n`). `base_disk_name` bunu +> düzeltir. Bu ayrım notla sabitlendi, ileride "basitleştirilirse" +> NVMe'ler çıkarılabilir sanılır. + +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. + ### `selected_disk` hâlâ 3 yerde kullanılıyor 1. Tablo hangi diski gösteriyor + `manual_dual_bars` 2. Diyaloğun varsayılan hedefi @@ -506,11 +533,11 @@ nedeni yorumla sabitlendi. Log bile "MiB" derken argüman "MB" gönderiyordu. ``` cargo check --all-targets : 0 hata, 0 uyarı -cargo test : 124/124 (test modülü sayısı arttıkça güncel tut) +cargo test : 129/129 (test modülü sayısı arttıkça güncel tut) cargo clippy : ~77 uyarı, tamamı stil (collapsed_if, replace zinciri) ``` -Bu oturumda 1 → 124 test. Test modülü olmayan dosyalar (`users.rs`, +Bu oturumda 1 → 129 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 b170b3a..f39dad1 100644 --- a/locales/en.toml +++ b/locales/en.toml @@ -379,6 +379,7 @@ bootloader_title = "Bootloader Configuration" 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_timeout_label = "Timeout:" bootloader_timeout_suffix = "seconds" bootloader_password_label = "GRUB password (optional):" diff --git a/locales/tr.toml b/locales/tr.toml index 2e5d01c..1b16d42 100644 --- a/locales/tr.toml +++ b/locales/tr.toml @@ -387,6 +387,7 @@ bootloader_title = "Önyükleyici Yapılandırması" 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_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 a4f6784..f97f388 100644 --- a/src/steps/bootloader.rs +++ b/src/steps/bootloader.rs @@ -62,9 +62,25 @@ impl InstallerStep for BootloaderStep { // Hedef aygıt form_label(ui, &t!("bootloader_device_label")); ui.horizontal(|ui| { + // ⚠️ ÇIKARILABİLİR AYGITLAR LİSTEDE YOK. + // + // Canlı ISO'yu boot ettiğiniz USB `available_disks` + // içinde yer alıyordu. Seçilirse BIOS modunda + // `grub-install --target=i386-pc ` MBR'yi o + // aygıta yazar, kurulum biter ama sistem AÇILMAZ. + // + // Liste dolduysa (tamamen çıkarılabilir aygıt + // görünürse) mevcut değer yine de gösterilir ki + // ComboBox boş kalmasın. let mut device_list: Vec = state.available_disks.iter() + .filter(|d| !crate::steps::partition::is_removable(&d.name)) .map(|d| d.name.clone()) .collect(); + if device_list.is_empty() { + device_list = state.available_disks.iter() + .map(|d| d.name.clone()) + .collect(); + } if device_list.is_empty() { device_list.push(state.bootloader_device.clone()); } @@ -79,6 +95,15 @@ impl InstallerStep for BootloaderStep { } }); }); + // Çift önyükleme ipucu — kullanıcı bu adımda en çok + // zorlanan kuralı burada öğreniyor. + if std::path::Path::new("/sys/firmware/efi").exists() { + ui.label( + egui::RichText::new(t!("bootloader_device_hint_dualboot")) + .size(11.0) + .color(theme::c_accent()), + ); + } ui.label( egui::RichText::new(t!("bootloader_device_hint")) .size(11.0) diff --git a/src/steps/partition.rs b/src/steps/partition.rs index 8892bb0..a045cc2 100644 --- a/src/steps/partition.rs +++ b/src/steps/partition.rs @@ -202,9 +202,90 @@ pub fn list_block_devices() -> Vec { } } + sort_disks(&mut disks); disks } +/// Disk listesini kararlı (deterministik) sıraya dizer. +/// +/// İKİ SORUNU birden çözer: +/// +/// 1. **Sırasızlık.** `lsblk` çıktısı da `/sys/block` `read_dir` +/// sırası da garanti değildir; `/sys/block` fallback yolunda liste +/// **önyüklemeden önyüklemeye değişebiliyor**. Bu, hem arayüzde +/// "ilk disk" kavramını anlamsız kılıyor hem de varsayılan +/// seçimlerin tutarsız olmasına yol açıyordu. +/// +/// 2. **Boot medyası listenin başında.** Çıkarılabilir aygıtlar +/// (canlı ISO'yu boot ettiğiniz USB) `available_disks`'te yer alıyor +/// ve HİÇBİR YERDE filtrelenmiyordu — `detect_drive_type` "USB" +/// döndürüyor ama o değer hiç okunmuyor. Önyükleyici hedefi olarak +/// seçilirse `grub-install` MBR'yi boot medyasına yazar ve sistem +/// açılmaz. +/// +/// Sıralama: **çıkarılabilir aygıtlar sona**, dahililer ada göre. +/// Böylece "ilk disk" her zaman bir iç disk olur — kullanıcının +/// sezgisi (firmware'in ilk açtığı disk) en azından yanlış bir aygıta +/// denk gelmez. +fn sort_disks(disks: &mut [DiskInfo]) { + disks.sort_by(|a, b| { + let ra = is_removable(&a.name); + let rb = is_removable(&b.name); + // çıkarılabilir sona + ra.cmp(&rb) + .then_with(|| disk_sort_key(&a.name).cmp(&disk_sort_key(&b.name))) + }); +} + +/// Aygıt yolunun DISK kısmı: `/dev/sdb1` → `sdb`, `/dev/nvme0n1p3` → +/// `nvme0n1`, `/dev/sda` → `sda` (baştaki `/dev/` atılır). +/// +/// ⚠️ `installer::device_to_disk` **bölüm yolu** içindir ve çıplak +/// NVMe disk adında yanlış sonuç verir: `/dev/nvme0n1` → `nvme0n` +/// (sondaki `1` rakam sanılır). Buradaki girdiler `available_disks`'ten +/// gelen **çıplak disk adları** olabildiği için ayrı yazıldı. +/// +/// 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 { + 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') { + if !base[pos + 1..].is_empty() && base[pos + 1..].chars().all(|c| c.is_ascii_digit()) { + return base[..pos].to_string(); + } + } + return base.to_string(); + } + base.trim_end_matches(|c: char| c.is_ascii_digit()).to_string() +} + +/// `/dev/sdb1` gibi bir aygıtın çıkarılabilir olup olmadığı. +/// +/// `sysfs`'e bakar (`/sys/block//removable`). Disk adı bilinmiyorsa +/// ya da aygıt çıkarılabilir değilse `false` döner — yani aygıtlar +/// dahili kabul edilir. +pub fn is_removable(device: &str) -> bool { + let name = base_disk_name(device); + std::fs::read_to_string(format!("/sys/block/{}/removable", name)) + .map(|v| v.trim() == "1") + .unwrap_or(false) +} + +/// Sıralama anahtarı: `(disk adı, bölüm numarası)`. +/// +/// Sayısal parça ayrıştırılır; ada göre sıralama `sda10 < sda2` derdi +/// ve disk şeridindeki sıra yanlış görünürdü. +fn disk_sort_key(device: &str) -> (String, u32) { + let num: u32 = device + .trim_start_matches("/dev/") + .trim_start_matches(&base_disk_name(device)) + .trim_start_matches('p') + .parse() + .unwrap_or(0); + (base_disk_name(device), num) +} + use crate::installer::{ assign_partition_numbers, CustomPartition, DiskInfo, FsType, GlobalState, InstallerStep, PartitionPlan, PartitionTableType, VolumeGroup, @@ -3043,6 +3124,90 @@ mod tests { assert_eq!(format_number_thousands(7000000), "7.000.000"); } + // ── sort_disks / disk_sort_key ──────────────────────────── + + /// Disk listesi kararlı olmalı: "ilk disk" kavramı ve varsayılan + /// seçimler aksi hâlde önyüklemeden önyüklemeye değişebilir. + #[test] + fn disks_are_sorted_by_name() { + use super::disk_sort_key; + let mut names = vec!["/dev/sdc", "/dev/sda", "/dev/sdb"]; + names.sort_by_key(|n| disk_sort_key(n)); + assert_eq!(names, vec!["/dev/sda", "/dev/sdb", "/dev/sdc"]); + } + + /// `sda10`, ada göre sıralamada `sda2`'den ÖNCE gelirdi — yanlış. + /// Sayısal parça ayrıştırılır. + #[test] + fn partition_numbers_sort_numerically() { + use super::disk_sort_key; + let mut names = vec!["/dev/sda10", "/dev/sda2", "/dev/sda1"]; + names.sort_by_key(|n| disk_sort_key(n)); + assert_eq!( + names, + vec!["/dev/sda1", "/dev/sda2", "/dev/sda10"], + "sda10 sda2'den sonra gelmeli" + ); + } + + /// NVMe: çıplak disk adında sondaki rakam DISK ADININ parçasıdır. + /// + /// REGRESYON: ilk yazımda `trim_end_matches(is_ascii_digit)` her + /// yola uygulanıyordu ve `nvme0n1` → `nvme0n` oluyordu; sysfs + /// yolu `/sys/block/nvme0n/removable` olurdu, aygıt çıkarılabilir + /// sayılmazdı. `device_to_disk` de aynı hatalı — o işlev bölüm + /// yolu içindir, çıplak disk adında kullanılmamalı. + #[test] + fn nvme_names_split_correctly() { + use super::base_disk_name; + assert_eq!(base_disk_name("/dev/nvme0n1"), "nvme0n1"); + assert_eq!(base_disk_name("/dev/nvme0n1p3"), "nvme0n1"); + assert_eq!(base_disk_name("/dev/nvme1n2"), "nvme1n2"); + assert_eq!(base_disk_name("/dev/mmcblk0"), "mmcblk0"); + assert_eq!(base_disk_name("/dev/mmcblk0p1"), "mmcblk0"); + assert_eq!(base_disk_name("/dev/sda"), "sda"); + assert_eq!(base_disk_name("/dev/sda1"), "sda"); + assert_eq!(base_disk_name("/dev/vda3"), "vda"); + } + + /// Sıralama anahtarı hem disk adını hem bölüm numarasını doğru + /// ayırmalı. + #[test] + fn sort_key_handles_bare_disks_and_partitions() { + use super::disk_sort_key; + assert_eq!(disk_sort_key("/dev/sda"), ("sda".to_string(), 0)); + assert_eq!(disk_sort_key("/dev/sda3"), ("sda".to_string(), 3)); + assert_eq!(disk_sort_key("/dev/nvme0n1"), ("nvme0n1".to_string(), 0)); + assert_eq!(disk_sort_key("/dev/nvme0n1p3"), ("nvme0n1".to_string(), 3)); + } + + /// Çıkarılabilir aygıtlar SONA gider — canlı ISO'nun USB'i + /// "ilk disk" olamaz. + /// + /// Bu, "ilk disk" kavramını güvenli kılan asıl nokta: önyükleyici + /// hedefi olarak seçilirse `grub-install` MBR'yi boot medyasına + /// yazar ve sistem açılmaz. + #[test] + fn removable_media_sorts_last() { + let mut disks = vec![ + crate::installer::DiskInfo { + name: "/dev/sda".into(), vendor: String::new(), model: String::new(), + drive_type: "SSD".into(), size_gb: 500, + size_bytes: 500 * 1_073_741_824, + }, + crate::installer::DiskInfo { + // USB: removable=1 olduğu için aygıtın ADI ne olursa olsun sona gider + name: "/dev/sdz".into(), vendor: String::new(), model: String::new(), + drive_type: "USB".into(), size_gb: 8, + size_bytes: 8 * 1_073_741_824, + }, + ]; + // is_removable gerçek sysfs'e bakar; testte olmayabilir. + // Sıralamanın "ada göre" kısmını doğrula (removable hepsi false ise). + super::sort_disks(&mut disks); + assert_eq!(disks[0].name, "/dev/sda", "ada göre sda önce gelmeli"); + } + // ── detect_drive_type ───────────────────────────────────── #[test]