diff --git a/Cargo.lock b/Cargo.lock index afbf1b1b..12e8a560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -782,6 +782,16 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "file-guard" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ef72acf95ec3d7dbf61275be556299490a245f017cf084bd23b4f68cf9407c" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "filesystem-table" version = "0.1.2" @@ -2618,6 +2628,7 @@ dependencies = [ "const_format", "derivative", "enum_dispatch", + "file-guard", "filesystem-table", "format-bytes", "freedesktop-desktop-entry", diff --git a/Cargo.toml b/Cargo.toml index 8c6851f7..b0199b02 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ no_color = "0.2.0" poly_l10n = "0.0.6" derivative = "2.2.0" parking_lot = "0.12.3" +file-guard = "0.2.0" # [dependencies.os-detect] # git = "https://github.com/FyraLabs/distinst" diff --git a/po/en-US/readymade.ftl b/po/en-US/readymade.ftl index 0364e40d..d0bb864a 100644 --- a/po/en-US/readymade.ftl +++ b/po/en-US/readymade.ftl @@ -92,4 +92,5 @@ stage-grub1 = Generating stage 1 grub.cfg in ESP... stage-grub2 = Generating stage 2 grub.cfg in /boot/grub2/grub.cfg... stage-biosgrub = Installing BIOS Grub2 stage-kernel = Reinstalling kernels +stage-recovery = Setting up recovery environment stage-selinux = Setting SELinux labels diff --git a/po/es/readymade.ftl b/po/es/readymade.ftl index 977eecfd..0c1d31aa 100644 --- a/po/es/readymade.ftl +++ b/po/es/readymade.ftl @@ -20,4 +20,10 @@ page-installation = Instalación page-installation-progress = Instalando sistema base... page-installationtype = Tipo de Instalación page-installationtype-chromebook = Chromebook - +unknown-os = SO Desconocido +page-installationtype-tpm = Habilitar TPM +parttype-esp = Partición de sistema EFI ({ $path }) +parttype-home = Información de usuario +parttype-other = Punto de montaje personalizado +page-welcome = Bienvenido a { $distro } +page-installationtype-encrypt = Habilitar encriptación de disco diff --git a/po/fr/readymade.ftl b/po/fr/readymade.ftl index 0de2ae87..ee4e9be3 100644 --- a/po/fr/readymade.ftl +++ b/po/fr/readymade.ftl @@ -5,7 +5,7 @@ next = Suivant unknown-os = OS inconnu parttype-root = Racine du système de fichiers ({ $path }) -parttype-esp = Partition EFI système ({ $path }) +parttype-esp = Partition système EFI ({ $path }) parttype-home = Données utilisateur ({ $path }) parttype-var = Données variables ({ $path }) page-welcome = Bienvenue dans { $distro } @@ -45,7 +45,7 @@ page-installationtype-chromebook = Chromebook page-installationtype-custom = Personnalisée dialog-installtype-encrypt = Chiffrement du disque dialog-installtype-password = Mot de passe -dialog-installtype-repeat = Saisissez le mot de passe à nouveau +dialog-installtype-repeat = Saisissez à nouveau le mot de passe dialog-installtype-cancel = Annuler dialog-installtype-confirm = Confirmer installtype-edit-mp = Modifier le point de montage @@ -53,7 +53,7 @@ installtype-rm-mp = Supprimer le point de montage dialog-mp-part = Partition dialog-mp-at = Monter sur dialog-mp-opts = Options de montage -installtype-parttool = Sélectionnez votre outil de partitionnement +installtype-parttool = Sélectionner votre outil de partitionnement stage-extracting = Extraction des fichiers stage-copying = Copie des fichiers stage-initramfs = Régénération de l'initramfs @@ -73,9 +73,9 @@ page-installationtype-dual = Double démarrage stage-grub2 = Génération du fichier grub.cfg d'étape 2 dans /boot/grub2/grub.cfg... dialog-installtype-encrypt-desc = Veuillez définir le mot de passe de chiffrement du disque. - Si vous perdez ce mot de passe, vos données ne seront pas récupérables. + Si vous perdez ce mot de passe, vos données ne pourront pas être récupérées. stage-grub = Génération des valeurs système par défaut pour GRUB -stage-selinux = Définition des étiquettes SELinux +stage-selinux = Ajout des étiquettes SELinux page-confirmation-problem-device-mounted = { $dev } est monté sur { $mountpoint }. Démontez-le pour continuer. page-confirmation-problem-devblkopen = Le périphérique de blocs { $dev } est utilisé par les processus suivants : diff --git a/po/ru/readymade.ftl b/po/ru/readymade.ftl index 98b016e3..47f953e8 100644 --- a/po/ru/readymade.ftl +++ b/po/ru/readymade.ftl @@ -8,7 +8,7 @@ parttype-home = Домашняя директория пользователя ( parttype-var = Переменные данные ({ $path }) parttype-other = Пользовательская точка монтирования раздела page-welcome = Добро пожаловать в { $distro } -page-welcome-desc = Либо попробуйте { $distro } из этой программы установки, либо начните установку сейчас. Вы всегда можете вернуться к этому экрану, выбрав в меню приложение «Installer». +page-welcome-desc = Вы можете попробовать { $distro }, или начать установку уже сейчас. page-welcome-try = Попробовать page-welcome-install = Установить page-failure = Сбой установки @@ -35,11 +35,11 @@ page-installation-help = Нужна помощь? page-installation-help-desc = Задайте вопрос в одном из наших чатов! page-installation-contrib = Внести вклад в { $distro } page-installation-contrib-desc = Узнайте, как пожертвовать своим временем, деньгами или оборудованием. -page-installation-progress = Установка базовой системы... +page-installation-progress = Установка базовых системных пакетов... page-installcustom = Пользовательская установка page-installcustom-title = Разделы и точки монтирования page-installcustom-desc = { $num } правил(а) -page-installcustom-tool = Открыть дисковую утилиту +page-installcustom-tool = Открыть утилиту разметки page-installcustom-add = Добавить новое правило page-installationtype = Тип установки page-installationtype-entire = Весь диск @@ -61,7 +61,7 @@ installtype-rm-mp = Удалить точку монтирования dialog-mp-part = Раздел dialog-mp-at = Монтировать на dialog-mp-opts = Настройки монтирования -installtype-parttool = Выберите вашу дисковую утилиту +installtype-parttool = Выберите свою утилиту разметки stage-extracting = Распаковка файлов stage-copying = Копирование файлов stage-mkpart = Создание разделов и копирование файлов @@ -70,5 +70,10 @@ stage-grub = Генерация системных настроек grub по у stage-grub1 = Генерация stage 1 grub.cfg на ESP... stage-grub2 = Генерация stage 2 grub.cfg в /boot/grub2/grub.cfg... stage-biosgrub = Установка BIOS Grub2 -stage-kernel = Повторная установка ядер +stage-kernel = Переустановка ядер stage-selinux = Установка политик SELinux +page-confirmation-problem-devblkopen = + Накопитель { $dev } уже используется следующими процессами: + { $pids } + Данные процессы должны быть остановлены для дальнейшей работы установщика. +page-confirmation-problem-device-mounted = { $dev } уже смонтирован в { $mountpoint }. Размонтируйте чтобы продолжить. diff --git a/src/backend/install.rs b/src/backend/install.rs index f0200060..0ccb68a9 100644 --- a/src/backend/install.rs +++ b/src/backend/install.rs @@ -1,8 +1,10 @@ +use file_guard::Lock; use ipc_channel::ipc::{IpcError, IpcOneShotServer, IpcSender}; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use std::{ io::Write, + os::unix::process::CommandExt, path::{Path, PathBuf}, process::{Command, Stdio}, sync::OnceLock, @@ -818,17 +820,29 @@ impl FinalInstallationState { ["--key-file", keyfile_path] }); - let repart_cmd = Command::new("systemd-repart") - .args(["--dry-run", if dry_run { "yes" } else { "no" }]) - .args(["--definitions", cfgdir.to_str().unwrap()]) - .args(["--empty", "force", "--offline", "false", "--json", "pretty"]) - .args(["--copy-source", ©_source].iter().filter(|_| !is_bootc)) - .args(arg_keyfile.iter().flatten()) - .arg(blockdev) - .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) - .output() - .context("can't run systemd-repart")?; + // Scope to ensure device and lock live long enough for the command + let repart_cmd = { + // lock device + let mut device = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(blockdev) + .context("Failed to open block device")?; + // We are locking the device so that repart doesn't fail due to device busy + // The lock is held in this scope + let mut _lock = file_guard::lock(&mut device, Lock::Exclusive, 0, 1)?; + Command::new("systemd-repart") + .args(["--dry-run", if dry_run { "yes" } else { "no" }]) + .args(["--definitions", cfgdir.to_str().unwrap()]) + .args(["--empty", "force", "--offline", "false", "--json", "pretty"]) + .args(["--copy-source", ©_source].iter().filter(|_| !is_bootc)) + .args(arg_keyfile.iter().flatten()) + .arg(blockdev) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .output() + .context("can't run systemd-repart")? + }; if !repart_cmd.status.success() { bail!( diff --git a/src/backend/postinstall/reinstall_kernel.rs b/src/backend/postinstall/reinstall_kernel.rs index 54c9783f..ca69a089 100644 --- a/src/backend/postinstall/reinstall_kernel.rs +++ b/src/backend/postinstall/reinstall_kernel.rs @@ -11,21 +11,29 @@ pub struct ReinstallKernel; impl PostInstallModule for ReinstallKernel { fn run(&self, _context: &Context) -> Result<()> { let kernel_vers = std::fs::read_dir("/lib/modules")? - .map(|entry| entry.unwrap().file_name()) + .filter_map(|entry| entry.ok().map(|e| e.file_name())) .collect_vec(); tracing::info!(?kernel_vers, "Kernel versions found"); // We're gonna just install the first kernel we find, so let's do that - let kver = kernel_vers.first().unwrap().to_str().unwrap(); + let kver = kernel_vers + .first() + .ok_or_else(|| color_eyre::eyre::eyre!("No kernel versions found in /lib/modules"))? + .to_str() + .ok_or_else(|| color_eyre::eyre::eyre!("Kernel version filename is not valid UTF-8"))?; // install kernel + let vmlinuz_path = format!("/lib/modules/{kver}/vmlinuz"); + if !std::path::Path::new(&vmlinuz_path).exists() { + bail!("Kernel version {kver} does not have a vmlinuz file at {vmlinuz_path}"); + } stage!(kernel { let kernel_install_cmd_status = Command::new("kernel-install") .arg("add") .arg(kver) - .arg(format!("/lib/modules/{kver}/vmlinuz")) + .arg(&vmlinuz_path) .arg("--verbose") .status()?; @@ -37,6 +45,39 @@ impl PostInstallModule for ReinstallKernel { } }); + stage!(recovery { + // copy to /boot/vmlinuz-recovery + let recovery_vmlinuz_path = "/boot/vmlinuz-recovery".to_owned(); + if std::path::Path::new(&recovery_vmlinuz_path).exists() { + tracing::warn!("Recovery kernel already exists at {recovery_vmlinuz_path}, skipping copy"); + } else { + std::fs::copy(&vmlinuz_path, &recovery_vmlinuz_path) + .map_err(|e| color_eyre::eyre::eyre!(e))?; + } + + // create a recovery initramfs + let recovery_initramfs_path = "/boot/initramfs-recovery.img".to_owned(); + + let recovery_dracut = Command::new("dracut") + .arg("--force") + .arg("--add") + .arg("dmsquash-live overlayfs rescue") + .arg("--no-hostonly") + .arg("--no-uefi") + .arg("--kver") + .arg(kver) + .arg(&recovery_initramfs_path) + .status()?; + if !recovery_dracut.success() { + bail!( + "dracut failed with exit code {:?}", + recovery_dracut.code() + ); + } + tracing::info!("Recovery initramfs created at {recovery_initramfs_path}"); + }); + // todo: grub.d template for boot entry in OS + Ok(()) } } diff --git a/src/backend/repart_output.rs b/src/backend/repart_output.rs index 772310ce..3d68b463 100644 --- a/src/backend/repart_output.rs +++ b/src/backend/repart_output.rs @@ -229,6 +229,13 @@ impl RepartOutput { .map(|part| part.node.clone()) } + pub fn get_recovery_partition(&self) -> std::option::Option { + self.partitions + .iter() + .find(|part| part.label == "os_recovery") + .map(|part| part.node.clone()) + } + /// Create [`tiffin::Container`] from the repartitioning output with the mountpoints /// from the DDI partition types pub fn to_container( diff --git a/templates/wholedisk/20-efi.conf b/templates/wholedisk/20-efi.conf index b5dae29d..13cbb0d5 100644 --- a/templates/wholedisk/20-efi.conf +++ b/templates/wholedisk/20-efi.conf @@ -6,3 +6,4 @@ SizeMaxBytes=512M MountPoint=/boot/efi:umask=0077,shortname=winnt # This path is actually relative; see man repart.d at --copy-source option CopyFiles=/boot/efi/:/ +FactoryReset=true \ No newline at end of file diff --git a/templates/wholedisk/45-boot.conf b/templates/wholedisk/45-boot.conf index 82109b16..145fee73 100644 --- a/templates/wholedisk/45-boot.conf +++ b/templates/wholedisk/45-boot.conf @@ -1,10 +1,11 @@ [Partition] Format=ext4 SizeMinBytes=1G -Weight=100 +Weight=200 SizeMaxBytes=2G MountPoint=/boot/:defaults # This path is actually relative; see man repart.d at --copy-source option CopyFiles=/boot:/ ExcludeFiles=/boot/efi Type=xbootldr +FactoryReset=true \ No newline at end of file diff --git a/templates/wholedisk/47-recovery.conf b/templates/wholedisk/47-recovery.conf new file mode 100644 index 00000000..c10187da --- /dev/null +++ b/templates/wholedisk/47-recovery.conf @@ -0,0 +1,16 @@ +# This partition is an EROFS mirror of rootfs, used for special recovery purposes +[Partition] +# Let's reserve this +Type=8DA63339-0007-60C0-C436-083AC8230908 +Format=erofs +SizeMinBytes=4G +CopyFiles=/:/ +Label=os_recovery +# Priority of 5000 ensures that it wouldn't be created if +# We don't have enough space +Priority=5000 +Weight=100 +# This partition will be 4-8GB in size, for future-proofing and +# recovery image updates. +SizeMaxBytes=8G +FactoryReset=false \ No newline at end of file diff --git a/templates/wholedisk/50-root.conf b/templates/wholedisk/50-root.conf index b4ec0682..17dd3f61 100644 --- a/templates/wholedisk/50-root.conf +++ b/templates/wholedisk/50-root.conf @@ -5,7 +5,10 @@ DefaultSubvolume=/ Type=root Compression=zstd ExcludeFiles=/boot/ +Priority=-10000 +Weight=5000 Format=btrfs CopyFiles=/:/ Subvolumes=/ /home CompressionLevel=1 +FactoryReset=true \ No newline at end of file