parted mkpart söz dizimi + canlı autologin çakışması düzeltmeleri

- Manuel bölümleme parted söz dizimi disk etiketine göre seçiliyor
  (GPT: ad+fs, msdos: primary/logical+fs); etiket parted print'ten
  okunuyor, boş diske mktable kuruluyor
- Yerelleşmiş parted çıktısı P0 veri kaybı yapıyordu: LC_ALL=C ile
  okunuyor, ayrıştırıcı Türkçe başlığı da tanıyor, mktable öncesi
  fiziksel bölüm kilidi var
- Otomatik mod mkpart 4 argümana geçti (ad + fs türü)
- GPT '/' adı sfdisk ile boşaltılıyor (warn-only, kurulumu durdurmaz)
- ConfigureAutologinJob: /etc/sddm.conf.d/10-yali-autologin.conf
  kanonik dosya, hedeften oturum tespiti, kapalıyken canlı
  config'in ezilmesi (boş User= / lightdm satır temizliği)
- Checkbox etiketi: 'Otomatik oturum aç' / 'Log in automatically'
- 158 test, 0 uyarı
This commit is contained in:
Erkan IŞIK
2026-09-29 18:59:42 +03:00
parent 546bb79be7
commit af33334b2c
5 changed files with 520 additions and 79 deletions
+49 -2
View File
@@ -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
+1 -1
View File
@@ -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)"
+1 -1
View File
@@ -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ı)"
+175 -46
View File
@@ -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::<Vec<_>>()
.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<String> {
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<String> {
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");
}
}
+294 -29
View File
@@ -298,7 +298,145 @@ fn read_physical_extents(disk: &str) -> Result<Vec<PhysicalExtent>, 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<bool> {
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<bool> {
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 <NAME> <FS> START END` (NAME serbest metin, boş OLAMAZ)
/// - msdos: `mkpart <TYPE> <FS> 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 <disk> <N> ""` 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 <AD> <FS> START END
run_parted(&["-s", disk, "mkpart", "EFI", "fat32", &start_s, &end_s]).await?;
} else {
// msdos: mkpart <TÜR> <FS> 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 <AD> <FS> START END (AD boş olamaz)
// msdos: mkpart <TÜR> <FS> 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);
}
}