new
This commit is contained in:
176
AGENTS.md
176
AGENTS.md
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
**BootRepairTool** 是一个用于修复 Linux 系统 GRUB 引导的图形化工具。适用于 Live USB/CD 环境,帮助用户修复损坏的 GRUB 引导加载器。
|
**BootRepairTool** 是一个用于修复 Linux 系统 GRUB 引导的图形化工具。适用于 Live USB/CD 环境,帮助用户修复损坏的 GRUB 引导加载器。
|
||||||
|
|
||||||
|
**v2.0 更新**: 参考 Calamares 安装程序的引导安装模块进行了全面优化,新增多架构支持、EFI fallback、混合启动模式、btrfs 子卷支持等功能。
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -25,58 +27,127 @@ BootRepairTool/
|
|||||||
### 1. 分区扫描 (`backend.py:scan_partitions`)
|
### 1. 分区扫描 (`backend.py:scan_partitions`)
|
||||||
- 使用 `lsblk -J` 获取磁盘和分区信息
|
- 使用 `lsblk -J` 获取磁盘和分区信息
|
||||||
- 识别 EFI 系统分区 (ESP)
|
- 识别 EFI 系统分区 (ESP)
|
||||||
|
- **NEW**: 支持 LVM 逻辑卷检测
|
||||||
|
- **NEW**: 支持 LUKS 加密分区检测
|
||||||
|
- **NEW**: 支持 btrfs 子卷检测
|
||||||
- 过滤 Live 系统自身的分区
|
- 过滤 Live 系统自身的分区
|
||||||
|
|
||||||
### 2. 系统挂载 (`backend.py:mount_target_system`)
|
### 2. 系统挂载 (`backend.py:mount_target_system`)
|
||||||
- 挂载根分区 (/)
|
- 挂载根分区 (/)
|
||||||
|
- **NEW**: 支持 btrfs 子卷挂载
|
||||||
- 挂载独立 /boot 分区(可选)
|
- 挂载独立 /boot 分区(可选)
|
||||||
- 挂载 EFI 分区(UEFI 模式)
|
- 挂载 EFI 分区(UEFI 模式)
|
||||||
- 绑定伪文件系统 (/dev, /proc, /sys, /run)
|
- 绑定伪文件系统 (/dev, /proc, /sys, /run)
|
||||||
|
|
||||||
### 3. 发行版检测 (`backend.py:detect_distro_type`)
|
### 3. 发行版检测 (`backend.py:detect_distro_type`)
|
||||||
- 支持: Arch, CentOS/RHEL, Debian, Ubuntu
|
支持以下发行版:
|
||||||
- 通过读取 `/etc/os-release` 识别
|
- **Arch 系**: Arch Linux, Manjaro, EndeavourOS, Garuda, CachyOS
|
||||||
|
- **Debian 系**: Debian, Ubuntu
|
||||||
|
- **RHEL 系**: CentOS, RHEL, Rocky Linux, AlmaLinux, Fedora
|
||||||
|
- **openSUSE**: Leap, Tumbleweed
|
||||||
|
- **NEW**: Void Linux
|
||||||
|
- **NEW**: Gentoo
|
||||||
|
- **NEW**: NixOS
|
||||||
|
|
||||||
### 4. GRUB 修复 (`backend.py:chroot_and_repair_grub`)
|
### 4. GRUB 修复 (`backend.py:chroot_and_repair_grub`)
|
||||||
- **BIOS 模式**: `grub-install /dev/sdX`
|
|
||||||
- **UEFI 模式**: `grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=GRUB`
|
|
||||||
- 更新 GRUB 配置:
|
|
||||||
- Debian/Ubuntu: `update-grub`
|
|
||||||
- Arch: `grub-mkconfig -o /boot/grub/grub.cfg`
|
|
||||||
- CentOS: `grub2-mkconfig -o /boot/grub2/grub.cfg`
|
|
||||||
|
|
||||||
### 5. 卸载清理 (`backend.py:unmount_target_system`)
|
#### BIOS 模式
|
||||||
|
- `grub-install --target=i386-pc --recheck --force /dev/sdX`
|
||||||
|
|
||||||
|
#### UEFI 模式 (参考 Calamares 实现)
|
||||||
|
- 自动检测系统架构 (x86_64/i386/arm64/loongarch64)
|
||||||
|
- 获取正确的 EFI 参数 (target, grub_file, boot_file)
|
||||||
|
- **多重安装策略**(自动回退):
|
||||||
|
1. 标准安装(带 NVRAM)
|
||||||
|
2. Live 环境/可移动模式(`--removable`)
|
||||||
|
3. 无 NVRAM 模式(`--no-nvram`)
|
||||||
|
- **NEW**: EFI Fallback 安装(复制到 `/EFI/Boot/bootx64.efi`)
|
||||||
|
- **NEW**: 支持自定义 EFI 启动项名称
|
||||||
|
|
||||||
|
#### 混合启动模式 (Hybrid GRUB)
|
||||||
|
- **NEW**: 同时安装 BIOS 和 UEFI 引导
|
||||||
|
- 适用于需要在不同固件模式下启动的场景
|
||||||
|
|
||||||
|
#### GRUB 配置更新
|
||||||
|
根据发行版自动选择正确的命令:
|
||||||
|
- Debian/Ubuntu: `update-grub` 或 `grub-mkconfig`
|
||||||
|
- Arch: `grub-mkconfig -o /boot/grub/grub.cfg`
|
||||||
|
- CentOS/RHEL/Fedora/openSUSE: `grub2-mkconfig -o /boot/grub2/grub.cfg`
|
||||||
|
|
||||||
|
### 5. 系统检测功能
|
||||||
|
|
||||||
|
#### EFI 检测 (`backend.get_efi_word_size`)
|
||||||
|
- 读取 `/sys/firmware/efi/fw_platform_size`
|
||||||
|
- 支持 32 位和 64 位 EFI
|
||||||
|
|
||||||
|
#### 架构检测 (`backend.get_grub_efi_parameters`)
|
||||||
|
支持多种架构:
|
||||||
|
| 架构 | EFI Target | GRUB 文件 | Boot 文件 |
|
||||||
|
|------|-----------|-----------|-----------|
|
||||||
|
| x86_64 | x86_64-efi | grubx64.efi | bootx64.efi |
|
||||||
|
| i386 | i386-efi | grubia32.efi | bootia32.efi |
|
||||||
|
| arm64 | arm64-efi | grubaa64.efi | bootaa64.efi |
|
||||||
|
| loongarch64 | loongarch64-efi | grubloongarch64.efi | bootloongarch64.efi |
|
||||||
|
|
||||||
|
#### Secure Boot 检测 (`backend.check_secure_boot_status`)
|
||||||
|
- 通过 `mokutil --sb-state` 检测
|
||||||
|
- 通过 EFI 变量检测
|
||||||
|
- 显示 Secure Boot 状态
|
||||||
|
|
||||||
|
#### Live 环境检测 (`backend.is_live_environment`)
|
||||||
|
- 检测 `/sys/firmware/efi/efivars` 可访问性
|
||||||
|
- 自动调整安装策略
|
||||||
|
|
||||||
|
### 6. 卸载清理 (`backend.py:unmount_target_system`)
|
||||||
- 逆序卸载绑定的文件系统
|
- 逆序卸载绑定的文件系统
|
||||||
- 卸载 EFI/boot/根分区
|
- 卸载 EFI/boot/根分区
|
||||||
- 清理临时挂载点
|
- 清理临时挂载点
|
||||||
|
- **NEW**: 重试机制处理繁忙资源
|
||||||
|
|
||||||
## 前端界面 (`frontend.py`)
|
## 前端界面 (`frontend.py`)
|
||||||
|
|
||||||
### 主要组件
|
### 主要组件
|
||||||
|
|
||||||
1. **分区选择区域**
|
1. **系统信息区域**
|
||||||
|
- 显示 CPU 架构
|
||||||
|
- 显示 EFI 位数(32/64)
|
||||||
|
- 显示 Live 环境状态
|
||||||
|
- 显示 Secure Boot 状态
|
||||||
|
|
||||||
|
2. **分区选择区域**
|
||||||
- 根分区 (/) - 必选
|
- 根分区 (/) - 必选
|
||||||
|
- **NEW**: btrfs 子卷选择(自动显示)
|
||||||
- 独立 /boot 分区 - 可选
|
- 独立 /boot 分区 - 可选
|
||||||
- EFI 系统分区 (ESP) - UEFI 模式下推荐
|
- EFI 系统分区 (ESP) - UEFI 模式下推荐
|
||||||
|
|
||||||
2. **目标磁盘选择**
|
3. **目标磁盘选择**
|
||||||
- 物理磁盘选择 (如 /dev/sda)
|
- 物理磁盘选择
|
||||||
- UEFI 模式复选框(自动检测 Live 环境)
|
- UEFI 模式复选框(根据 Live 环境自动设置)
|
||||||
|
|
||||||
3. **日志窗口**
|
4. **高级选项**
|
||||||
- 彩色日志输出(信息/警告/错误/成功)
|
- **NEW**: 混合启动模式(同时安装 BIOS + UEFI)
|
||||||
|
- **NEW**: EFI Fallback 选项
|
||||||
|
- **NEW**: 自定义 EFI 启动项名称
|
||||||
|
- **NEW**: 强制安装选项
|
||||||
|
|
||||||
|
5. **日志窗口**
|
||||||
|
- 彩色日志输出(调试/信息/警告/错误/成功/步骤)
|
||||||
|
- 时间戳显示
|
||||||
- 自动滚动
|
- 自动滚动
|
||||||
|
- 使用等宽字体(Consolas)
|
||||||
|
|
||||||
### 工作流程
|
### 工作流程
|
||||||
|
|
||||||
```
|
```
|
||||||
扫描分区 → 用户选择分区 → 点击修复 → 挂载系统 →
|
扫描分区 → 用户选择分区 → 检测系统信息 →
|
||||||
|
配置高级选项 → 点击修复 → 挂载系统 →
|
||||||
检测发行版 → chroot修复GRUB → 卸载分区 → 完成
|
检测发行版 → chroot修复GRUB → 卸载分区 → 完成
|
||||||
```
|
```
|
||||||
|
|
||||||
### 线程安全
|
### 自动功能
|
||||||
|
- **自动选择目标磁盘**: 根据根分区路径推断父磁盘
|
||||||
修复过程在独立线程中运行,避免 UI 卡死。使用 `master.after()` 进行线程安全的日志更新。
|
- **自动检测 btrfs**: 显示子卷选择器
|
||||||
|
- **自动检测 LVM**: 提示手动选择磁盘
|
||||||
|
- **UEFI 模式建议**: 根据 Live 环境给出建议
|
||||||
|
|
||||||
## 代码规范
|
## 代码规范
|
||||||
|
|
||||||
@@ -88,10 +159,26 @@ success, stdout, stderr = run_command([...])
|
|||||||
success, disks, partitions, efi_parts, err = scan_partitions()
|
success, disks, partitions, efi_parts, err = scan_partitions()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 日志系统
|
||||||
|
使用分级日志系统:
|
||||||
|
```python
|
||||||
|
log_debug(msg) # 调试信息(灰色)
|
||||||
|
log_info(msg) # 一般信息(黑色)
|
||||||
|
log_warning(msg) # 警告(橙色)
|
||||||
|
log_error(msg) # 错误(红色)
|
||||||
|
log_success(msg) # 成功(绿色)
|
||||||
|
log_step(msg) # 步骤(蓝色加粗)
|
||||||
|
```
|
||||||
|
|
||||||
|
支持日志回调,可将日志实时显示到前端:
|
||||||
|
```python
|
||||||
|
backend.set_log_callback(callback_function)
|
||||||
|
```
|
||||||
|
|
||||||
### 错误处理
|
### 错误处理
|
||||||
- 命令执行失败时自动清理已挂载的分区
|
- 命令执行失败时自动清理已挂载的分区
|
||||||
- 日志记录使用 `[Backend]` 前缀
|
- 详细的错误信息返回
|
||||||
- GUI 使用弹窗显示关键错误
|
- 多重回退策略(如 GRUB 安装)
|
||||||
|
|
||||||
## 使用限制
|
## 使用限制
|
||||||
|
|
||||||
@@ -100,27 +187,42 @@ success, disks, partitions, efi_parts, err = scan_partitions()
|
|||||||
3. **单线程修复**: 同时只能进行一个修复任务
|
3. **单线程修复**: 同时只能进行一个修复任务
|
||||||
4. **UEFI 自动检测**: 根据 Live 环境自动设置 UEFI 模式
|
4. **UEFI 自动检测**: 根据 Live 环境自动设置 UEFI 模式
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### EFI 启动问题
|
||||||
|
1. **标准模式安装失败**: 工具会自动尝试 removable 模式
|
||||||
|
2. **无法找到启动项**: 启用 EFI Fallback 选项
|
||||||
|
3. **Secure Boot 问题**: 需要在 BIOS 中禁用 Secure Boot
|
||||||
|
4. **固件不兼容**: 某些固件需要特定的 EFI 文件路径
|
||||||
|
|
||||||
|
### 分区问题
|
||||||
|
1. **LVM 逻辑卷**: 需要手动选择目标磁盘
|
||||||
|
2. **LUKS 加密**: 需要预先解锁加密分区
|
||||||
|
3. **btrfs 子卷**: 工具会自动检测并显示选择器
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
|
|
||||||
直接运行 `backend.py` 可进行基础测试(仅扫描分区):
|
直接运行 `backend.py` 可进行基础测试:
|
||||||
```bash
|
```bash
|
||||||
sudo python backend.py
|
sudo python backend.py
|
||||||
```
|
```
|
||||||
|
|
||||||
完整修复测试需要取消注释 `__main__` 块中的测试代码并设置实际分区。
|
完整修复测试需要设置实际分区参数。
|
||||||
|
|
||||||
|
## 参考
|
||||||
|
|
||||||
|
本工具 v2.0 参考了以下项目:
|
||||||
|
- **Calamares**: https://calamares.io
|
||||||
|
- 引导安装模块 (`bootloader/main.py`)
|
||||||
|
- EFI 参数获取逻辑
|
||||||
|
- 多重安装策略
|
||||||
|
|
||||||
## 潜在改进点
|
## 潜在改进点
|
||||||
|
|
||||||
1. **添加 requirements.txt**: 当前无第三方依赖
|
1. **systemd-boot 支持**: 添加 systemd-boot 安装选项
|
||||||
2. **国际化**: 当前仅支持中文界面
|
2. **rEFInd 支持**: 添加 rEFInd 安装选项(后端已支持)
|
||||||
3. **配置文件**: 支持保存/加载常用配置
|
3. **ZFS 支持**: 完善 ZFS 文件系统处理
|
||||||
4. **日志持久化**: 将日志保存到文件
|
4. **LUKS 解锁**: 集成 LUKS 解锁功能
|
||||||
5. **多语言发行版支持**: 扩展 detect_distro_type
|
5. **图形化分区**: 添加分区可视化显示
|
||||||
6. **Btrfs/LVM 支持**: 当前仅支持标准分区
|
6. **多语言**: 国际化支持
|
||||||
|
7. **日志持久化**: 将日志保存到文件
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
- **数据风险**: 操作磁盘有数据丢失风险,建议备份
|
|
||||||
- **EFI 分区警告**: UEFI 模式下未选择 ESP 会弹出警告
|
|
||||||
- **临时挂载点**: 使用 `/mnt_grub_repair_<timestamp>` 格式
|
|
||||||
- **依赖命令**: 需要系统中存在 `lsblk`, `mount`, `umount`, `grub-install`, `grub-mkconfig`/`update-grub`
|
|
||||||
|
|||||||
Binary file not shown.
1115
backend.py
1115
backend.py
File diff suppressed because it is too large
Load Diff
561
frontend.py
561
frontend.py
@@ -1,124 +1,268 @@
|
|||||||
# frontend.py
|
# frontend.py
|
||||||
|
# 参考 Calamares 引导安装模块优化
|
||||||
|
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox
|
from tkinter import ttk, messagebox, scrolledtext
|
||||||
import threading
|
import threading
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import backend # 导入后端逻辑
|
import backend
|
||||||
|
|
||||||
|
|
||||||
class GrubRepairApp:
|
class GrubRepairApp:
|
||||||
def __init__(self, master):
|
def __init__(self, master):
|
||||||
self.master = master
|
self.master = master
|
||||||
master.title("Linux GRUB 引导修复工具")
|
master.title("Linux GRUB 引导修复工具 v2.0")
|
||||||
master.geometry("850x650") # 调整窗口大小
|
master.geometry("900x750")
|
||||||
master.resizable(True, True) # 允许窗口大小调整
|
master.resizable(True, True)
|
||||||
|
|
||||||
|
# 设置窗口最小大小
|
||||||
|
master.minsize(800, 600)
|
||||||
|
|
||||||
self.mount_point = None
|
self.mount_point = None
|
||||||
self.selected_root_partition_info = None # 存储字典信息
|
self.selected_root_partition_info = None
|
||||||
self.selected_boot_partition_info = None
|
self.selected_boot_partition_info = None
|
||||||
self.selected_efi_partition_info = None
|
self.selected_efi_partition_info = None
|
||||||
self.selected_target_disk_info = None
|
self.selected_target_disk_info = None
|
||||||
self.is_uefi_mode = False
|
self.is_uefi_mode = False
|
||||||
|
self.btrfs_subvolume = None
|
||||||
|
|
||||||
self.all_disks_data = {} # {display_name: {"name": "/dev/sda"}}
|
self.all_disks_data = {}
|
||||||
self.all_partitions_data = {} # {display_name: {"name": "/dev/sda1", ...}}
|
self.all_partitions_data = {}
|
||||||
self.all_efi_partitions_data = {} # {display_name: {"name": "/dev/sda1", ...}}
|
self.all_efi_partitions_data = {}
|
||||||
|
|
||||||
|
# 系统信息
|
||||||
|
self.system_info = {}
|
||||||
|
self._detect_system_info()
|
||||||
|
|
||||||
self.create_widgets()
|
self.create_widgets()
|
||||||
self.scan_partitions_gui()
|
self.scan_partitions_gui()
|
||||||
|
|
||||||
def create_widgets(self):
|
# 设置后端日志回调
|
||||||
# Frame for partition selection
|
backend.set_log_callback(self._backend_log_callback)
|
||||||
self.partition_frame = ttk.LabelFrame(self.master, text="1. 选择目标系统分区", padding="10")
|
|
||||||
self.partition_frame.pack(padx=10, pady=5, fill="x")
|
|
||||||
self.partition_frame.columnconfigure(1, weight=1) # 让 Combobox 填充宽度
|
|
||||||
|
|
||||||
ttk.Label(self.partition_frame, text="根分区 (/):").grid(row=0, column=0, padx=5, pady=2, sticky="w")
|
def _detect_system_info(self):
|
||||||
|
"""检测系统信息"""
|
||||||
|
self.system_info["efi_bits"] = backend.get_efi_word_size()
|
||||||
|
self.system_info["cpu_arch"] = __import__('platform').machine()
|
||||||
|
self.system_info["is_live"] = backend.is_live_environment()
|
||||||
|
sb_supported, sb_enabled = backend.check_secure_boot_status()
|
||||||
|
self.system_info["sb_supported"] = sb_supported
|
||||||
|
self.system_info["sb_enabled"] = sb_enabled
|
||||||
|
|
||||||
|
# 检测 Live 环境 UEFI 状态
|
||||||
|
self.system_info["live_is_uefi"] = os.path.exists("/sys/firmware/efi")
|
||||||
|
|
||||||
|
def create_widgets(self):
|
||||||
|
"""创建 GUI 组件"""
|
||||||
|
# 创建主滚动区域
|
||||||
|
main_canvas = tk.Canvas(self.master)
|
||||||
|
scrollbar = ttk.Scrollbar(self.master, orient="vertical", command=main_canvas.yview)
|
||||||
|
self.main_frame = ttk.Frame(main_canvas)
|
||||||
|
|
||||||
|
self.main_frame.bind(
|
||||||
|
"<Configure>",
|
||||||
|
lambda e: main_canvas.configure(scrollregion=main_canvas.bbox("all"))
|
||||||
|
)
|
||||||
|
|
||||||
|
main_canvas.create_window((0, 0), window=self.main_frame, anchor="nw")
|
||||||
|
main_canvas.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
main_canvas.pack(side="left", fill="both", expand=True)
|
||||||
|
scrollbar.pack(side="right", fill="y")
|
||||||
|
|
||||||
|
# 鼠标滚轮支持
|
||||||
|
def on_mousewheel(event):
|
||||||
|
main_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
|
||||||
|
main_canvas.bind_all("<MouseWheel>", on_mousewheel)
|
||||||
|
|
||||||
|
# ===== 系统信息区域 =====
|
||||||
|
self.info_frame = ttk.LabelFrame(self.main_frame, text="系统信息", padding="10")
|
||||||
|
self.info_frame.pack(padx=10, pady=5, fill="x")
|
||||||
|
|
||||||
|
info_text = f"""
|
||||||
|
架构: {self.system_info.get('cpu_arch', 'Unknown')} |
|
||||||
|
EFI位数: {self.system_info.get('efi_bits', 'Unknown')}位 |
|
||||||
|
Live环境: {'是' if self.system_info.get('is_live') else '否'} |
|
||||||
|
Secure Boot: {'已启用' if self.system_info.get('sb_enabled') else '已禁用/不支持'}
|
||||||
|
"""
|
||||||
|
ttk.Label(self.info_frame, text=info_text.strip()).pack(anchor="w")
|
||||||
|
|
||||||
|
# ===== 分区选择区域 =====
|
||||||
|
self.partition_frame = ttk.LabelFrame(self.main_frame, text="1. 选择目标系统分区", padding="10")
|
||||||
|
self.partition_frame.pack(padx=10, pady=5, fill="x")
|
||||||
|
self.partition_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
# 根分区
|
||||||
|
ttk.Label(self.partition_frame, text="根分区 (/) *:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||||
self.root_partition_combo = ttk.Combobox(self.partition_frame, state="readonly")
|
self.root_partition_combo = ttk.Combobox(self.partition_frame, state="readonly")
|
||||||
self.root_partition_combo.grid(row=0, column=1, padx=5, pady=2, sticky="ew")
|
self.root_partition_combo.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||||
self.root_partition_combo.bind("<<ComboboxSelected>>", self.on_root_partition_selected)
|
self.root_partition_combo.bind("<<ComboboxSelected>>", self.on_root_partition_selected)
|
||||||
|
|
||||||
ttk.Label(self.partition_frame, text="独立 /boot 分区 (可选):").grid(row=1, column=0, padx=5, pady=2, sticky="w")
|
# btrfs 子卷选择(初始隐藏)
|
||||||
|
self.btrfs_frame = ttk.Frame(self.partition_frame)
|
||||||
|
self.btrfs_frame.grid(row=1, column=0, columnspan=2, sticky="ew", padx=5, pady=2)
|
||||||
|
self.btrfs_frame.grid_remove() # 初始隐藏
|
||||||
|
|
||||||
|
ttk.Label(self.btrfs_frame, text="Btrfs 子卷:").pack(side="left", padx=(0, 5))
|
||||||
|
self.btrfs_subvolume_combo = ttk.Combobox(self.btrfs_frame, state="readonly", width=30)
|
||||||
|
self.btrfs_subvolume_combo.pack(side="left", fill="x", expand=True)
|
||||||
|
ttk.Button(self.btrfs_frame, text="刷新", command=self._refresh_btrfs_subvolumes).pack(side="left", padx=(5, 0))
|
||||||
|
|
||||||
|
# /boot 分区
|
||||||
|
ttk.Label(self.partition_frame, text="独立 /boot 分区:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
|
||||||
self.boot_partition_combo = ttk.Combobox(self.partition_frame, state="readonly")
|
self.boot_partition_combo = ttk.Combobox(self.partition_frame, state="readonly")
|
||||||
self.boot_partition_combo.grid(row=1, column=1, padx=5, pady=2, sticky="ew")
|
self.boot_partition_combo.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
ttk.Label(self.partition_frame, text="EFI 系统分区 (ESP, 仅UEFI):").grid(row=2, column=0, padx=5, pady=2, sticky="w")
|
# EFI 分区
|
||||||
|
ttk.Label(self.partition_frame, text="EFI 系统分区 (ESP):").grid(row=3, column=0, padx=5, pady=5, sticky="w")
|
||||||
self.efi_partition_combo = ttk.Combobox(self.partition_frame, state="readonly")
|
self.efi_partition_combo = ttk.Combobox(self.partition_frame, state="readonly")
|
||||||
self.efi_partition_combo.grid(row=2, column=1, padx=5, pady=2, sticky="ew")
|
self.efi_partition_combo.grid(row=3, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
# Frame for target disk selection
|
# ===== 目标磁盘选择 =====
|
||||||
self.disk_frame = ttk.LabelFrame(self.master, text="2. 选择GRUB安装目标磁盘", padding="10")
|
self.disk_frame = ttk.LabelFrame(self.main_frame, text="2. 选择GRUB安装目标", padding="10")
|
||||||
self.disk_frame.pack(padx=10, pady=5, fill="x")
|
self.disk_frame.pack(padx=10, pady=5, fill="x")
|
||||||
self.disk_frame.columnconfigure(1, weight=1)
|
self.disk_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
ttk.Label(self.disk_frame, text="目标磁盘 (例如 /dev/sda):").grid(row=0, column=0, padx=5, pady=2, sticky="w")
|
ttk.Label(self.disk_frame, text="目标磁盘 *:").grid(row=0, column=0, padx=5, pady=5, sticky="w")
|
||||||
self.target_disk_combo = ttk.Combobox(self.disk_frame, state="readonly")
|
self.target_disk_combo = ttk.Combobox(self.disk_frame, state="readonly")
|
||||||
self.target_disk_combo.grid(row=0, column=1, padx=5, pady=2, sticky="ew")
|
self.target_disk_combo.grid(row=0, column=1, padx=5, pady=5, sticky="ew")
|
||||||
|
|
||||||
self.uefi_mode_var = tk.BooleanVar(value=False)
|
# UEFI 模式选择
|
||||||
self.uefi_check = ttk.Checkbutton(self.disk_frame, text="目标系统使用UEFI启动 (Live环境检测为UEFI)", variable=self.uefi_mode_var)
|
self.uefi_mode_var = tk.BooleanVar(value=self.system_info.get("live_is_uefi", False))
|
||||||
self.uefi_check.grid(row=1, column=0, columnspan=2, padx=5, pady=2, sticky="w")
|
self.is_uefi_mode = self.uefi_mode_var.get()
|
||||||
|
|
||||||
# 尝试自动检测Live环境是否为UEFI
|
uefi_text = f"使用 UEFI 模式 (Live环境检测为{'UEFI' if self.system_info.get('live_is_uefi') else 'BIOS'})"
|
||||||
if os.path.exists("/sys/firmware/efi"):
|
self.uefi_check = ttk.Checkbutton(
|
||||||
self.uefi_mode_var.set(True)
|
self.disk_frame,
|
||||||
self.uefi_check.config(state="disabled") # 如果Live是UEFI,通常目标系统也是,且不建议更改
|
text=uefi_text,
|
||||||
self.is_uefi_mode = True # 直接设置
|
variable=self.uefi_mode_var,
|
||||||
else:
|
command=self.on_uefi_mode_changed
|
||||||
self.uefi_check.config(state="normal")
|
)
|
||||||
self.is_uefi_mode = False # 直接设置
|
self.uefi_check.grid(row=1, column=0, columnspan=2, padx=5, pady=5, sticky="w")
|
||||||
self.uefi_check.config(text="目标系统使用UEFI启动 (Live环境检测为BIOS)")
|
|
||||||
|
|
||||||
self.uefi_mode_var.trace_add("write", self.on_uefi_check_changed)
|
# 如果 Live 环境是 UEFI,提示用户
|
||||||
|
if self.system_info.get("live_is_uefi"):
|
||||||
|
ttk.Label(
|
||||||
|
self.disk_frame,
|
||||||
|
text="⚠️ Live 环境以 UEFI 模式启动,建议保持 UEFI 模式选中",
|
||||||
|
foreground="orange"
|
||||||
|
).grid(row=2, column=0, columnspan=2, padx=5, sticky="w")
|
||||||
|
|
||||||
|
# ===== 高级选项 =====
|
||||||
|
self.advanced_frame = ttk.LabelFrame(self.main_frame, text="3. 高级选项", padding="10")
|
||||||
|
self.advanced_frame.pack(padx=10, pady=5, fill="x")
|
||||||
|
|
||||||
# Start Repair Button
|
# 混合启动模式
|
||||||
self.start_button = ttk.Button(self.master, text="3. 开始修复 GRUB", command=self.start_repair_thread)
|
self.hybrid_mode_var = tk.BooleanVar(value=False)
|
||||||
self.start_button.pack(padx=10, pady=10, fill="x")
|
self.hybrid_check = ttk.Checkbutton(
|
||||||
|
self.advanced_frame,
|
||||||
|
text="混合启动模式(同时安装 BIOS 和 UEFI 引导)",
|
||||||
|
variable=self.hybrid_mode_var
|
||||||
|
)
|
||||||
|
self.hybrid_check.grid(row=0, column=0, columnspan=2, padx=5, pady=2, sticky="w")
|
||||||
|
|
||||||
# Progress and Log
|
# EFI Fallback
|
||||||
self.log_frame = ttk.LabelFrame(self.master, text="修复日志", padding="10")
|
self.fallback_var = tk.BooleanVar(value=True)
|
||||||
|
self.fallback_check = ttk.Checkbutton(
|
||||||
|
self.advanced_frame,
|
||||||
|
text="安装 EFI Fallback(推荐,提高兼容性)",
|
||||||
|
variable=self.fallback_var
|
||||||
|
)
|
||||||
|
self.fallback_check.grid(row=1, column=0, columnspan=2, padx=5, pady=2, sticky="w")
|
||||||
|
|
||||||
|
# EFI Bootloader ID
|
||||||
|
ttk.Label(self.advanced_frame, text="EFI 启动项名称:").grid(row=2, column=0, padx=5, pady=5, sticky="w")
|
||||||
|
self.bootloader_id_var = tk.StringVar(value="GRUB")
|
||||||
|
self.bootloader_id_entry = ttk.Entry(self.advanced_frame, textvariable=self.bootloader_id_var, width=20)
|
||||||
|
self.bootloader_id_entry.grid(row=2, column=1, padx=5, pady=5, sticky="w")
|
||||||
|
|
||||||
|
# 额外选项
|
||||||
|
self.force_install_var = tk.BooleanVar(value=True)
|
||||||
|
ttk.Checkbutton(
|
||||||
|
self.advanced_frame,
|
||||||
|
text="强制安装(--force)",
|
||||||
|
variable=self.force_install_var
|
||||||
|
).grid(row=3, column=0, columnspan=2, padx=5, pady=2, sticky="w")
|
||||||
|
|
||||||
|
# ===== 操作按钮 =====
|
||||||
|
self.button_frame = ttk.Frame(self.main_frame)
|
||||||
|
self.button_frame.pack(padx=10, pady=10, fill="x")
|
||||||
|
|
||||||
|
self.start_button = ttk.Button(
|
||||||
|
self.button_frame,
|
||||||
|
text="🔧 开始修复 GRUB",
|
||||||
|
command=self.start_repair_thread
|
||||||
|
)
|
||||||
|
self.start_button.pack(side="left", padx=(0, 5))
|
||||||
|
|
||||||
|
self.rescan_button = ttk.Button(
|
||||||
|
self.button_frame,
|
||||||
|
text="🔄 重新扫描分区",
|
||||||
|
command=self.scan_partitions_gui
|
||||||
|
)
|
||||||
|
self.rescan_button.pack(side="left", padx=5)
|
||||||
|
|
||||||
|
# ===== 日志区域 =====
|
||||||
|
self.log_frame = ttk.LabelFrame(self.main_frame, text="修复日志", padding="10")
|
||||||
self.log_frame.pack(padx=10, pady=5, fill="both", expand=True)
|
self.log_frame.pack(padx=10, pady=5, fill="both", expand=True)
|
||||||
|
|
||||||
self.log_text = tk.Text(self.log_frame, wrap="word", height=15, state="disabled", font=("Monospace", 10))
|
self.log_text = scrolledtext.ScrolledText(
|
||||||
|
self.log_frame,
|
||||||
|
wrap="word",
|
||||||
|
height=15,
|
||||||
|
state="disabled",
|
||||||
|
font=("Consolas", 10)
|
||||||
|
)
|
||||||
self.log_text.pack(padx=5, pady=5, fill="both", expand=True)
|
self.log_text.pack(padx=5, pady=5, fill="both", expand=True)
|
||||||
self.log_scrollbar = ttk.Scrollbar(self.log_text, command=self.log_text.yview)
|
|
||||||
self.log_scrollbar.pack(side="right", fill="y")
|
|
||||||
self.log_text.config(yscrollcommand=self.log_scrollbar.set)
|
|
||||||
|
|
||||||
def log_message(self, message, level="info"):
|
# 配置日志标签颜色
|
||||||
"""
|
self.log_text.tag_config("debug", foreground="gray")
|
||||||
线程安全的日志记录函数。
|
self.log_text.tag_config("info", foreground="black")
|
||||||
"""
|
self.log_text.tag_config("warning", foreground="orange")
|
||||||
|
self.log_text.tag_config("error", foreground="red")
|
||||||
|
self.log_text.tag_config("success", foreground="green")
|
||||||
|
self.log_text.tag_config("step", foreground="blue", font=("Consolas", 10, "bold"))
|
||||||
|
|
||||||
|
def _backend_log_callback(self, message: str, level):
|
||||||
|
"""后端日志回调函数"""
|
||||||
|
level_str = level.value if hasattr(level, 'value') else str(level).lower()
|
||||||
|
self._append_log(message, level_str)
|
||||||
|
|
||||||
|
def log_message(self, message: str, level: str = "info"):
|
||||||
|
"""线程安全的日志记录"""
|
||||||
self.master.after(0, self._append_log, message, level)
|
self.master.after(0, self._append_log, message, level)
|
||||||
|
|
||||||
def _append_log(self, message, level):
|
def _append_log(self, message: str, level: str = "info"):
|
||||||
|
"""添加日志到文本框"""
|
||||||
self.log_text.config(state="normal")
|
self.log_text.config(state="normal")
|
||||||
|
|
||||||
tag = "info"
|
# 添加时间戳
|
||||||
if level == "error":
|
import datetime
|
||||||
tag = "error"
|
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
|
||||||
self.log_text.tag_config("error", foreground="red")
|
log_line = f"[{timestamp}] {message}\n"
|
||||||
elif level == "warning":
|
|
||||||
tag = "warning"
|
|
||||||
self.log_text.tag_config("warning", foreground="orange")
|
|
||||||
elif level == "success":
|
|
||||||
tag = "success"
|
|
||||||
self.log_text.tag_config("success", foreground="green")
|
|
||||||
|
|
||||||
self.log_text.insert(tk.END, message + "\n", tag)
|
self.log_text.insert(tk.END, log_line, level)
|
||||||
self.log_text.see(tk.END)
|
self.log_text.see(tk.END)
|
||||||
self.log_text.config(state="disabled")
|
self.log_text.config(state="disabled")
|
||||||
|
|
||||||
def scan_partitions_gui(self):
|
def scan_partitions_gui(self):
|
||||||
|
"""扫描分区"""
|
||||||
self.log_message("正在扫描分区...", "info")
|
self.log_message("正在扫描分区...", "info")
|
||||||
|
self.rescan_button.config(state="disabled")
|
||||||
|
|
||||||
|
# 清空现有数据
|
||||||
|
self.root_partition_combo.set('')
|
||||||
|
self.boot_partition_combo.set('')
|
||||||
|
self.efi_partition_combo.set('')
|
||||||
|
self.target_disk_combo.set('')
|
||||||
|
|
||||||
success, disks, partitions, efi_partitions, err = backend.scan_partitions()
|
success, disks, partitions, efi_partitions, err = backend.scan_partitions()
|
||||||
|
|
||||||
if not success:
|
if not success:
|
||||||
self.log_message(f"分区扫描失败: {err}", "error")
|
self.log_message(f"分区扫描失败: {err}", "error")
|
||||||
messagebox.showerror("错误", f"无法扫描分区: {err}\n请确保您有sudo权限并已安装lsblk。")
|
messagebox.showerror("错误", f"无法扫描分区: {err}\n请确保您有sudo权限并已安装lsblk。")
|
||||||
|
self.rescan_button.config(state="normal")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 清空旧数据
|
# 清空旧数据
|
||||||
@@ -126,31 +270,46 @@ class GrubRepairApp:
|
|||||||
self.all_partitions_data = {}
|
self.all_partitions_data = {}
|
||||||
self.all_efi_partitions_data = {}
|
self.all_efi_partitions_data = {}
|
||||||
|
|
||||||
|
# 更新磁盘列表
|
||||||
disk_display_names = []
|
disk_display_names = []
|
||||||
for d in disks:
|
for d in disks:
|
||||||
display_name = d["name"]
|
display_name = f"{d['name']} ({d.get('size', 'unknown')})"
|
||||||
self.all_disks_data[display_name] = d
|
self.all_disks_data[display_name] = d
|
||||||
disk_display_names.append(display_name)
|
disk_display_names.append(display_name)
|
||||||
self.target_disk_combo['values'] = disk_display_names
|
self.target_disk_combo['values'] = disk_display_names
|
||||||
|
|
||||||
|
# 更新分区列表
|
||||||
partition_display_names = []
|
partition_display_names = []
|
||||||
for p in partitions:
|
for p in partitions:
|
||||||
display_name = f"{p['name']} ({p['fstype']} - {p['size']})"
|
extra_info = []
|
||||||
|
if p.get('is_lvm'):
|
||||||
|
extra_info.append("LVM")
|
||||||
|
if p.get('is_btrfs'):
|
||||||
|
extra_info.append("Btrfs")
|
||||||
|
if p.get('is_luks'):
|
||||||
|
extra_info.append("LUKS")
|
||||||
|
|
||||||
|
extra_str = f" [{', '.join(extra_info)}]" if extra_info else ""
|
||||||
|
display_name = f"{p['name']} ({p['fstype']} - {p['size']}){extra_str}"
|
||||||
self.all_partitions_data[display_name] = p
|
self.all_partitions_data[display_name] = p
|
||||||
partition_display_names.append(display_name)
|
partition_display_names.append(display_name)
|
||||||
self.root_partition_combo['values'] = partition_display_names
|
|
||||||
self.boot_partition_combo['values'] = [""] + partition_display_names # 允许不选择独立/boot
|
|
||||||
|
|
||||||
|
self.root_partition_combo['values'] = partition_display_names
|
||||||
|
self.boot_partition_combo['values'] = [""] + partition_display_names
|
||||||
|
|
||||||
|
# 更新 EFI 分区列表
|
||||||
efi_partition_display_names = []
|
efi_partition_display_names = []
|
||||||
for p in efi_partitions:
|
for p in efi_partitions:
|
||||||
display_name = f"{p['name']} ({p['fstype']} - {p['size']})"
|
display_name = f"{p['name']} ({p['fstype']} - {p['size']})"
|
||||||
self.all_efi_partitions_data[display_name] = p
|
self.all_efi_partitions_data[display_name] = p
|
||||||
efi_partition_display_names.append(display_name)
|
efi_partition_display_names.append(display_name)
|
||||||
self.efi_partition_combo['values'] = [""] + efi_partition_display_names # 允许不选择EFI分区
|
self.efi_partition_combo['values'] = [""] + efi_partition_display_names
|
||||||
|
|
||||||
self.log_message("分区扫描完成。请选择目标系统分区。", "info")
|
self.log_message(f"扫描完成: 发现 {len(disks)} 个磁盘, {len(partitions)} 个分区", "success")
|
||||||
|
self.rescan_button.config(state="normal")
|
||||||
|
|
||||||
def on_root_partition_selected(self, event):
|
def on_root_partition_selected(self, event=None):
|
||||||
|
"""根分区选择事件"""
|
||||||
selected_display = self.root_partition_combo.get()
|
selected_display = self.root_partition_combo.get()
|
||||||
self.selected_root_partition_info = self.all_partitions_data.get(selected_display)
|
self.selected_root_partition_info = self.all_partitions_data.get(selected_display)
|
||||||
|
|
||||||
@@ -158,152 +317,260 @@ class GrubRepairApp:
|
|||||||
root_part_name = self.selected_root_partition_info['name']
|
root_part_name = self.selected_root_partition_info['name']
|
||||||
self.log_message(f"已选择根分区: {root_part_name}", "info")
|
self.log_message(f"已选择根分区: {root_part_name}", "info")
|
||||||
|
|
||||||
# 检查是否是 LVM 逻辑卷
|
# 检测是否为 btrfs
|
||||||
|
if self.selected_root_partition_info.get('fstype') == 'btrfs':
|
||||||
|
self.log_message("检测到 Btrfs 文件系统", "info")
|
||||||
|
self.btrfs_frame.grid() # 显示子卷选择
|
||||||
|
self._refresh_btrfs_subvolumes()
|
||||||
|
else:
|
||||||
|
self.btrfs_frame.grid_remove() # 隐藏子卷选择
|
||||||
|
self.btrfs_subvolume = None
|
||||||
|
|
||||||
|
# 自动选择目标磁盘
|
||||||
is_lvm = self.selected_root_partition_info.get('is_lvm', False)
|
is_lvm = self.selected_root_partition_info.get('is_lvm', False)
|
||||||
|
|
||||||
if is_lvm:
|
if is_lvm:
|
||||||
# LVM 逻辑卷无法直接从路径推断父磁盘,提示用户手动选择
|
|
||||||
self.target_disk_combo.set("")
|
self.target_disk_combo.set("")
|
||||||
self.selected_target_disk_info = None
|
self.selected_target_disk_info = None
|
||||||
self.log_message("检测到 LVM 逻辑卷,请手动选择 GRUB 安装的目标磁盘。", "warning")
|
self.log_message("检测到 LVM 逻辑卷,请手动选择目标磁盘", "warning")
|
||||||
else:
|
else:
|
||||||
# 尝试根据根分区所在的磁盘自动选择目标磁盘
|
# 根据分区路径推断磁盘
|
||||||
# /dev/sda1 -> /dev/sda
|
disk_name_from_root = self._extract_disk_from_partition(root_part_name)
|
||||||
# /dev/nvme0n1p1 -> /dev/nvme0n1
|
|
||||||
# /dev/mmcblk0p1 -> /dev/mmcblk0
|
# 在磁盘列表中查找匹配项
|
||||||
disk_name_from_root = re.sub(r'(\d+)?p?\d+$', '', root_part_name)
|
for disk_display in self.target_disk_combo['values']:
|
||||||
if disk_name_from_root in self.target_disk_combo['values']:
|
if disk_name_from_root in disk_display:
|
||||||
self.target_disk_combo.set(disk_name_from_root)
|
self.target_disk_combo.set(disk_display)
|
||||||
self.selected_target_disk_info = self.all_disks_data.get(disk_name_from_root)
|
self.selected_target_disk_info = self.all_disks_data.get(disk_display)
|
||||||
self.log_message(f"自动选择目标磁盘: {self.selected_target_disk_info['name']}", "info")
|
self.log_message(f"自动选择目标磁盘: {self.selected_target_disk_info['name']}", "info")
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
self.target_disk_combo.set("")
|
self.target_disk_combo.set("")
|
||||||
self.selected_target_disk_info = None
|
self.selected_target_disk_info = None
|
||||||
self.log_message("无法自动选择目标磁盘,请手动选择。", "warning")
|
|
||||||
else:
|
else:
|
||||||
self.selected_root_partition_info = None
|
self.selected_root_partition_info = None
|
||||||
self.target_disk_combo.set("")
|
self.target_disk_combo.set("")
|
||||||
self.selected_target_disk_info = None
|
self.selected_target_disk_info = None
|
||||||
|
self.btrfs_frame.grid_remove()
|
||||||
|
|
||||||
def on_uefi_check_changed(self, *args):
|
def _extract_disk_from_partition(self, partition: str) -> str:
|
||||||
|
"""从分区路径提取磁盘路径"""
|
||||||
|
# /dev/sda1 -> /dev/sda
|
||||||
|
# /dev/nvme0n1p1 -> /dev/nvme0n1
|
||||||
|
# /dev/mmcblk0p1 -> /dev/mmcblk0
|
||||||
|
# /dev/mapper/vg-lv -> 保持原样(LVM)
|
||||||
|
|
||||||
|
if partition.startswith("/dev/mapper/"):
|
||||||
|
return partition # LVM 无法自动推断
|
||||||
|
|
||||||
|
# 移除分区号
|
||||||
|
import re
|
||||||
|
patterns = [
|
||||||
|
r'^(/dev/nvme\d+n\d+)p\d+$', # NVMe
|
||||||
|
r'^(/dev/mmcblk\d+)p\d+$', # eMMC
|
||||||
|
r'^(/dev/[a-zA-Z]+)\d+$', # SATA/SCSI
|
||||||
|
]
|
||||||
|
|
||||||
|
for pattern in patterns:
|
||||||
|
match = re.match(pattern, partition)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
|
||||||
|
return partition
|
||||||
|
|
||||||
|
def _refresh_btrfs_subvolumes(self):
|
||||||
|
"""刷新 btrfs 子卷列表"""
|
||||||
|
if not self.selected_root_partition_info:
|
||||||
|
return
|
||||||
|
|
||||||
|
partition = self.selected_root_partition_info['name']
|
||||||
|
subvolumes = backend.get_btrfs_root_subvolume(partition)
|
||||||
|
|
||||||
|
# 尝试获取所有子卷
|
||||||
|
try:
|
||||||
|
import subprocess
|
||||||
|
result = subprocess.run(
|
||||||
|
["sudo", "btrfs", "subvolume", "list", partition],
|
||||||
|
capture_output=True, text=True, timeout=10
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
all_subvols = ["(默认 - 不指定)"]
|
||||||
|
for line in result.stdout.strip().split('\n'):
|
||||||
|
match = re.search(r'path\s+(\S+)', line)
|
||||||
|
if match:
|
||||||
|
all_subvols.append(match.group(1))
|
||||||
|
self.btrfs_subvolume_combo['values'] = all_subvols
|
||||||
|
if len(all_subvols) > 1:
|
||||||
|
self.btrfs_subvolume_combo.set(all_subvols[0])
|
||||||
|
except Exception as e:
|
||||||
|
self.log_message(f"获取 btrfs 子卷失败: {e}", "warning")
|
||||||
|
|
||||||
|
def on_uefi_mode_changed(self):
|
||||||
|
"""UEFI 模式切换事件"""
|
||||||
self.is_uefi_mode = self.uefi_mode_var.get()
|
self.is_uefi_mode = self.uefi_mode_var.get()
|
||||||
self.log_message(f"UEFI模式选择: {'启用' if self.is_uefi_mode else '禁用'}", "info")
|
self.log_message(f"UEFI 模式: {'启用' if self.is_uefi_mode else '禁用'}", "info")
|
||||||
|
|
||||||
|
# 如果启用 UEFI,建议必须选择 EFI 分区
|
||||||
|
if self.is_uefi_mode:
|
||||||
|
self.log_message("提示: UEFI 模式建议指定 EFI 系统分区", "info")
|
||||||
|
|
||||||
def start_repair_thread(self):
|
def start_repair_thread(self):
|
||||||
# 禁用按钮防止重复点击
|
"""启动修复线程"""
|
||||||
|
# 获取用户选择
|
||||||
|
self.selected_root_partition_info = self.all_partitions_data.get(self.root_partition_combo.get())
|
||||||
|
self.selected_boot_partition_info = self.all_partitions_data.get(self.boot_partition_combo.get()) or None
|
||||||
|
self.selected_efi_partition_info = self.all_efi_partitions_data.get(self.efi_partition_combo.get()) or None
|
||||||
|
self.selected_target_disk_info = self.all_disks_data.get(self.target_disk_combo.get())
|
||||||
|
self.is_uefi_mode = self.uefi_mode_var.get()
|
||||||
|
|
||||||
|
# 获取 btrfs 子卷
|
||||||
|
btrfs_subvol = self.btrfs_subvolume_combo.get() if hasattr(self, 'btrfs_subvolume_combo') else ""
|
||||||
|
if btrfs_subvol and btrfs_subvol != "(默认 - 不指定)":
|
||||||
|
self.btrfs_subvolume = btrfs_subvol
|
||||||
|
else:
|
||||||
|
self.btrfs_subvolume = None
|
||||||
|
|
||||||
|
# 验证输入
|
||||||
|
if not self.selected_root_partition_info:
|
||||||
|
messagebox.showerror("错误", "请选择目标系统的根分区!")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not self.selected_target_disk_info and not self.is_uefi_mode:
|
||||||
|
messagebox.showerror("错误", "BIOS 模式下必须选择 GRUB 安装的目标磁盘!")
|
||||||
|
return
|
||||||
|
|
||||||
|
# UEFI 模式下检查 EFI 分区
|
||||||
|
if self.is_uefi_mode and not self.selected_efi_partition_info:
|
||||||
|
response = messagebox.askyesno(
|
||||||
|
"警告",
|
||||||
|
"您选择了 UEFI 模式,但未选择 EFI 系统分区 (ESP)。\n"
|
||||||
|
"这可能导致修复失败。是否继续?"
|
||||||
|
)
|
||||||
|
if not response:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 禁用按钮
|
||||||
self.start_button.config(state="disabled")
|
self.start_button.config(state="disabled")
|
||||||
|
self.rescan_button.config(state="disabled")
|
||||||
|
|
||||||
# 清空日志
|
# 清空日志
|
||||||
self.log_text.config(state="normal")
|
self.log_text.config(state="normal")
|
||||||
self.log_text.delete(1.0, tk.END)
|
self.log_text.delete(1.0, tk.END)
|
||||||
self.log_text.config(state="disabled")
|
self.log_text.config(state="disabled")
|
||||||
self.log_message("--- GRUB 修复过程开始 ---", "info")
|
|
||||||
|
|
||||||
# 获取用户选择
|
self.log_message("=" * 60, "step")
|
||||||
self.selected_root_partition_info = self.all_partitions_data.get(self.root_partition_combo.get())
|
self.log_message("GRUB 引导修复过程开始", "step")
|
||||||
self.selected_boot_partition_info = self.all_partitions_data.get(self.boot_partition_combo.get())
|
self.log_message("=" * 60, "step")
|
||||||
self.selected_efi_partition_info = self.all_efi_partitions_data.get(self.efi_partition_combo.get())
|
|
||||||
self.selected_target_disk_info = self.all_disks_data.get(self.target_disk_combo.get())
|
|
||||||
self.is_uefi_mode = self.uefi_mode_var.get()
|
|
||||||
|
|
||||||
if not self.selected_root_partition_info:
|
# 启动修复线程
|
||||||
messagebox.showerror("错误", "请选择目标系统的根分区!")
|
repair_thread = threading.Thread(target=self._run_repair_process, daemon=True)
|
||||||
self.log_message("未选择根分区,修复中止。", "error")
|
|
||||||
self.start_button.config(state="normal")
|
|
||||||
return
|
|
||||||
if not self.selected_target_disk_info:
|
|
||||||
messagebox.showerror("错误", "请选择GRUB要安装到的目标磁盘!")
|
|
||||||
self.log_message("未选择目标磁盘,修复中止。", "error")
|
|
||||||
self.start_button.config(state="normal")
|
|
||||||
return
|
|
||||||
|
|
||||||
# 再次确认UEFI模式下是否选择了EFI分区
|
|
||||||
if self.is_uefi_mode and not self.selected_efi_partition_info:
|
|
||||||
response = messagebox.askyesno("警告", "您选择了UEFI模式,但未选择EFI系统分区 (ESP)。这可能导致修复失败。是否继续?")
|
|
||||||
if not response:
|
|
||||||
self.log_message("用户取消,未选择EFI分区。", "warning")
|
|
||||||
self.start_button.config(state="normal")
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
# 启动新线程进行修复,避免UI卡死
|
|
||||||
repair_thread = threading.Thread(target=self._run_repair_process)
|
|
||||||
repair_thread.start()
|
repair_thread.start()
|
||||||
|
|
||||||
def _run_repair_process(self):
|
def _run_repair_process(self):
|
||||||
|
"""执行修复过程"""
|
||||||
try:
|
try:
|
||||||
root_part_path = self.selected_root_partition_info['name']
|
root_part_path = self.selected_root_partition_info['name']
|
||||||
boot_part_path = self.selected_boot_partition_info['name'] if self.selected_boot_partition_info else None
|
boot_part_path = self.selected_boot_partition_info['name'] if self.selected_boot_partition_info else None
|
||||||
efi_part_path = self.selected_efi_partition_info['name'] if self.selected_efi_partition_info else None
|
efi_part_path = self.selected_efi_partition_info['name'] if self.selected_efi_partition_info else None
|
||||||
target_disk_path = self.selected_target_disk_info['name']
|
target_disk_path = self.selected_target_disk_info['name'] if self.selected_target_disk_info else None
|
||||||
|
|
||||||
self.log_message(f"选择的根分区: {root_part_path}", "info")
|
self.log_message(f"根分区: {root_part_path}", "info")
|
||||||
self.log_message(f"选择的独立 /boot 分区: {boot_part_path if boot_part_path else '无'}", "info")
|
self.log_message(f"/boot 分区: {boot_part_path or '无'}", "info")
|
||||||
self.log_message(f"选择的EFI分区: {efi_part_path if efi_part_path else '无'}", "info")
|
self.log_message(f"EFI 分区: {efi_part_path or '无'}", "info")
|
||||||
self.log_message(f"GRUB安装目标磁盘: {target_disk_path}", "info")
|
self.log_message(f"目标磁盘: {target_disk_path or 'N/A (UEFI only)'}", "info")
|
||||||
self.log_message(f"UEFI模式: {'启用' if self.is_uefi_mode else '禁用'}", "info")
|
self.log_message(f"UEFI 模式: {'是' if self.is_uefi_mode else '否'}", "info")
|
||||||
|
self.log_message(f"Btrfs 子卷: {self.btrfs_subvolume or '无'}", "info")
|
||||||
|
|
||||||
# 1. 挂载目标系统
|
# 1. 挂载目标系统
|
||||||
self.log_message("正在挂载目标系统分区...", "info")
|
self.log_message("正在挂载目标系统分区...", "step")
|
||||||
mount_ok, self.mount_point, mount_err = backend.mount_target_system(
|
mount_ok, self.mount_point, mount_err = backend.mount_target_system(
|
||||||
root_part_path,
|
root_part_path,
|
||||||
boot_part_path,
|
boot_part_path,
|
||||||
efi_part_path if self.is_uefi_mode else None # 只有UEFI模式才挂载EFI分区
|
efi_part_path if self.is_uefi_mode else None,
|
||||||
|
self.btrfs_subvolume
|
||||||
)
|
)
|
||||||
if not mount_ok:
|
if not mount_ok:
|
||||||
self.log_message(f"挂载失败,修复中止: {mount_err}", "error")
|
self.log_message(f"挂载失败: {mount_err}", "error")
|
||||||
self.master.after(0, lambda: messagebox.showerror("修复失败", f"无法挂载目标系统分区。请检查分区选择和权限。\n错误: {mount_err}"))
|
self.master.after(0, lambda: messagebox.showerror("修复失败", f"无法挂载目标系统:\n{mount_err}"))
|
||||||
return
|
return
|
||||||
|
|
||||||
# 2. 尝试识别发行版类型
|
# 2. 检测发行版
|
||||||
self.log_message("正在检测目标系统发行版...", "info")
|
self.log_message("正在检测目标系统发行版...", "step")
|
||||||
distro_type = backend.detect_distro_type(self.mount_point)
|
distro_type = backend.detect_distro_type(self.mount_point)
|
||||||
self.log_message(f"检测到目标系统发行版: {distro_type}", "info")
|
self.log_message(f"检测到发行版: {distro_type}", "info")
|
||||||
|
|
||||||
|
# 3. Chroot 并修复 GRUB
|
||||||
|
self.log_message("正在进入 Chroot 环境并修复 GRUB...", "step")
|
||||||
|
|
||||||
|
install_hybrid = self.hybrid_mode_var.get()
|
||||||
|
use_fallback = self.fallback_var.get()
|
||||||
|
bootloader_id = self.bootloader_id_var.get() or "GRUB"
|
||||||
|
|
||||||
|
self.log_message(f"混合模式: {'是' if install_hybrid else '否'}", "info")
|
||||||
|
self.log_message(f"EFI Fallback: {'是' if use_fallback else '否'}", "info")
|
||||||
|
self.log_message(f"启动项名称: {bootloader_id}", "info")
|
||||||
|
|
||||||
# 3. Chroot并修复GRUB
|
|
||||||
self.log_message("正在进入Chroot环境并修复GRUB...", "info")
|
|
||||||
repair_ok, repair_err = backend.chroot_and_repair_grub(
|
repair_ok, repair_err = backend.chroot_and_repair_grub(
|
||||||
self.mount_point,
|
self.mount_point,
|
||||||
target_disk_path,
|
target_disk_path or "", # UEFI 模式下可能为空
|
||||||
self.is_uefi_mode,
|
self.is_uefi_mode,
|
||||||
distro_type
|
distro_type,
|
||||||
|
install_hybrid=install_hybrid,
|
||||||
|
use_fallback=use_fallback,
|
||||||
|
efi_bootloader_id=bootloader_id
|
||||||
)
|
)
|
||||||
if not repair_ok:
|
|
||||||
self.log_message(f"GRUB修复失败: {repair_err}", "error")
|
|
||||||
self.master.after(0, lambda: messagebox.showerror("修复失败", f"GRUB修复过程中发生错误。\n错误: {repair_err}"))
|
|
||||||
# 即使失败,也要尝试卸载
|
|
||||||
else:
|
|
||||||
self.log_message("GRUB修复命令执行成功!", "success")
|
|
||||||
|
|
||||||
|
if not repair_ok:
|
||||||
|
self.log_message(f"GRUB 修复失败: {repair_err}", "error")
|
||||||
|
self.master.after(0, lambda: messagebox.showerror("修复失败", f"GRUB 修复失败:\n{repair_err}"))
|
||||||
|
else:
|
||||||
|
self.log_message("GRUB 修复命令执行成功!", "success")
|
||||||
|
|
||||||
# 4. 卸载分区
|
# 4. 卸载分区
|
||||||
self.log_message("正在卸载目标系统分区...", "info")
|
self.log_message("正在卸载目标系统分区...", "step")
|
||||||
unmount_ok, unmount_err = backend.unmount_target_system(self.mount_point)
|
unmount_ok, unmount_err = backend.unmount_target_system(self.mount_point)
|
||||||
if not unmount_ok:
|
if not unmount_ok:
|
||||||
self.log_message(f"卸载分区失败: {unmount_err}", "error")
|
self.log_message(f"卸载分区失败: {unmount_err}", "warning")
|
||||||
self.master.after(0, lambda: messagebox.showwarning("卸载警告", f"部分分区可能未能正确卸载。请手动检查并卸载。\n错误: {unmount_err}"))
|
|
||||||
else:
|
else:
|
||||||
self.log_message("所有分区已成功卸载。", "success")
|
self.log_message("所有分区已成功卸载", "success")
|
||||||
|
|
||||||
self.log_message("--- GRUB 修复过程结束 ---", "info")
|
self.log_message("=" * 60, "step")
|
||||||
|
self.log_message("GRUB 修复过程结束", "step")
|
||||||
|
self.log_message("=" * 60, "step")
|
||||||
|
|
||||||
# 最终结果提示
|
# 最终结果
|
||||||
if repair_ok and unmount_ok:
|
if repair_ok and unmount_ok:
|
||||||
self.master.after(0, lambda: messagebox.showinfo("修复成功", "GRUB引导修复已完成!请重启系统以验证。"))
|
self.master.after(0, lambda: messagebox.showinfo(
|
||||||
|
"修复成功",
|
||||||
|
"GRUB 引导修复已完成!\n\n"
|
||||||
|
"请重启系统以验证修复结果。\n"
|
||||||
|
"如果仍无法启动,请检查BIOS设置中的启动顺序。"
|
||||||
|
))
|
||||||
elif repair_ok and not unmount_ok:
|
elif repair_ok and not unmount_ok:
|
||||||
self.master.after(0, lambda: messagebox.showwarning("修复完成,但有警告", "GRUB引导修复已完成,但部分分区卸载失败。请重启系统以验证。"))
|
self.master.after(0, lambda: messagebox.showwarning(
|
||||||
|
"修复完成,但有警告",
|
||||||
|
"GRUB 引导修复已完成,但部分分区卸载失败。\n"
|
||||||
|
"建议重启系统,这会自动清理挂载点。"
|
||||||
|
))
|
||||||
else:
|
else:
|
||||||
self.master.after(0, lambda: messagebox.showerror("修复失败", "GRUB引导修复过程中发生错误。请检查日志并尝试手动修复。"))
|
self.master.after(0, lambda: messagebox.showerror(
|
||||||
|
"修复失败",
|
||||||
|
"GRUB 引导修复过程中发生错误。\n"
|
||||||
|
"请检查日志并尝试手动修复。"
|
||||||
|
))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.log_message(f"发生未预期的错误: {str(e)}", "error")
|
self.log_message(f"发生未预期的错误: {str(e)}", "error")
|
||||||
|
import traceback
|
||||||
|
self.log_message(traceback.format_exc(), "debug")
|
||||||
self.master.after(0, lambda err=str(e): messagebox.showerror("错误", f"修复过程中发生错误:\n{err}"))
|
self.master.after(0, lambda err=str(e): messagebox.showerror("错误", f"修复过程中发生错误:\n{err}"))
|
||||||
finally:
|
finally:
|
||||||
# 确保按钮状态被恢复
|
# 恢复按钮状态
|
||||||
self.master.after(0, lambda: self.start_button.config(state="normal"))
|
self.master.after(0, lambda: self.start_button.config(state="normal"))
|
||||||
|
self.master.after(0, lambda: self.rescan_button.config(state="normal"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
||||||
app = GrubRepairApp(root)
|
app = GrubRepairApp(root)
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
|
|||||||
967
tip/main.py
Normal file
967
tip/main.py
Normal file
@@ -0,0 +1,967 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# === This file is part of Calamares - <https://calamares.io> ===
|
||||||
|
#
|
||||||
|
# SPDX-FileCopyrightText: 2014 Aurélien Gâteau <agateau@kde.org>
|
||||||
|
# SPDX-FileCopyrightText: 2014 Anke Boersma <demm@kaosx.us>
|
||||||
|
# SPDX-FileCopyrightText: 2014 Daniel Hillenbrand <codeworkx@bbqlinux.org>
|
||||||
|
# SPDX-FileCopyrightText: 2014 Benjamin Vaudour <benjamin.vaudour@yahoo.fr>
|
||||||
|
# SPDX-FileCopyrightText: 2014-2019 Kevin Kofler <kevin.kofler@chello.at>
|
||||||
|
# SPDX-FileCopyrightText: 2015-2018 Philip Mueller <philm@manjaro.org>
|
||||||
|
# SPDX-FileCopyrightText: 2016-2017 Teo Mrnjavac <teo@kde.org>
|
||||||
|
# SPDX-FileCopyrightText: 2017 Alf Gaida <agaida@siduction.org>
|
||||||
|
# SPDX-FileCopyrightText: 2017-2019 Adriaan de Groot <groot@kde.org>
|
||||||
|
# SPDX-FileCopyrightText: 2017 Gabriel Craciunescu <crazy@frugalware.org>
|
||||||
|
# SPDX-FileCopyrightText: 2017 Ben Green <Bezzy1999@hotmail.com>
|
||||||
|
# SPDX-FileCopyrightText: 2021 Neal Gompa <ngompa13@gmail.com>
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
#
|
||||||
|
# Calamares is Free Software: see the License-Identifier above.
|
||||||
|
#
|
||||||
|
|
||||||
|
import fileinput
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import libcalamares
|
||||||
|
|
||||||
|
from libcalamares.utils import check_target_env_call, check_target_env_output
|
||||||
|
|
||||||
|
import gettext
|
||||||
|
|
||||||
|
_ = gettext.translation("calamares-python",
|
||||||
|
localedir=libcalamares.utils.gettext_path(),
|
||||||
|
languages=libcalamares.utils.gettext_languages(),
|
||||||
|
fallback=True).gettext
|
||||||
|
|
||||||
|
# This is the sanitizer used all over to tidy up filenames
|
||||||
|
# to make identifiers (or to clean up names to make filenames).
|
||||||
|
file_name_sanitizer = str.maketrans(" /()", "_-__")
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_name():
|
||||||
|
return _("Install bootloader.")
|
||||||
|
|
||||||
|
|
||||||
|
def get_uuid():
|
||||||
|
"""
|
||||||
|
Checks and passes 'uuid' to other routine.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
partitions = libcalamares.globalstorage.value("partitions")
|
||||||
|
|
||||||
|
for partition in partitions:
|
||||||
|
if partition["mountPoint"] == "/":
|
||||||
|
libcalamares.utils.debug("Root partition uuid: \"{!s}\"".format(partition["uuid"]))
|
||||||
|
return partition["uuid"]
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_kernel_line(kernel_type):
|
||||||
|
"""
|
||||||
|
Passes 'kernel_line' to other routine based on configuration file.
|
||||||
|
|
||||||
|
:param kernel_type:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if kernel_type == "fallback":
|
||||||
|
if "fallbackKernelLine" in libcalamares.job.configuration:
|
||||||
|
return libcalamares.job.configuration["fallbackKernelLine"]
|
||||||
|
else:
|
||||||
|
return " (fallback)"
|
||||||
|
else:
|
||||||
|
if "kernelLine" in libcalamares.job.configuration:
|
||||||
|
return libcalamares.job.configuration["kernelLine"]
|
||||||
|
else:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_zfs_root():
|
||||||
|
"""
|
||||||
|
Looks in global storage to find the zfs root
|
||||||
|
|
||||||
|
:return: A string containing the path to the zfs root or None if it is not found
|
||||||
|
"""
|
||||||
|
|
||||||
|
zfs = libcalamares.globalstorage.value("zfsDatasets")
|
||||||
|
|
||||||
|
if not zfs:
|
||||||
|
libcalamares.utils.warning("Failed to locate zfs dataset list")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find the root dataset
|
||||||
|
for dataset in zfs:
|
||||||
|
try:
|
||||||
|
if dataset["mountpoint"] == "/":
|
||||||
|
return dataset["zpool"] + "/" + dataset["dsName"]
|
||||||
|
except KeyError:
|
||||||
|
# This should be impossible
|
||||||
|
libcalamares.utils.warning("Internal error handling zfs dataset")
|
||||||
|
raise
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_btrfs_root(partition):
|
||||||
|
""" Returns True if the partition object refers to a btrfs root filesystem
|
||||||
|
|
||||||
|
:param partition: A partition map from global storage
|
||||||
|
:return: True if btrfs and root, False otherwise
|
||||||
|
"""
|
||||||
|
return partition["mountPoint"] == "/" and partition["fs"] == "btrfs"
|
||||||
|
|
||||||
|
|
||||||
|
def is_zfs_root(partition):
|
||||||
|
""" Returns True if the partition object refers to a zfs root filesystem
|
||||||
|
|
||||||
|
:param partition: A partition map from global storage
|
||||||
|
:return: True if zfs and root, False otherwise
|
||||||
|
"""
|
||||||
|
return partition["mountPoint"] == "/" and partition["fs"] == "zfs"
|
||||||
|
|
||||||
|
|
||||||
|
def have_program_in_target(program : str):
|
||||||
|
"""Returns @c True if @p program is in path in the target"""
|
||||||
|
return libcalamares.utils.target_env_call(["/usr/bin/which", program]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def get_kernel_params(uuid):
|
||||||
|
# Configured kernel parameters (default "quiet"), if plymouth installed, add splash
|
||||||
|
# screen parameter and then "rw".
|
||||||
|
kernel_params = libcalamares.job.configuration.get("kernelParams", ["quiet"])
|
||||||
|
if have_program_in_target("plymouth"):
|
||||||
|
kernel_params.append("splash")
|
||||||
|
kernel_params.append("rw")
|
||||||
|
|
||||||
|
use_systemd_naming = have_program_in_target("dracut") or (libcalamares.utils.target_env_call(["/usr/bin/grep", "-q", "^HOOKS.*systemd", "/etc/mkinitcpio.conf"]) == 0)
|
||||||
|
|
||||||
|
partitions = libcalamares.globalstorage.value("partitions")
|
||||||
|
|
||||||
|
cryptdevice_params = []
|
||||||
|
swap_uuid = ""
|
||||||
|
swap_outer_mappername = None
|
||||||
|
swap_outer_uuid = None
|
||||||
|
|
||||||
|
# Take over swap settings:
|
||||||
|
# - unencrypted swap partition sets swap_uuid
|
||||||
|
# - encrypted root sets cryptdevice_params
|
||||||
|
for partition in partitions:
|
||||||
|
if partition["fs"] == "linuxswap" and not partition.get("claimed", None):
|
||||||
|
# Skip foreign swap
|
||||||
|
continue
|
||||||
|
has_luks = "luksMapperName" in partition
|
||||||
|
if partition["fs"] == "linuxswap" and not has_luks:
|
||||||
|
swap_uuid = partition["uuid"]
|
||||||
|
|
||||||
|
if partition["fs"] == "linuxswap" and has_luks:
|
||||||
|
swap_outer_mappername = partition["luksMapperName"]
|
||||||
|
swap_outer_uuid = partition["luksUuid"]
|
||||||
|
|
||||||
|
if partition["mountPoint"] == "/" and has_luks:
|
||||||
|
if use_systemd_naming:
|
||||||
|
cryptdevice_params = [f"rd.luks.uuid={partition['luksUuid']}"]
|
||||||
|
else:
|
||||||
|
cryptdevice_params = [f"cryptdevice=UUID={partition['luksUuid']}:{partition['luksMapperName']}"]
|
||||||
|
cryptdevice_params.append(f"root=/dev/mapper/{partition['luksMapperName']}")
|
||||||
|
|
||||||
|
# btrfs and zfs handling
|
||||||
|
for partition in partitions:
|
||||||
|
# If a btrfs root subvolume wasn't set, it means the root is directly on the partition
|
||||||
|
# and this option isn't needed
|
||||||
|
if is_btrfs_root(partition):
|
||||||
|
btrfs_root_subvolume = libcalamares.globalstorage.value("btrfsRootSubvolume")
|
||||||
|
if btrfs_root_subvolume:
|
||||||
|
kernel_params.append("rootflags=subvol=" + btrfs_root_subvolume)
|
||||||
|
|
||||||
|
# zfs needs to be told the location of the root dataset
|
||||||
|
if is_zfs_root(partition):
|
||||||
|
zfs_root_path = get_zfs_root()
|
||||||
|
if zfs_root_path is not None:
|
||||||
|
kernel_params.append("root=ZFS=" + zfs_root_path)
|
||||||
|
else:
|
||||||
|
# Something is really broken if we get to this point
|
||||||
|
libcalamares.utils.warning("Internal error handling zfs dataset")
|
||||||
|
raise Exception("Internal zfs data missing, please contact your distribution")
|
||||||
|
|
||||||
|
if cryptdevice_params:
|
||||||
|
kernel_params.extend(cryptdevice_params)
|
||||||
|
else:
|
||||||
|
kernel_params.append("root=UUID={!s}".format(uuid))
|
||||||
|
|
||||||
|
if swap_uuid:
|
||||||
|
kernel_params.append("resume=UUID={!s}".format(swap_uuid))
|
||||||
|
|
||||||
|
if use_systemd_naming and swap_outer_uuid:
|
||||||
|
kernel_params.append(f"rd.luks.uuid={swap_outer_uuid}")
|
||||||
|
|
||||||
|
if swap_outer_mappername:
|
||||||
|
kernel_params.append(f"resume=/dev/mapper/{swap_outer_mappername}")
|
||||||
|
|
||||||
|
return kernel_params
|
||||||
|
|
||||||
|
|
||||||
|
def create_systemd_boot_conf(installation_root_path, efi_dir, uuid, kernel, kernel_version):
|
||||||
|
"""
|
||||||
|
Creates systemd-boot configuration files based on given parameters.
|
||||||
|
|
||||||
|
:param installation_root_path: A string containing the absolute path to the root of the installation
|
||||||
|
:param efi_dir: A string containing the path to the efi dir relative to the root of the installation
|
||||||
|
:param uuid: A string containing the UUID of the root volume
|
||||||
|
:param kernel: A string containing the path to the kernel relative to the root of the installation
|
||||||
|
:param kernel_version: The kernel version string
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get the kernel params and write them to /etc/kernel/cmdline
|
||||||
|
# This file is used by kernel-install
|
||||||
|
kernel_params = " ".join(get_kernel_params(uuid))
|
||||||
|
kernel_cmdline_path = os.path.join(installation_root_path, "etc", "kernel")
|
||||||
|
os.makedirs(kernel_cmdline_path, exist_ok=True)
|
||||||
|
with open(os.path.join(kernel_cmdline_path, "cmdline"), "w") as cmdline_file:
|
||||||
|
cmdline_file.write(kernel_params)
|
||||||
|
|
||||||
|
libcalamares.utils.debug(f"Configuring kernel version {kernel_version}")
|
||||||
|
|
||||||
|
# get the machine-id
|
||||||
|
with open(os.path.join(installation_root_path, "etc", "machine-id"), 'r') as machineid_file:
|
||||||
|
machine_id = machineid_file.read().rstrip('\n')
|
||||||
|
|
||||||
|
# Ensure the directory exists
|
||||||
|
machine_dir = os.path.join(installation_root_path + efi_dir, machine_id)
|
||||||
|
os.makedirs(machine_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Call kernel-install for each kernel
|
||||||
|
libcalamares.utils.target_env_process_output(["kernel-install",
|
||||||
|
"add",
|
||||||
|
kernel_version,
|
||||||
|
os.path.join("/", kernel)])
|
||||||
|
|
||||||
|
|
||||||
|
def create_loader(loader_path, installation_root_path):
|
||||||
|
"""
|
||||||
|
Writes configuration for loader.
|
||||||
|
|
||||||
|
:param loader_path: The absolute path to the loader.conf file
|
||||||
|
:param installation_root_path: The path to the root of the target installation
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get the machine-id
|
||||||
|
with open(os.path.join(installation_root_path, "etc", "machine-id"), 'r') as machineid_file:
|
||||||
|
machine_id = machineid_file.read().rstrip('\n')
|
||||||
|
|
||||||
|
try:
|
||||||
|
loader_entries = libcalamares.job.configuration["loaderEntries"]
|
||||||
|
except KeyError:
|
||||||
|
libcalamares.utils.debug("No aditional loader entries found in config")
|
||||||
|
loader_entries = []
|
||||||
|
pass
|
||||||
|
|
||||||
|
lines = [f"default {machine_id}*"]
|
||||||
|
|
||||||
|
lines.extend(loader_entries)
|
||||||
|
|
||||||
|
with open(loader_path, 'w') as loader_file:
|
||||||
|
for line in lines:
|
||||||
|
loader_file.write(line + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
class SuffixIterator(object):
|
||||||
|
"""
|
||||||
|
Wrapper for one of the "generator" classes below to behave like
|
||||||
|
a proper Python iterator. The iterator is initialized with a
|
||||||
|
maximum number of attempts to generate a new suffix.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, attempts, generator):
|
||||||
|
self.generator = generator
|
||||||
|
self.attempts = attempts
|
||||||
|
self.counter = 0
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __next__(self):
|
||||||
|
self.counter += 1
|
||||||
|
if self.counter <= self.attempts:
|
||||||
|
return self.generator.next()
|
||||||
|
raise StopIteration
|
||||||
|
|
||||||
|
|
||||||
|
class serialEfi(object):
|
||||||
|
"""
|
||||||
|
EFI Id generator that appends a serial number to the given name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
# So the first call to next() will bump it to 0
|
||||||
|
self.counter = -1
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
self.counter += 1
|
||||||
|
if self.counter > 0:
|
||||||
|
return "{!s}{!s}".format(self.name, self.counter)
|
||||||
|
else:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
def render_in_base(value, base_values, length=-1):
|
||||||
|
"""
|
||||||
|
Renders @p value in base-N, where N is the number of
|
||||||
|
items in @p base_values. When rendering, use the items
|
||||||
|
of @p base_values (e.g. use "0123456789" to get regular decimal
|
||||||
|
rendering, or "ABCDEFGHIJ" for letters-as-numbers 'encoding').
|
||||||
|
|
||||||
|
If length is positive, pads out to at least that long with
|
||||||
|
leading "zeroes", whatever base_values[0] is.
|
||||||
|
"""
|
||||||
|
if value < 0:
|
||||||
|
raise ValueError("Cannot render negative values")
|
||||||
|
if len(base_values) < 2:
|
||||||
|
raise ValueError("Insufficient items for base-N rendering")
|
||||||
|
if length < 1:
|
||||||
|
length = 1
|
||||||
|
digits = []
|
||||||
|
base = len(base_values)
|
||||||
|
while value > 0:
|
||||||
|
place = value % base
|
||||||
|
value = value // base
|
||||||
|
digits.append(base_values[place])
|
||||||
|
while len(digits) < length:
|
||||||
|
digits.append(base_values[0])
|
||||||
|
return "".join(reversed(digits))
|
||||||
|
|
||||||
|
|
||||||
|
class randomEfi(object):
|
||||||
|
"""
|
||||||
|
EFI Id generator that appends a random 4-digit hex number to the given name.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
# So the first call to next() will bump it to 0
|
||||||
|
self.counter = -1
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
self.counter += 1
|
||||||
|
if self.counter > 0:
|
||||||
|
import random
|
||||||
|
v = random.randint(0, 65535) # 16 bits
|
||||||
|
return "{!s}{!s}".format(self.name, render_in_base(v, "0123456789ABCDEF", 4))
|
||||||
|
else:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class phraseEfi(object):
|
||||||
|
"""
|
||||||
|
EFI Id generator that appends a random phrase to the given name.
|
||||||
|
"""
|
||||||
|
words = ("Sun", "Moon", "Mars", "Soyuz", "Falcon", "Kuaizhou", "Gaganyaan")
|
||||||
|
|
||||||
|
def __init__(self, name):
|
||||||
|
self.name = name
|
||||||
|
# So the first call to next() will bump it to 0
|
||||||
|
self.counter = -1
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
self.counter += 1
|
||||||
|
if self.counter > 0:
|
||||||
|
import random
|
||||||
|
desired_length = 1 + self.counter // 5
|
||||||
|
v = random.randint(0, len(self.words) ** desired_length)
|
||||||
|
return "{!s}{!s}".format(self.name, render_in_base(v, self.words))
|
||||||
|
else:
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
def get_efi_suffix_generator(name):
|
||||||
|
"""
|
||||||
|
Handle EFI bootloader Ids with ${<something>} for suffix-processing.
|
||||||
|
"""
|
||||||
|
if "${" not in name:
|
||||||
|
raise ValueError("Misplaced call to get_efi_suffix_generator, no ${}")
|
||||||
|
if not name.endswith("}"):
|
||||||
|
raise ValueError("Misplaced call to get_efi_suffix_generator, no trailing ${}")
|
||||||
|
if name.count("${") > 1:
|
||||||
|
raise ValueError("EFI ID {!r} contains multiple generators".format(name))
|
||||||
|
import re
|
||||||
|
prefix, generator_name = re.match("(.*)\${([^}]*)}$", name).groups()
|
||||||
|
if generator_name not in ("SERIAL", "RANDOM", "PHRASE"):
|
||||||
|
raise ValueError("EFI suffix {!r} is unknown".format(generator_name))
|
||||||
|
|
||||||
|
generator = None
|
||||||
|
if generator_name == "SERIAL":
|
||||||
|
generator = serialEfi(prefix)
|
||||||
|
elif generator_name == "RANDOM":
|
||||||
|
generator = randomEfi(prefix)
|
||||||
|
elif generator_name == "PHRASE":
|
||||||
|
generator = phraseEfi(prefix)
|
||||||
|
if generator is None:
|
||||||
|
raise ValueError("EFI suffix {!r} is unsupported".format(generator_name))
|
||||||
|
|
||||||
|
return generator
|
||||||
|
|
||||||
|
|
||||||
|
def change_efi_suffix(efi_directory, bootloader_id):
|
||||||
|
"""
|
||||||
|
Returns a label based on @p bootloader_id that is usable within
|
||||||
|
@p efi_directory. If there is a ${<something>} suffix marker
|
||||||
|
in the given id, tries to generate a unique label.
|
||||||
|
"""
|
||||||
|
if bootloader_id.endswith("}") and "${" in bootloader_id:
|
||||||
|
# Do 10 attempts with any suffix generator
|
||||||
|
g = SuffixIterator(10, get_efi_suffix_generator(bootloader_id))
|
||||||
|
else:
|
||||||
|
# Just one attempt
|
||||||
|
g = [bootloader_id]
|
||||||
|
|
||||||
|
for candidate_name in g:
|
||||||
|
if not os.path.exists(os.path.join(efi_directory, candidate_name)):
|
||||||
|
return candidate_name
|
||||||
|
return bootloader_id
|
||||||
|
|
||||||
|
|
||||||
|
def efi_label(efi_directory):
|
||||||
|
"""
|
||||||
|
Returns a sanitized label, possibly unique, that can be
|
||||||
|
used within @p efi_directory.
|
||||||
|
"""
|
||||||
|
if "efiBootloaderId" in libcalamares.job.configuration:
|
||||||
|
efi_bootloader_id = change_efi_suffix(efi_directory, libcalamares.job.configuration["efiBootloaderId"])
|
||||||
|
else:
|
||||||
|
branding = libcalamares.globalstorage.value("branding")
|
||||||
|
efi_bootloader_id = branding["bootloaderEntryName"]
|
||||||
|
|
||||||
|
return efi_bootloader_id.translate(file_name_sanitizer)
|
||||||
|
|
||||||
|
|
||||||
|
def efi_word_size():
|
||||||
|
# get bitness of the underlying UEFI
|
||||||
|
try:
|
||||||
|
sysfile = open("/sys/firmware/efi/fw_platform_size", "r")
|
||||||
|
efi_bitness = sysfile.read(2)
|
||||||
|
except Exception:
|
||||||
|
# if the kernel is older than 4.0, the UEFI bitness likely isn't
|
||||||
|
# exposed to the userspace so we assume a 64 bit UEFI here
|
||||||
|
efi_bitness = "64"
|
||||||
|
return efi_bitness
|
||||||
|
|
||||||
|
|
||||||
|
def efi_boot_next():
|
||||||
|
"""
|
||||||
|
Tell EFI to definitely boot into the just-installed
|
||||||
|
system next time.
|
||||||
|
"""
|
||||||
|
boot_mgr = libcalamares.job.configuration["efiBootMgr"]
|
||||||
|
boot_entry = None
|
||||||
|
efi_bootvars = subprocess.check_output([boot_mgr], universal_newlines=True)
|
||||||
|
for line in efi_bootvars.split('\n'):
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
words = line.split()
|
||||||
|
if len(words) >= 2 and words[0] == "BootOrder:":
|
||||||
|
boot_entry = words[1].split(',')[0]
|
||||||
|
break
|
||||||
|
if boot_entry:
|
||||||
|
subprocess.call([boot_mgr, "-n", boot_entry])
|
||||||
|
|
||||||
|
|
||||||
|
def get_kernels(installation_root_path):
|
||||||
|
"""
|
||||||
|
Gets a list of kernels and associated values for each kernel. This will work as is for many distros.
|
||||||
|
If not, it should be safe to modify it to better support your distro
|
||||||
|
|
||||||
|
:param installation_root_path: A string with the absolute path to the root of the installation
|
||||||
|
|
||||||
|
Returns a list of 3-tuples
|
||||||
|
|
||||||
|
Each 3-tuple contains the kernel, kernel_type and kernel_version
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
kernel_search_path = libcalamares.job.configuration["kernelSearchPath"]
|
||||||
|
except KeyError:
|
||||||
|
libcalamares.utils.warning("No kernel pattern found in configuration, using '/usr/lib/modules'")
|
||||||
|
kernel_search_path = "/usr/lib/modules"
|
||||||
|
pass
|
||||||
|
|
||||||
|
kernel_list = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
kernel_pattern = libcalamares.job.configuration["kernelPattern"]
|
||||||
|
except KeyError:
|
||||||
|
libcalamares.utils.warning("No kernel pattern found in configuration, using 'vmlinuz'")
|
||||||
|
kernel_pattern = "vmlinuz"
|
||||||
|
pass
|
||||||
|
|
||||||
|
# find all the installed kernels
|
||||||
|
for root, dirs, files in os.walk(os.path.join(installation_root_path, kernel_search_path.lstrip('/'))):
|
||||||
|
for file in files:
|
||||||
|
if re.search(kernel_pattern, file):
|
||||||
|
rel_root = os.path.relpath(root, installation_root_path)
|
||||||
|
kernel_list.append((os.path.join(rel_root, file), "default", os.path.basename(root)))
|
||||||
|
|
||||||
|
return kernel_list
|
||||||
|
|
||||||
|
|
||||||
|
def install_clr_boot_manager():
|
||||||
|
"""
|
||||||
|
Installs clr-boot-manager as the bootloader for EFI systems
|
||||||
|
"""
|
||||||
|
libcalamares.utils.debug("Bootloader: clr-boot-manager")
|
||||||
|
|
||||||
|
installation_root_path = libcalamares.globalstorage.value("rootMountPoint")
|
||||||
|
kernel_config_path = os.path.join(installation_root_path, "etc", "kernel")
|
||||||
|
os.makedirs(kernel_config_path, exist_ok=True)
|
||||||
|
cmdline_path = os.path.join(kernel_config_path, "cmdline")
|
||||||
|
|
||||||
|
# Get the kernel params
|
||||||
|
uuid = get_uuid()
|
||||||
|
kernel_params = " ".join(get_kernel_params(uuid))
|
||||||
|
|
||||||
|
# Write out the cmdline file for clr-boot-manager
|
||||||
|
with open(cmdline_path, "w") as cmdline_file:
|
||||||
|
cmdline_file.write(kernel_params)
|
||||||
|
|
||||||
|
check_target_env_call(["clr-boot-manager", "update"])
|
||||||
|
|
||||||
|
|
||||||
|
def install_systemd_boot(efi_directory):
|
||||||
|
"""
|
||||||
|
Installs systemd-boot as bootloader for EFI setups.
|
||||||
|
|
||||||
|
:param efi_directory:
|
||||||
|
"""
|
||||||
|
libcalamares.utils.debug("Bootloader: systemd-boot")
|
||||||
|
installation_root_path = libcalamares.globalstorage.value("rootMountPoint")
|
||||||
|
install_efi_directory = installation_root_path + efi_directory
|
||||||
|
uuid = get_uuid()
|
||||||
|
loader_path = os.path.join(install_efi_directory,
|
||||||
|
"loader",
|
||||||
|
"loader.conf")
|
||||||
|
subprocess.call(["bootctl",
|
||||||
|
"--path={!s}".format(install_efi_directory),
|
||||||
|
"install"])
|
||||||
|
|
||||||
|
for (kernel, kernel_type, kernel_version) in get_kernels(installation_root_path):
|
||||||
|
create_systemd_boot_conf(installation_root_path,
|
||||||
|
efi_directory,
|
||||||
|
uuid,
|
||||||
|
kernel,
|
||||||
|
kernel_version)
|
||||||
|
|
||||||
|
create_loader(loader_path, installation_root_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_grub_efi_parameters():
|
||||||
|
"""
|
||||||
|
Returns a 3-tuple of suitable parameters for GRUB EFI installation,
|
||||||
|
depending on the host machine architecture. The return is
|
||||||
|
- target name
|
||||||
|
- grub.efi name
|
||||||
|
- boot.efi name
|
||||||
|
all three are strings. May return None if there is no suitable
|
||||||
|
set for the current machine. May return unsuitable values if the
|
||||||
|
host architecture is unknown (e.g. defaults to x86_64).
|
||||||
|
"""
|
||||||
|
import platform
|
||||||
|
efi_bitness = efi_word_size()
|
||||||
|
cpu_type = platform.machine()
|
||||||
|
|
||||||
|
if efi_bitness == "32":
|
||||||
|
# Assume all 32-bitters are legacy x86
|
||||||
|
return "i386-efi", "grubia32.efi", "bootia32.efi"
|
||||||
|
elif efi_bitness == "64" and cpu_type == "aarch64":
|
||||||
|
return "arm64-efi", "grubaa64.efi", "bootaa64.efi"
|
||||||
|
elif efi_bitness == "64" and cpu_type == "loongarch64":
|
||||||
|
return "loongarch64-efi", "grubloongarch64.efi", "bootloongarch64.efi"
|
||||||
|
elif efi_bitness == "64":
|
||||||
|
# If it's not ARM, must by AMD64
|
||||||
|
return "x86_64-efi", "grubx64.efi", "bootx64.efi"
|
||||||
|
libcalamares.utils.warning(
|
||||||
|
"Could not find GRUB parameters for bits {b} and cpu {c}".format(b=repr(efi_bitness), c=repr(cpu_type)))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def run_grub_mkconfig(partitions, output_file):
|
||||||
|
"""
|
||||||
|
Runs grub-mkconfig in the target environment
|
||||||
|
|
||||||
|
:param partitions: The partitions list from global storage
|
||||||
|
:param output_file: A string containing the path to the generating grub config file
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# zfs needs an environment variable set for grub-mkconfig
|
||||||
|
if any([is_zfs_root(partition) for partition in partitions]):
|
||||||
|
check_target_env_call(["sh", "-c", "ZPOOL_VDEV_NAME_PATH=1 " +
|
||||||
|
libcalamares.job.configuration["grubMkconfig"] + " -o " + output_file])
|
||||||
|
else:
|
||||||
|
# The input file /etc/default/grub should already be filled out by the
|
||||||
|
# grubcfg job module.
|
||||||
|
check_target_env_call([libcalamares.job.configuration["grubMkconfig"], "-o", output_file])
|
||||||
|
|
||||||
|
|
||||||
|
def run_grub_install(fw_type, partitions, efi_directory, install_hybrid_grub):
|
||||||
|
"""
|
||||||
|
Runs grub-install in the target environment
|
||||||
|
|
||||||
|
:param fw_type: A string which is "efi" for UEFI installs. Any other value results in a BIOS install
|
||||||
|
:param partitions: The partitions list from global storage
|
||||||
|
:param efi_directory: The path of the efi directory relative to the root of the install
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
libcalamares.utils.debug("run_grub_install------")
|
||||||
|
|
||||||
|
is_zfs = any([is_zfs_root(partition) for partition in partitions])
|
||||||
|
|
||||||
|
# zfs needs an environment variable set for grub
|
||||||
|
if is_zfs:
|
||||||
|
check_target_env_call(["sh", "-c", "echo ZPOOL_VDEV_NAME_PATH=1 >> /etc/environment"])
|
||||||
|
|
||||||
|
if fw_type == "efi":
|
||||||
|
assert efi_directory is not None
|
||||||
|
efi_bootloader_id = efi_label(efi_directory)
|
||||||
|
efi_target, efi_grub_file, efi_boot_file = get_grub_efi_parameters()
|
||||||
|
|
||||||
|
if is_zfs:
|
||||||
|
check_target_env_call(["sh", "-c", "ZPOOL_VDEV_NAME_PATH=1 " + libcalamares.job.configuration["grubInstall"]
|
||||||
|
+ " --target=" + efi_target + " --efi-directory=" + efi_directory
|
||||||
|
+ " --bootloader-id=" + efi_bootloader_id + " --force"])
|
||||||
|
else:
|
||||||
|
check_target_env_call([libcalamares.job.configuration["grubInstall"],
|
||||||
|
"--target=" + efi_target,
|
||||||
|
"--efi-directory=" + efi_directory,
|
||||||
|
"--bootloader-id=" + efi_bootloader_id,
|
||||||
|
"--force"])
|
||||||
|
else:
|
||||||
|
|
||||||
|
if libcalamares.globalstorage.value("bootLoader") is None and install_hybrid_grub:
|
||||||
|
efi_install_path = libcalamares.globalstorage.value("efiSystemPartition")
|
||||||
|
if efi_install_path is None or efi_install_path == "":
|
||||||
|
efi_install_path = "/boot/efi"
|
||||||
|
find_esp_disk_command = f"lsblk -o PKNAME \"$(df --output=source '{efi_install_path}' | tail -n1)\""
|
||||||
|
boot_loader_install_path = check_target_env_output(["sh", "-c", find_esp_disk_command]).strip()
|
||||||
|
if not "\n" in boot_loader_install_path:
|
||||||
|
libcalamares.utils.warning(_("Cannot find the drive containing the EFI system partition!"))
|
||||||
|
return
|
||||||
|
boot_loader_install_path = "/dev/" + boot_loader_install_path.split("\n")[1]
|
||||||
|
else:
|
||||||
|
boot_loader = libcalamares.globalstorage.value("bootLoader")
|
||||||
|
boot_loader_install_path = boot_loader["installPath"]
|
||||||
|
libcalamares.utils.debug(f"boot_loader_install_path: {boot_loader_install_path}")
|
||||||
|
if boot_loader_install_path is None:
|
||||||
|
libcalamares.utils.debug(f"boot_loader_install_path-none: {boot_loader_install_path}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# boot_loader_install_path points to the physical disk to install GRUB
|
||||||
|
# to. It should start with "/dev/", and be at least as long as the
|
||||||
|
# string "/dev/sda".
|
||||||
|
if not boot_loader_install_path.startswith("/dev/") or len(boot_loader_install_path) < 8:
|
||||||
|
raise ValueError(f"boot_loader_install_path contains unexpected value '{boot_loader_install_path}'")
|
||||||
|
|
||||||
|
if is_zfs:
|
||||||
|
check_target_env_call(["sh", "-c", "ZPOOL_VDEV_NAME_PATH=1 "
|
||||||
|
+ libcalamares.job.configuration["grubInstall"]
|
||||||
|
+ " --target=i386-pc --recheck --force "
|
||||||
|
+ boot_loader_install_path])
|
||||||
|
else:
|
||||||
|
libcalamares.utils.debug(f"grubInstall-boot_loader_install_path: {boot_loader_install_path}")
|
||||||
|
check_target_env_call([libcalamares.job.configuration["grubInstall"],
|
||||||
|
"--target=i386-pc",
|
||||||
|
"--recheck",
|
||||||
|
"--force",
|
||||||
|
boot_loader_install_path])
|
||||||
|
|
||||||
|
|
||||||
|
def install_grub(efi_directory, fw_type, install_hybrid_grub):
|
||||||
|
"""
|
||||||
|
Installs grub as bootloader, either in pc or efi mode.
|
||||||
|
|
||||||
|
:param efi_directory:
|
||||||
|
:param fw_type:
|
||||||
|
:param install_hybrid_grub:
|
||||||
|
"""
|
||||||
|
# get the partition from global storage
|
||||||
|
partitions = libcalamares.globalstorage.value("partitions")
|
||||||
|
if not partitions:
|
||||||
|
libcalamares.utils.warning(_("Failed to install grub, no partitions defined in global storage"))
|
||||||
|
return
|
||||||
|
|
||||||
|
if fw_type != "bios" and fw_type != "efi":
|
||||||
|
raise ValueError("fw_type must be 'bios' or 'efi'")
|
||||||
|
|
||||||
|
if fw_type == "efi" or install_hybrid_grub:
|
||||||
|
libcalamares.utils.debug("Bootloader: grub (efi)")
|
||||||
|
libcalamares.utils.debug(f"install_hybrid_grub: {install_hybrid_grub}")
|
||||||
|
installation_root_path = libcalamares.globalstorage.value("rootMountPoint")
|
||||||
|
install_efi_directory = installation_root_path + efi_directory
|
||||||
|
|
||||||
|
if not os.path.isdir(install_efi_directory):
|
||||||
|
os.makedirs(install_efi_directory)
|
||||||
|
|
||||||
|
efi_bootloader_id = efi_label(efi_directory)
|
||||||
|
|
||||||
|
efi_target, efi_grub_file, efi_boot_file = get_grub_efi_parameters()
|
||||||
|
|
||||||
|
run_grub_install("efi", partitions, efi_directory, install_hybrid_grub)
|
||||||
|
|
||||||
|
# VFAT is weird, see issue CAL-385
|
||||||
|
install_efi_directory_firmware = (vfat_correct_case(
|
||||||
|
install_efi_directory,
|
||||||
|
"EFI"))
|
||||||
|
if not os.path.exists(install_efi_directory_firmware):
|
||||||
|
os.makedirs(install_efi_directory_firmware)
|
||||||
|
|
||||||
|
# there might be several values for the boot directory
|
||||||
|
# most usual they are boot, Boot, BOOT
|
||||||
|
|
||||||
|
install_efi_boot_directory = (vfat_correct_case(
|
||||||
|
install_efi_directory_firmware,
|
||||||
|
"boot"))
|
||||||
|
if not os.path.exists(install_efi_boot_directory):
|
||||||
|
os.makedirs(install_efi_boot_directory)
|
||||||
|
|
||||||
|
# Workaround for some UEFI firmwares
|
||||||
|
fallback = "installEFIFallback"
|
||||||
|
libcalamares.utils.debug("UEFI Fallback: " + str(libcalamares.job.configuration.get(fallback, "<unset>")))
|
||||||
|
if libcalamares.job.configuration.get(fallback, True):
|
||||||
|
libcalamares.utils.debug(" .. installing '{!s}' fallback firmware".format(efi_boot_file))
|
||||||
|
efi_file_source = os.path.join(install_efi_directory_firmware,
|
||||||
|
efi_bootloader_id,
|
||||||
|
efi_grub_file)
|
||||||
|
efi_file_target = os.path.join(install_efi_boot_directory, efi_boot_file)
|
||||||
|
|
||||||
|
shutil.copy2(efi_file_source, efi_file_target)
|
||||||
|
if fw_type == "bios" or install_hybrid_grub:
|
||||||
|
libcalamares.utils.debug("Bootloader: grub (bios)")
|
||||||
|
run_grub_install("bios", partitions, efi_directory, install_hybrid_grub)
|
||||||
|
|
||||||
|
run_grub_mkconfig(partitions, libcalamares.job.configuration["grubCfg"])
|
||||||
|
|
||||||
|
|
||||||
|
def install_secureboot(efi_directory):
|
||||||
|
"""
|
||||||
|
Installs the secureboot shim in the system by calling efibootmgr.
|
||||||
|
"""
|
||||||
|
efi_bootloader_id = efi_label(efi_directory)
|
||||||
|
|
||||||
|
installation_root_path = libcalamares.globalstorage.value("rootMountPoint")
|
||||||
|
install_efi_directory = installation_root_path + efi_directory
|
||||||
|
|
||||||
|
if efi_word_size() == "64":
|
||||||
|
install_efi_bin = "shimx64.efi"
|
||||||
|
elif efi_word_size() == "32":
|
||||||
|
install_efi_bin = "shimia32.efi"
|
||||||
|
else:
|
||||||
|
libcalamares.utils.warning(f"Unknown efi word size of {efi_word_size()} found")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Copied, roughly, from openSUSE's install script,
|
||||||
|
# and pythonified. *disk* is something like /dev/sda,
|
||||||
|
# while *drive* may return "(disk/dev/sda,gpt1)" ..
|
||||||
|
# we're interested in the numbers in the second part
|
||||||
|
# of that tuple.
|
||||||
|
efi_drive = subprocess.check_output([
|
||||||
|
libcalamares.job.configuration["grubProbe"],
|
||||||
|
"-t", "drive", "--device-map=", install_efi_directory]).decode("ascii")
|
||||||
|
efi_disk = subprocess.check_output([
|
||||||
|
libcalamares.job.configuration["grubProbe"],
|
||||||
|
"-t", "disk", "--device-map=", install_efi_directory]).decode("ascii")
|
||||||
|
|
||||||
|
efi_drive_partition = efi_drive.replace("(", "").replace(")", "").split(",")[1]
|
||||||
|
# Get the first run of digits from the partition
|
||||||
|
efi_partition_number = None
|
||||||
|
c = 0
|
||||||
|
start = None
|
||||||
|
while c < len(efi_drive_partition):
|
||||||
|
if efi_drive_partition[c].isdigit() and start is None:
|
||||||
|
start = c
|
||||||
|
if not efi_drive_partition[c].isdigit() and start is not None:
|
||||||
|
efi_partition_number = efi_drive_partition[start:c]
|
||||||
|
break
|
||||||
|
c += 1
|
||||||
|
if efi_partition_number is None:
|
||||||
|
raise ValueError("No partition number found for %s" % install_efi_directory)
|
||||||
|
|
||||||
|
subprocess.call([
|
||||||
|
libcalamares.job.configuration["efiBootMgr"],
|
||||||
|
"-c",
|
||||||
|
"-w",
|
||||||
|
"-L", efi_bootloader_id,
|
||||||
|
"-d", efi_disk,
|
||||||
|
"-p", efi_partition_number,
|
||||||
|
"-l", install_efi_directory + "/" + install_efi_bin])
|
||||||
|
|
||||||
|
efi_boot_next()
|
||||||
|
|
||||||
|
# The input file /etc/default/grub should already be filled out by the
|
||||||
|
# grubcfg job module.
|
||||||
|
check_target_env_call([libcalamares.job.configuration["grubMkconfig"],
|
||||||
|
"-o", os.path.join(efi_directory, "EFI",
|
||||||
|
efi_bootloader_id, "grub.cfg")])
|
||||||
|
|
||||||
|
|
||||||
|
def vfat_correct_case(parent, name):
|
||||||
|
for candidate in os.listdir(parent):
|
||||||
|
if name.lower() == candidate.lower():
|
||||||
|
return os.path.join(parent, candidate)
|
||||||
|
return os.path.join(parent, name)
|
||||||
|
|
||||||
|
|
||||||
|
def efi_partitions(efi_boot_path):
|
||||||
|
"""
|
||||||
|
The (one) partition mounted on @p efi_boot_path, or an empty list.
|
||||||
|
"""
|
||||||
|
return [p for p in libcalamares.globalstorage.value("partitions") if p["mountPoint"] == efi_boot_path]
|
||||||
|
|
||||||
|
|
||||||
|
def update_refind_config(efi_directory, installation_root_path):
|
||||||
|
"""
|
||||||
|
:param efi_directory: The path to the efi directory relative to the root
|
||||||
|
:param installation_root_path: The path to the root of the installation
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
kernel_list = libcalamares.job.configuration["refindKernelList"]
|
||||||
|
except KeyError:
|
||||||
|
libcalamares.utils.warning('refindKernelList not set. Skipping updating refind.conf')
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update the config in the file
|
||||||
|
for line in fileinput.input(installation_root_path + efi_directory + "/EFI/refind/refind.conf", inplace=True):
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("#extra_kernel_version_strings") or line.startswith("extra_kernel_version_strings"):
|
||||||
|
line = line.lstrip("#")
|
||||||
|
for kernel in kernel_list:
|
||||||
|
if kernel not in line:
|
||||||
|
line += "," + kernel
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
def install_refind(efi_directory):
|
||||||
|
try:
|
||||||
|
installation_root_path = libcalamares.globalstorage.value("rootMountPoint")
|
||||||
|
except KeyError:
|
||||||
|
libcalamares.utils.warning('Global storage value "rootMountPoint" missing')
|
||||||
|
|
||||||
|
install_efi_directory = installation_root_path + efi_directory
|
||||||
|
uuid = get_uuid()
|
||||||
|
kernel_params = " ".join(get_kernel_params(uuid))
|
||||||
|
conf_path = os.path.join(installation_root_path, "boot/refind_linux.conf")
|
||||||
|
|
||||||
|
check_target_env_call(["refind-install"])
|
||||||
|
|
||||||
|
with open(conf_path, "r") as refind_file:
|
||||||
|
filedata = [x.strip() for x in refind_file.readlines()]
|
||||||
|
|
||||||
|
with open(conf_path, 'w') as refind_file:
|
||||||
|
for line in filedata:
|
||||||
|
if line.startswith('"Boot with standard options"'):
|
||||||
|
line = f'"Boot with standard options" "{kernel_params}"'
|
||||||
|
elif line.startswith('"Boot to single-user mode"'):
|
||||||
|
line = f'"Boot to single-user mode" "{kernel_params}" single'
|
||||||
|
refind_file.write(line + "\n")
|
||||||
|
|
||||||
|
update_refind_config(efi_directory, installation_root_path)
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_bootloader(fw_type, install_hybrid_grub):
|
||||||
|
"""
|
||||||
|
Prepares bootloader.
|
||||||
|
Based on value 'efi_boot_loader', it either calls systemd-boot
|
||||||
|
or grub to be installed.
|
||||||
|
|
||||||
|
:param fw_type:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Get the boot loader selection from global storage if it is set in the config file
|
||||||
|
try:
|
||||||
|
gs_name = libcalamares.job.configuration["efiBootLoaderVar"]
|
||||||
|
if libcalamares.globalstorage.contains(gs_name):
|
||||||
|
efi_boot_loader = libcalamares.globalstorage.value(gs_name)
|
||||||
|
else:
|
||||||
|
libcalamares.utils.warning(
|
||||||
|
f"Specified global storage value not found in global storage")
|
||||||
|
return None
|
||||||
|
except KeyError:
|
||||||
|
# If the conf value for using global storage is not set, use the setting from the config file.
|
||||||
|
try:
|
||||||
|
efi_boot_loader = libcalamares.job.configuration["efiBootLoader"]
|
||||||
|
except KeyError:
|
||||||
|
if fw_type == "efi" or install_hybrid_grub:
|
||||||
|
libcalamares.utils.warning("Configuration missing both efiBootLoader and efiBootLoaderVar on an EFI-enabled "
|
||||||
|
"system, bootloader not installed")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If the user has selected not to install bootloader, bail out here
|
||||||
|
if efi_boot_loader.casefold() == "none":
|
||||||
|
libcalamares.utils.debug("Skipping bootloader installation since no bootloader was selected")
|
||||||
|
return None
|
||||||
|
|
||||||
|
efi_directory = libcalamares.globalstorage.value("efiSystemPartition")
|
||||||
|
|
||||||
|
if efi_boot_loader == "clr-boot-manager":
|
||||||
|
if fw_type != "efi":
|
||||||
|
# Grub has to be installed first on non-EFI systems
|
||||||
|
install_grub(efi_directory, fw_type)
|
||||||
|
install_clr_boot_manager()
|
||||||
|
elif efi_boot_loader == "systemd-boot" and fw_type == "efi":
|
||||||
|
install_systemd_boot(efi_directory)
|
||||||
|
elif efi_boot_loader == "sb-shim" and fw_type == "efi":
|
||||||
|
install_secureboot(efi_directory)
|
||||||
|
elif efi_boot_loader == "refind" and fw_type == "efi":
|
||||||
|
install_refind(efi_directory)
|
||||||
|
elif efi_boot_loader == "grub" or fw_type != "efi":
|
||||||
|
install_grub(efi_directory, fw_type, install_hybrid_grub)
|
||||||
|
else:
|
||||||
|
libcalamares.utils.debug("WARNING: the combination of "
|
||||||
|
"boot-loader '{!s}' and firmware '{!s}' "
|
||||||
|
"is not supported.".format(efi_boot_loader, fw_type))
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
"""
|
||||||
|
Starts procedure and passes 'fw_type' to other routine.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
|
||||||
|
fw_type = libcalamares.globalstorage.value("firmwareType")
|
||||||
|
boot_loader = libcalamares.globalstorage.value("bootLoader")
|
||||||
|
|
||||||
|
install_hybrid_grub = libcalamares.job.configuration.get("installHybridGRUB", False)
|
||||||
|
efi_boot_loader = libcalamares.job.configuration.get("efiBootLoader", "")
|
||||||
|
|
||||||
|
if install_hybrid_grub == True and efi_boot_loader != "grub":
|
||||||
|
raise ValueError(f"efi_boot_loader '{efi_boot_loader}' is illegal when install_hybrid_grub is 'true'!")
|
||||||
|
|
||||||
|
if boot_loader is None and fw_type != "efi":
|
||||||
|
libcalamares.utils.warning("Non-EFI system, and no bootloader is set.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
partitions = libcalamares.globalstorage.value("partitions")
|
||||||
|
if fw_type == "efi":
|
||||||
|
efi_system_partition = libcalamares.globalstorage.value("efiSystemPartition")
|
||||||
|
esp_found = [p for p in partitions if p["mountPoint"] == efi_system_partition]
|
||||||
|
if not esp_found:
|
||||||
|
libcalamares.utils.warning("EFI system, but nothing mounted on {!s}".format(efi_system_partition))
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
prepare_bootloader(fw_type, install_hybrid_grub)
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
libcalamares.utils.warning(str(e))
|
||||||
|
libcalamares.utils.debug("stdout:" + str(e.stdout))
|
||||||
|
libcalamares.utils.debug("stderr:" + str(e.stderr))
|
||||||
|
return (_("Bootloader installation error"),
|
||||||
|
_("The bootloader could not be installed. The installation command <pre>{!s}</pre> returned error "
|
||||||
|
"code {!s}.")
|
||||||
|
.format(e.cmd, e.returncode))
|
||||||
|
|
||||||
|
return None
|
||||||
Reference in New Issue
Block a user