feat(bootloader): varsayılan aygıt artık mevcut işletim sistemi olan disk

Kullanıcı kararı: "zaten işletim sistemi olan diski seç".

## 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

Salt kök diski varsayılan tutmak dual-boot'ta sistemi açılmaz
bırakıyordu. `selected_disk` ise manuel modda "aktif düzenleme
diski" olduğu için /home eklemek için geçilen diske yanlış yönlendiriyordu.

## Kural (default_boot_device, saf + 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)

"İş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.

Tek diskli kurulumda 3. adım çalışır → davranış kök diskiyle aynı,
geriye dönük uyum bozulmaz.

## Tespit: alt süreç YOK

`disk_partition_count()` sysfs'ten okuyor
(`/sys/block/<disk>/<disk><N>` girdileri). `lsblk` çağırmak
`on_enter`'da bile gereksiz ve kare başına alt süreç yasağına
ayrık düşüyordu.

## UI

`boot_device_reason` aynı fonksiyonu çağırıp seçimin bu kurala
uyup uymadığını denetler; uyuyorsa gerekçe metni gösterilir. Böylece
sessiz bir varsayılan sürpriz olmaz ve **ikinci bir uygulama olmaz**.

## Testler (129 → 137)

bootloader.rs'de test modülü yoktu, eklendi (8 test):
- mevcut OS diski seçilir (kullanıcının senaryosu)
- tek disk → kök disk
- hiçbir disk bölümlü değil → kök disk
- kök disk "mevcut OS" sayılmaz
- birden çok adayda sıralı ilki seçilir
- kök disk bilinmiyorsa
- liste boşken kök disk kullanılır
- hiçbir şey bilinmiyorsa None

Bir test ilk yazımda yanlış beklenti koydu: liste boşken None bekliyordum,
doğrusu kök diske düşmek (liste yalnızca OS diski arayışı için gerekiyor).
Beklenti düzeltildi.

cargo check --all-targets : 0 hata, 0 uyarı
cargo test              : 137/137

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erkan IŞIK
2026-09-27 18:58:59 +03:00
parent c6a1d0fb6f
commit bd407adab3
5 changed files with 275 additions and 12 deletions
+41 -2
View File
@@ -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()`).
+1
View File
@@ -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):"
+1
View File
@@ -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ı):"
+231 -9
View File
@@ -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/<disk>/<disk><N>`, `…/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 "<base><rakam>" 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<F>(
root_disk: Option<&str>,
disks: &[crate::installer::DiskInfo],
has_partitions: F,
) -> Option<String>
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<String> {
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<String> = 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);
}
}
+1 -1
View File
@@ -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') {