forked from pisilinux-rs/yali-rs
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:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user