fix(bootloader): disk listesini sırala, boot medyasını hedeften ele

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 <usb>` 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) <noreply@anthropic.com>
This commit is contained in:
Erkan IŞIK
2026-09-27 18:48:44 +03:00
parent f150ab0002
commit c6a1d0fb6f
5 changed files with 221 additions and 2 deletions
+29 -2
View File
@@ -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()`).
+1
View File
@@ -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):"
+1
View File
@@ -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ı):"
+25
View File
@@ -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 <usb>` 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<String> = 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)
+165
View File
@@ -202,9 +202,90 @@ pub fn list_block_devices() -> Vec<crate::installer::DiskInfo> {
}
}
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/<dev>/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]