fix bug 打包

This commit is contained in:
zj
2026-02-12 05:58:50 +08:00
parent 6255af2362
commit 54d725e031
9 changed files with 236 additions and 684 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build/
/__pycache__/

38
LinuxGrubRepair.spec Normal file
View File

@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['frontend.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
optimize=0,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='LinuxGrubRepair',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
)

Binary file not shown.

Binary file not shown.

View File

@@ -1340,22 +1340,7 @@ def _manual_install_efi_files(mount_point: str, efi_target: str, efi_grub_file:
modules = "part_gpt part_msdos fat ext2 xfs btrfs normal boot linux configfile search search_fs_uuid search_fs_file" modules = "part_gpt part_msdos fat ext2 xfs btrfs normal boot linux configfile search search_fs_uuid search_fs_file"
# 构建 chroot 内的输出路径 # 构建 chroot 内的输出路径
chroot_output_path = f"/boot/efi/EFI/{bootloader_id}/{efi_grub_file}" chroot_output_path = f"/boot/efi/EFI/{efi_bootloader_id}/{efi_grub_file}"
# 检测是否有独立的 /boot 分区
has_separate_boot = os.path.ismount(os.path.join(mount_point, "boot"))
# 确定正确的 prefix
# 对于独立 /boot 分区,使用 '(<device>)/grub2' 格式,让 GRUB 自动搜索
# 或者使用 '/boot/grub2' 配合 search 命令
if has_separate_boot:
# 独立 /boot 分区:使用相对路径,依赖 grub.cfg 中的 search 命令
# 或者使用 '(,gpt2)/grub2' 格式指定分区
grub_prefix = "/grub2"
log_info(f"检测到独立 /boot 分区,使用 prefix: {grub_prefix}")
else:
grub_prefix = "/boot/grub2"
log_info(f"使用标准 prefix: {grub_prefix}")
# 尝试使用 grub-mkimage 生成 EFI 文件 # 尝试使用 grub-mkimage 生成 EFI 文件
chroot_cmd_prefix = ["sudo", "chroot", mount_point] chroot_cmd_prefix = ["sudo", "chroot", mount_point]
@@ -1363,7 +1348,7 @@ def _manual_install_efi_files(mount_point: str, efi_target: str, efi_grub_file:
"grub2-mkimage", "grub2-mkimage",
"-o", chroot_output_path, "-o", chroot_output_path,
"-O", efi_target, "-O", efi_target,
"-p", grub_prefix, "-p", "/boot/grub2",
] + modules.split() ] + modules.split()
success, _, stderr = run_command( success, _, stderr = run_command(
@@ -1376,52 +1361,6 @@ def _manual_install_efi_files(mount_point: str, efi_target: str, efi_grub_file:
host_output_path = os.path.join(mount_point, chroot_output_path.lstrip('/')) host_output_path = os.path.join(mount_point, chroot_output_path.lstrip('/'))
if success and os.path.exists(host_output_path): if success and os.path.exists(host_output_path):
log_success(f"✓ 成功生成 EFI 文件: {host_output_path}") log_success(f"✓ 成功生成 EFI 文件: {host_output_path}")
# 对于独立 /boot 分区,创建辅助配置
if has_separate_boot:
log_info("为独立 /boot 分区创建辅助配置...")
# 获取 /boot 分区的 UUID
boot_uuid = None
try:
success_uuid, uuid_out, _ = run_command(
["sudo", "blkid", "-s", "UUID", "-o", "value", f"{mount_point}/boot"],
"获取 /boot 分区 UUID",
timeout=10
)
if success_uuid:
boot_uuid = uuid_out.strip()
except:
pass
# 创建 EFI 分区上的 grub.cfgGRUB 首先查找这里)
helper_cfg_path = os.path.join(mount_point, f"boot/efi/EFI/{bootloader_id}/grub.cfg")
try:
# 使用通用 search --file 方法
helper_cfg = '''# BootRepairTool - Auto-find /boot partition
# Try to find grub2/grub.cfg on any partition
if search --no-floppy --set=root --file /grub2/grub.cfg; then
set prefix=($root)/grub2
elif search --no-floppy --set=root --file /grub/grub.cfg; then
set prefix=($root)/grub
elif search --no-floppy --set=root --file /boot/grub2/grub.cfg; then
set prefix=($root)/boot/grub2
elif search --no-floppy --set=root --file /boot/grub/grub.cfg; then
set prefix=($root)/boot/grub
fi
if [ -n "$root" ] && [ -n "$prefix" ]; then
configfile ${prefix}/grub.cfg
else
echo "Error: Could not find grub.cfg"
fi
'''
with open(helper_cfg_path, 'w') as f:
f.write(helper_cfg)
log_success(f"✓ 创建 EFI 辅助 grub.cfg: {helper_cfg_path}")
except Exception as e:
log_warning(f"创建辅助 grub.cfg 失败: {e}")
else: else:
log_error(f"生成 EFI 文件失败: {stderr}") log_error(f"生成 EFI 文件失败: {stderr}")
return False return False
@@ -1515,54 +1454,6 @@ def install_efi_fallback(mount_point: str, efi_target: str, efi_grub_file: str,
try: try:
shutil.copy2(source_file, target_file) shutil.copy2(source_file, target_file)
log_success(f"✓ EFI fallback 安装成功: {source_file} -> {target_file}") log_success(f"✓ EFI fallback 安装成功: {source_file} -> {target_file}")
# 为 fallback 创建 grub.cfg独立 /boot 分区时需要)
has_separate_boot = os.path.ismount(os.path.join(mount_point, "boot"))
if has_separate_boot:
log_info("检测到独立 /boot 分区,为 fallback 创建 grub.cfg...")
# 获取 /boot 分区 UUID
boot_uuid = None
try:
success_uuid, uuid_out, _ = run_command(
["sudo", "blkid", "-s", "UUID", "-o", "value", f"{mount_point}/boot"],
"获取 /boot UUID for fallback",
timeout=10
)
if success_uuid:
boot_uuid = uuid_out.strip()
except:
pass
# 在 Boot 目录创建 grub.cfg
fallback_cfg_path = os.path.join(boot_dir, "grub.cfg")
try:
# 使用通用 search --file 方法
fallback_cfg = '''# BootRepairTool - Auto-find /boot partition
# Try to find grub2/grub.cfg on any partition
if search --no-floppy --set=root --file /grub2/grub.cfg; then
set prefix=($root)/grub2
elif search --no-floppy --set=root --file /grub/grub.cfg; then
set prefix=($root)/grub
elif search --no-floppy --set=root --file /boot/grub2/grub.cfg; then
set prefix=($root)/boot/grub2
elif search --no-floppy --set=root --file /boot/grub/grub.cfg; then
set prefix=($root)/boot/grub
fi
if [ -n "$root" ] && [ -n "$prefix" ]; then
configfile ${prefix}/grub.cfg
else
echo "Error: Could not find grub.cfg"
fi
'''
with open(fallback_cfg_path, 'w') as f:
f.write(fallback_cfg)
log_success(f"✓ 创建 fallback grub.cfg: {fallback_cfg_path}")
except Exception as e:
log_warning(f"创建 fallback grub.cfg 失败: {e}")
return True return True
except Exception as e: except Exception as e:
log_error(f"复制 EFI fallback 文件失败: {e}") log_error(f"复制 EFI fallback 文件失败: {e}")
@@ -1729,61 +1620,17 @@ def check_and_restore_kernel(mount_point: str, distro_type: str, has_separate_bo
) )
elif distro_type in ["arch", "manjaro"]: elif distro_type in ["arch", "manjaro"]:
# Arch 使用 mkinitcpio # Arch 使用 mkinitcpio
# 首先检查是否有 preset 文件
preset_path = os.path.join(mount_point, f"etc/mkinitcpio.d/{kernel_ver}.preset")
if os.path.exists(preset_path):
# 使用 preset 生成
success, _, stderr = run_command( success, _, stderr = run_command(
chroot_cmd_prefix + ["mkinitcpio", "-p", kernel_ver], chroot_cmd_prefix + ["mkinitcpio", "-p", kernel_ver],
f"生成 initramfs for {kernel_ver}", f"生成 initramfs for {kernel_ver}",
timeout=120 timeout=120
) )
else:
# 没有 preset 文件,直接生成 initramfs
log_warning(f"没有找到 preset 文件,使用直接生成模式")
initramfs_path = f"/boot/initramfs-{kernel_ver}.img"
success, _, stderr = run_command(
chroot_cmd_prefix + ["mkinitcpio", "-g", initramfs_path, "-k", kernel_ver],
f"直接生成 initramfs for {kernel_ver}",
timeout=120
)
# 如果直接生成失败,尝试创建临时 preset
if not success:
log_info("尝试创建临时 preset 文件...")
try:
preset_dir = os.path.join(mount_point, "etc/mkinitcpio.d")
os.makedirs(preset_dir, exist_ok=True)
# 创建基本 preset 文件
preset_content = f"""# mkinitcpio preset file for {kernel_ver}
ALL_config="/etc/mkinitcpio.conf"
ALL_kver="{kernel_ver}"
PRESETS=('default')
default_image="/boot/initramfs-{kernel_ver}.img"
"""
with open(preset_path, 'w') as f:
f.write(preset_content)
log_info(f"✓ 创建临时 preset: {preset_path}")
# 再次尝试使用 preset 生成
success, _, stderr = run_command(
chroot_cmd_prefix + ["mkinitcpio", "-p", kernel_ver],
f"使用临时 preset 生成 initramfs for {kernel_ver}",
timeout=120
)
except Exception as e:
log_warning(f"创建临时 preset 失败: {e}")
else: else:
# 通用方法:尝试 dracut # 通用方法:尝试 dracut
success, _, stderr = run_command( success, _, stderr = run_command(
chroot_cmd_prefix + ["dracut", "-f", f"/boot/initramfs-{kernel_ver}.img", kernel_ver], chroot_cmd_prefix + ["dracut", "-f", f"/boot/initramfs-{kernel_ver}.img", kernel_ver],
f"生成 initramfs for {kernel_ver}", f"生成 initramfs for {kernel_ver}",
timeout=120 timeout=120
) )
if success: if success:
@@ -1829,11 +1676,6 @@ def restore_bls_entries(mount_point: str, distro_type: str) -> Tuple[bool, str]:
except Exception as e: except Exception as e:
log_debug(f"检查 BLS 失败: {e}") log_debug(f"检查 BLS 失败: {e}")
# 对于 CentOS/RHEL/Fedora 8+,强制使用 BLS即使 grub.cfg 不存在或被清空)
if not uses_bls and distro_type in ["centos", "rhel", "fedora", "rocky", "almalinux"]:
log_info(f"{distro_type} 8+ 默认使用 BLS启用 BLS 恢复")
uses_bls = True
if not uses_bls: if not uses_bls:
log_debug("系统不使用 BLS跳过 BLS 恢复") log_debug("系统不使用 BLS跳过 BLS 恢复")
return True, "" return True, ""
@@ -2103,80 +1945,6 @@ def chroot_and_repair_grub(mount_point: str, target_disk: str,
grub_install_success = False grub_install_success = False
install_errors = [] install_errors = []
# ===== 清空并重建 GRUB 环境(针对严重损坏的系统)=====
# 这个步骤在 UEFI 和 BIOS 模式下都需要
log_step("清空并重建 GRUB 环境")
# 确定正确的 GRUB 目录名
if distro_type in ["centos", "rhel", "fedora", "rocky", "almalinux", "opensuse"]:
grub_dir_name = "grub2"
else:
grub_dir_name = "grub"
# 1. 检查并恢复 GRUB 模块(从系统包中)
log_info("检查 GRUB EFI 模块...")
grub_modules_dir = os.path.join(mount_point, f"usr/lib/grub/x86_64-efi")
boot_grub_mods = os.path.join(mount_point, "boot", grub_dir_name, "x86_64-efi")
if os.path.exists(grub_modules_dir):
log_info(f"✓ 系统模块目录: {grub_modules_dir}")
# 确保 /boot/grub2/x86_64-efi 存在且包含模块
if not os.path.exists(boot_grub_mods) or len(os.listdir(boot_grub_mods)) < 10:
log_info("复制 GRUB 模块到 /boot...")
try:
import shutil
if os.path.exists(boot_grub_mods):
shutil.rmtree(boot_grub_mods)
shutil.copytree(grub_modules_dir, boot_grub_mods)
mod_count = len(os.listdir(boot_grub_mods))
log_success(f"✓ 复制 {mod_count} 个模块到 {boot_grub_mods}")
except Exception as e:
log_warning(f"复制模块失败: {e}")
# 2. 清空 /boot/grub* 目录中的配置和临时文件(保留模块和字体)
boot_grub_paths = [
os.path.join(mount_point, "boot", "grub"),
os.path.join(mount_point, "boot", "grub2"),
]
for grub_path in boot_grub_paths:
if os.path.exists(grub_path):
log_info(f"清理目录: {grub_path}")
try:
# 只删除配置文件和临时文件,保留 x86_64-efi 目录和字体
preserve_dirs = {"x86_64-efi", "i386-pc", "fonts", "locale"}
for item in os.listdir(grub_path):
if item in preserve_dirs:
continue
item_path = os.path.join(grub_path, item)
if os.path.isfile(item_path) or os.path.islink(item_path):
os.unlink(item_path)
log_debug(f" 删除文件: {item}")
elif os.path.isdir(item_path):
import shutil
shutil.rmtree(item_path)
log_debug(f" 删除目录: {item}")
log_success(f"✓ 已清理: {grub_path}")
except Exception as e:
log_warning(f"清理失败: {e}")
# 3. 创建 grubenv 文件(普通文件,不是符号链接)
# GRUB2 要求环境块精确为 4096 字节(一个扇区)
grubenv_path = os.path.join(mount_point, "boot", grub_dir_name, "grubenv")
try:
# 构建 4096 字节的 grubenv 内容
header = "# GRUB Environment Block\n"
saved_entry = "saved_entry=\n"
# 计算需要填充的字节数4096 - header - saved_entry - 结尾的 #
padding_size = 4096 - len(header) - len(saved_entry) - 1
content = header + saved_entry + "#" * padding_size
with open(grubenv_path, 'w') as f:
f.write(content)
log_success(f"✓ 创建 grubenv ({os.path.getsize(grubenv_path)} 字节): {grubenv_path}")
except Exception as e:
log_warning(f"创建 grubenv 失败: {e}")
log_success("✓ GRUB 环境重建完成")
# ===== UEFI 安装 ===== # ===== UEFI 安装 =====
if is_uefi or install_hybrid: if is_uefi or install_hybrid:
log_step("安装 UEFI GRUB") log_step("安装 UEFI GRUB")
@@ -2193,75 +1961,6 @@ def chroot_and_repair_grub(mount_point: str, target_disk: str,
else: else:
log_warning(f"✗ EFI 目录不存在: {efi_check_path}") log_warning(f"✗ EFI 目录不存在: {efi_check_path}")
# ===== 清空 EFI 分区并重建UEFI 模式特有)=====
log_step("清空 EFI 分区并重建")
# 1. 清空 EFI 分区中的 GRUB 相关目录
efi_cleanup_dirs = ["GRUB", "grub", "Boot", "boot", "centos", "redhat", "fedora"]
for efi_dir in efi_cleanup_dirs:
efi_dir_path = os.path.join(mount_point, "boot/efi/EFI", efi_dir)
if os.path.exists(efi_dir_path):
log_info(f"清空 EFI 目录: {efi_dir_path}")
try:
import shutil
shutil.rmtree(efi_dir_path)
log_success(f"✓ 已删除: {efi_dir_path}")
except Exception as e:
log_warning(f"删除失败: {e}")
# 2. 重建 EFI 目录结构
log_info("重建 EFI 目录结构...")
dirs_to_create = [
os.path.join(mount_point, "boot/efi/EFI", efi_bootloader_id),
os.path.join(mount_point, "boot/efi/EFI", "Boot"),
]
for dir_path in dirs_to_create:
try:
os.makedirs(dir_path, exist_ok=True)
log_info(f"✓ 创建目录: {dir_path}")
except Exception as e:
log_warning(f"创建目录失败: {e}")
log_success("✓ GRUB 环境重建完成")
# 5. 为独立 /boot 分区创建 EFI 辅助 grub.cfg
has_separate_boot = os.path.ismount(os.path.join(mount_point, "boot"))
if has_separate_boot:
log_info("为独立 /boot 分区创建 EFI 辅助 grub.cfg...")
# 为每个 EFI 目录创建 grub.cfg使用通用搜索方法
for efi_dir in [efi_bootloader_id, "Boot"]:
efi_cfg_dir = os.path.join(mount_point, "boot/efi/EFI", efi_dir)
if os.path.exists(efi_cfg_dir):
efi_cfg_path = os.path.join(efi_cfg_dir, "grub.cfg")
try:
# 使用 search --file 通用搜索方法
efi_cfg_content = '''# BootRepairTool - Auto-find /boot partition
# Try to find grub2/grub.cfg on any partition
if search --no-floppy --set=root --file /grub2/grub.cfg; then
set prefix=($root)/grub2
elif search --no-floppy --set=root --file /grub/grub.cfg; then
set prefix=($root)/grub
elif search --no-floppy --set=root --file /boot/grub2/grub.cfg; then
set prefix=($root)/boot/grub2
elif search --no-floppy --set=root --file /boot/grub/grub.cfg; then
set prefix=($root)/boot/grub
fi
if [ -n "$root" ] && [ -n "$prefix" ]; then
configfile ${prefix}/grub.cfg
else
echo "Error: Could not find grub.cfg on any partition."
echo "Please check your /boot partition."
fi
'''
with open(efi_cfg_path, 'w') as f:
f.write(efi_cfg_content)
log_success(f"✓ 创建 {efi_dir}/grub.cfg (通用搜索)")
except Exception as e:
log_warning(f" 创建失败: {e}")
grub_install_cmd = chroot_cmd_prefix + [ grub_install_cmd = chroot_cmd_prefix + [
_grub_install_cmd, _grub_install_cmd,
f"--target={efi_target}", f"--target={efi_target}",
@@ -2437,32 +2136,20 @@ fi
log_step("更新 GRUB 配置文件") log_step("更新 GRUB 配置文件")
# 确定配置文件路径 # 确定配置文件路径
# 根据发行版类型确定正确的 GRUB 目录名
if distro_type in ["centos", "rhel", "fedora", "rocky", "almalinux", "opensuse"]:
# RHEL 系使用 grub2
preferred_grub_dir = "grub2"
fallback_grub_dir = "grub"
else:
# Debian/Ubuntu/Arch 等使用 grub
preferred_grub_dir = "grub"
fallback_grub_dir = "grub2"
possible_config_paths = [ possible_config_paths = [
f"/boot/{preferred_grub_dir}/grub.cfg", "/boot/grub/grub.cfg",
f"/boot/{fallback_grub_dir}/grub.cfg", "/boot/grub2/grub.cfg",
] ]
config_path = "" config_path = ""
for cfg_path in possible_config_paths: for cfg_path in possible_config_paths:
full_path = os.path.join(mount_point, cfg_path.lstrip('/')) full_path = os.path.join(mount_point, cfg_path.lstrip('/'))
# 优先检查目录是否存在,但也考虑发行版偏好
if os.path.exists(os.path.dirname(full_path)): if os.path.exists(os.path.dirname(full_path)):
config_path = cfg_path config_path = cfg_path
break break
# 如果没有找到,使用发行版偏好路径
if not config_path: if not config_path:
config_path = f"/boot/{preferred_grub_dir}/grub.cfg" config_path = "/boot/grub/grub.cfg"
# 确定更新命令 # 确定更新命令
if distro_type in ["debian", "ubuntu"]: if distro_type in ["debian", "ubuntu"]:
@@ -2478,188 +2165,6 @@ fi
log_info(f"使用命令: {' '.join(grub_update_cmd)}") log_info(f"使用命令: {' '.join(grub_update_cmd)}")
log_info(f"配置文件路径: {config_path}") log_info(f"配置文件路径: {config_path}")
# ===== 确保 grubenv 文件存在 =====
# CentOS/RHEL/Fedora 等系统需要 grubenv 文件,否则 grub2-mkconfig 会失败
grubenv_dir = os.path.dirname(config_path) # /boot/grub2 或 /boot/grub
grubenv_path = os.path.join(mount_point, grubenv_dir.lstrip('/'), "grubenv")
if not os.path.exists(grubenv_path):
log_info(f"grubenv 文件不存在,尝试创建...")
# 首先确保目录存在(在 chroot 环境中创建)
grubenv_dir_in_chroot = os.path.dirname(config_path) # /boot/grub2 或 /boot/grub
success, _, _ = run_command(
chroot_cmd_prefix + ["mkdir", "-p", grubenv_dir_in_chroot],
"创建 GRUB 配置目录",
timeout=5
)
if success:
log_info(f"✓ 创建目录: {grubenv_dir_in_chroot}")
# 检查 /boot 是否已挂载(在 chroot 内)
success, stdout, _ = run_command(
chroot_cmd_prefix + ["mount"],
"检查挂载状态",
timeout=5
)
if success:
boot_mounts = [line for line in stdout.split('\n') if 'boot' in line.lower()]
if boot_mounts:
log_info(f"/boot 挂载状态: {', '.join(boot_mounts)}")
else:
log_info("/boot 挂载状态: 未找到相关挂载")
else:
log_info("/boot 挂载状态: 检查失败")
# 方法1: 使用 grub2-editenv 创建
editenv_cmd = None
for cmd_name in ["grub2-editenv", "grub-editenv"]:
for cmd_dir in ["/usr/bin", "/bin", "/usr/sbin", "/sbin"]:
test_path = os.path.join(mount_point, cmd_dir.lstrip('/'), cmd_name)
if os.path.exists(test_path):
editenv_cmd = os.path.join(cmd_dir, cmd_name)
break
if editenv_cmd:
break
if editenv_cmd:
success, _, stderr = run_command(
chroot_cmd_prefix + [editenv_cmd, grubenv_dir_in_chroot + "/grubenv", "create"],
"创建 grubenv 文件",
timeout=10
)
# 重新检查文件是否存在(可能在不同路径)
if success:
log_success(f"✓ 成功创建 grubenv 文件")
else:
log_warning(f"grub2-editenv 创建失败: {stderr}")
# 方法2: 如果 /boot 是独立分区,尝试在挂载的主机路径创建
if not os.path.exists(grubenv_path):
# 根据发行版确定正确的目录名 (grub2 vs grub)
if distro_type in ["centos", "rhel", "fedora", "rocky", "almalinux", "opensuse"]:
grub_dir_name = "grub2"
else:
grub_dir_name = "grub"
# 首先确保首选目录存在(使用 shell 命令,因为 /boot 可能是独立分区)
preferred_dir = os.path.dirname(grubenv_path) # 正确路径的目录
# 使用 mkdir 命令创建目录(在 chroot 内,确保在正确的文件系统中)
grubenv_dir_in_chroot = os.path.dirname(config_path) # /boot/grub2 或 /boot/grub
success, _, _ = run_command(
chroot_cmd_prefix + ["mkdir", "-p", grubenv_dir_in_chroot],
f"确保 {grubenv_dir_in_chroot} 目录存在",
timeout=5
)
if success:
log_info(f"✓ 确保目录存在: {grubenv_dir_in_chroot}")
# 再次检查 grubenv_path主机路径
log_debug(f"检查 grubenv 路径: {grubenv_path}, 存在: {os.path.exists(grubenv_path)}")
log_debug(f"检查目录存在: {preferred_dir}, 存在: {os.path.exists(preferred_dir)}")
# 尝试多种可能的路径,检查是否已有 grubenv 在错误位置
possible_wrong_paths = [
os.path.join(mount_point, "boot", "grub" if grub_dir_name == "grub2" else "grub2", "grubenv"),
os.path.join(mount_point, "grub" if grub_dir_name == "grub2" else "grub2", "grubenv"),
]
wrong_path_found = None
for wrong_path in possible_wrong_paths:
if os.path.exists(wrong_path):
wrong_path_found = wrong_path
log_info(f"发现 grubenv 在错误位置: {wrong_path}")
break
# 如果找到错误位置的 grubenv复制到正确位置
if wrong_path_found and not os.path.exists(grubenv_path):
try:
import shutil
# 确保目标目录存在(在主机上)
os.makedirs(os.path.dirname(grubenv_path), exist_ok=True)
shutil.copy2(wrong_path_found, grubenv_path)
log_success(f"✓ 复制 grubenv 到正确位置: {grubenv_path}")
except Exception as e:
log_warning(f"复制失败: {e}")
# 检查是否是符号链接CentOS 8 等系统的 grubenv 可能是指向 EFI 分区的符号链接)
if not os.path.exists(grubenv_path) and os.path.islink(grubenv_path):
log_info(f"检测到 grubenv 是符号链接,尝试创建链接目标...")
try:
link_target = os.readlink(grubenv_path)
log_info(f" 链接目标: {link_target}")
# 解析链接目标为绝对路径
if link_target.startswith('/'):
# 绝对路径
target_path = os.path.join(mount_point, link_target.lstrip('/'))
else:
# 相对路径(如 ../efi/EFI/centos/grubenv
link_dir = os.path.dirname(grubenv_path)
target_path = os.path.normpath(os.path.join(link_dir, link_target))
log_info(f" 解析后的目标路径: {target_path}")
# 创建目标目录
target_dir = os.path.dirname(target_path)
if not os.path.exists(target_dir):
os.makedirs(target_dir, exist_ok=True)
log_info(f"✓ 创建目录: {target_dir}")
# 创建目标文件(精确 4096 字节的 GRUB 环境块)
header = "# GRUB Environment Block\n"
saved_entry = "saved_entry=\n"
padding_size = 4096 - len(header) - len(saved_entry) - 1
grubenv_content = header + saved_entry + "#" * padding_size
with open(target_path, 'w') as f:
f.write(grubenv_content)
log_success(f"✓ 创建符号链接目标文件: {target_path}")
except Exception as e:
log_warning(f"创建符号链接目标失败: {e}")
# 如果创建符号链接目标失败,删除损坏的符号链接并创建普通文件
try:
os.unlink(grubenv_path)
log_info(f" 已删除损坏的符号链接: {grubenv_path}")
except Exception as e2:
log_warning(f" 删除符号链接失败: {e2}")
# 如果仍未创建成功,使用 shell 命令在 chroot 内创建
if not os.path.exists(grubenv_path):
log_info(f"尝试使用 shell 命令创建 grubenv 文件...")
# 方法: 使用 echo 和 tee 在 chroot 内创建文件
grubenv_chroot_path = grubenv_dir_in_chroot + "/grubenv"
# 精确 4096 字节的 grubenv
header = "# GRUB Environment Block\n"
saved_entry = "saved_entry=\n"
padding_size = 4096 - len(header) - len(saved_entry) - 1
grubenv_content = header + saved_entry + "#" * padding_size
# 使用 printf 创建文件(更可靠)
success, _, stderr = run_command(
chroot_cmd_prefix + ["sh", "-c", f'printf "%s" "{grubenv_content}" > "{grubenv_chroot_path}"'],
"使用 shell 创建 grubenv 文件",
timeout=5
)
if success or os.path.exists(grubenv_path):
log_success(f"✓ 手动创建 grubenv 文件成功: {grubenv_path}")
else:
log_warning(f"Shell 创建失败: {stderr}")
# 最后尝试: 使用 touch 创建空文件
success, _, _ = run_command(
chroot_cmd_prefix + ["touch", grubenv_chroot_path],
"创建空 grubenv 文件",
timeout=5
)
if success or os.path.exists(grubenv_path):
log_success(f"✓ 创建空 grubenv 文件成功: {grubenv_path}")
else:
log_warning(f"所有方法创建 grubenv 文件均失败")
# 执行配置更新 # 执行配置更新
success, stdout, stderr = run_command( success, stdout, stderr = run_command(
chroot_cmd_prefix + grub_update_cmd, chroot_cmd_prefix + grub_update_cmd,
@@ -2669,163 +2174,7 @@ fi
if not success: if not success:
log_error(f"GRUB 配置文件更新失败: {stderr}") log_error(f"GRUB 配置文件更新失败: {stderr}")
return False, f"GRUB配置文件更新失败: {stderr}"
# 备选方案:手动创建基本 grub.cfg
log_step("尝试手动创建基本 grub.cfg")
# 获取根分区信息
root_part = None
root_uuid = None
try:
# 从 /etc/fstab 获取根分区
fstab_path = os.path.join(mount_point, "etc/fstab")
if os.path.exists(fstab_path):
with open(fstab_path, 'r') as f:
for line in f:
if line.startswith('#') or not line.strip():
continue
parts = line.split()
if len(parts) >= 2 and parts[1] == '/':
root_part = parts[0]
break
# 获取 UUID
if root_part and root_part.startswith('UUID='):
root_uuid = root_part[5:].strip('"')
elif root_part and root_part.startswith('/dev/'):
success_uuid, uuid_out, _ = run_command(
["sudo", "blkid", "-s", "UUID", "-o", "value", root_part],
f"获取 {root_part} 的 UUID",
timeout=10
)
if success_uuid:
root_uuid = uuid_out.strip()
except Exception as e:
log_warning(f"获取根分区信息失败: {e}")
# 查找内核文件
boot_dir = os.path.join(mount_point, "boot")
kernel_files = []
initramfs_files = []
try:
for f in os.listdir(boot_dir):
if f.startswith('vmlinuz-'):
kernel_files.append(f)
elif f.startswith('initramfs-') and f.endswith('.img') and 'kdump' not in f:
initramfs_files.append(f)
kernel_files.sort(reverse=True)
initramfs_files.sort(reverse=True)
except Exception as e:
log_warning(f"查找内核文件失败: {e}")
if kernel_files and root_uuid:
# 检测是否有独立 /boot 分区
boot_uuid = None
try:
boot_mount_point = os.path.join(mount_point, "boot")
if os.path.ismount(boot_mount_point):
# 有独立 /boot 分区,获取其 UUID
success_uuid, uuid_out, _ = run_command(
["sudo", "blkid", "-s", "UUID", "-o", "value", boot_mount_point],
"获取 /boot 分区 UUID",
timeout=10
)
if success_uuid:
boot_uuid = uuid_out.strip()
log_info(f"检测到独立 /boot 分区UUID: {boot_uuid}")
except Exception as e:
log_warning(f"获取 /boot UUID 失败: {e}")
# 创建基本 grub.cfg
if boot_uuid:
# 独立 /boot 分区配置 - 需要先搜索 /boot 分区
grub_cfg_content = f"""# Minimal GRUB config generated by BootRepairTool
# For system with separate /boot partition
set timeout=5
set default=0
# Load necessary modules
insmod part_gpt
insmod xfs
insmod fat
# Search for the /boot partition by UUID, or try search by file as fallback
if search --no-floppy --fs-uuid --set=root {boot_uuid}; then
set prefix=($root)/grub2
else
# Fallback: search by file
search --no-floppy --set=root --file /grub2/grub.cfg
set prefix=($root)/grub2
fi
menuentry 'CentOS Linux (Auto)' {{
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod xfs
linux /{kernel_files[0]} root=UUID={root_uuid} ro rhgb quiet
initrd /{initramfs_files[0] if initramfs_files else ''}
}}
menuentry 'CentOS Linux (Rescue)' {{
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod xfs
linux /{kernel_files[0]} root=UUID={root_uuid} ro rescue
initrd /{initramfs_files[0] if initramfs_files else ''}
}}
"""
else:
# 无独立 /boot 分区配置
grub_cfg_content = f"""# Minimal GRUB config generated by BootRepairTool
set timeout=5
set default=0
# Load necessary modules
insmod part_gpt
insmod xfs
insmod fat
# Search for the root partition by UUID
search --no-floppy --fs-uuid --set=root {root_uuid}
menuentry 'CentOS Linux (Auto)' {{
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod xfs
linux /{kernel_files[0]} root=UUID={root_uuid} ro rhgb quiet
initrd /{initramfs_files[0] if initramfs_files else ''}
}}
menuentry 'CentOS Linux (Rescue)' {{
load_video
set gfxpayload=keep
insmod gzio
insmod part_gpt
insmod xfs
linux /{kernel_files[0]} root=UUID={root_uuid} ro rescue
initrd /{initramfs_files[0] if initramfs_files else ''}
}}
"""
try:
with open(full_config_path, 'w') as f:
f.write(grub_cfg_content)
log_success(f"✓ 手动创建 grub.cfg: {full_config_path}")
log_info(f" 内核: {kernel_files[0]}")
log_info(f" UUID: {root_uuid}")
success = True
except Exception as e:
log_error(f"手动创建 grub.cfg 失败: {e}")
return False, f"GRUB配置失败: {stderr}"
else:
return False, f"GRUB配置失败无法获取内核或UUID"
log_success("✓ GRUB 配置文件更新成功") log_success("✓ GRUB 配置文件更新成功")
@@ -2839,30 +2188,10 @@ menuentry 'CentOS Linux (Rescue)' {{
content = f.read() content = f.read()
menu_entries = content.count('menuentry') menu_entries = content.count('menuentry')
log_info(f" 发现 {menu_entries} 个启动菜单项") log_info(f" 发现 {menu_entries} 个启动菜单项")
# 验证配置文件是否有效
if size < 1000:
log_warning(f"配置文件太小 ({size} 字节),可能有问题")
if menu_entries == 0:
log_warning("配置文件中没有找到启动菜单项!")
log_info("尝试重新生成配置...")
# 再次尝试生成配置
success2, stdout2, stderr2 = run_command(
chroot_cmd_prefix + grub_update_cmd,
"重新生成GRUB配置",
timeout=120
)
if success2:
# 重新读取
with open(full_config_path, 'r') as f2:
content2 = f2.read()
menu_entries2 = content2.count('menuentry')
log_info(f"重新生成后: {len(content2)} 字节, {menu_entries2} 个菜单项")
except Exception as e: except Exception as e:
log_warning(f" 无法读取配置文件: {e}") log_warning(f" 无法读取配置文件: {e}")
else: else:
log_warning(f" 配置文件未找到: {full_config_path}") log_warning(f" 配置文件未找到: {full_config_path}")
return False, f"grub.cfg 文件未生成: {full_config_path}"
# ===== 输出启动提示 ===== # ===== 输出启动提示 =====
if is_uefi or install_hybrid: if is_uefi or install_hybrid:

143
build.py Executable file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
BootRepairTool 打包脚本
使用 Python 3.9 + PyInstaller 打包
"""
import sys
import os
import subprocess
import shutil
# 需要的 Python 版本
REQUIRED_PYTHON_VERSION = (3, 9)
def check_python_version():
"""检查 Python 版本是否为 3.9"""
current = sys.version_info[:2]
if current != REQUIRED_PYTHON_VERSION:
print(f"错误: 需要使用 Python {REQUIRED_PYTHON_VERSION[0]}.{REQUIRED_PYTHON_VERSION[1]}")
print(f"当前版本: Python {current[0]}.{current[1]}")
print("\n请使用以下命令运行:")
print(f" python3.9 {sys.argv[0]}")
return False
print(f"✓ Python 版本检查通过: {sys.version.split()[0]}")
return True
def install_dependencies():
"""安装打包所需的依赖"""
print("\n[1/4] 安装依赖...")
deps = ["pyinstaller"]
for dep in deps:
print(f" 安装 {dep}...")
try:
subprocess.check_call([sys.executable, "-m", "pip", "install", "-q", dep])
print(f"{dep} 安装成功")
except subprocess.CalledProcessError as e:
print(f"{dep} 安装失败: {e}")
return False
return True
def clean_build():
"""清理之前的构建文件"""
print("\n[2/4] 清理构建文件...")
dirs_to_remove = ["build", "dist"]
files_to_remove = ["*.pyc", "*.spec"]
for d in dirs_to_remove:
if os.path.exists(d):
shutil.rmtree(d)
print(f" 删除 {d}/")
# 清理 __pycache__
for root, dirs, files in os.walk("."):
if "__pycache__" in dirs:
pycache_path = os.path.join(root, "__pycache__")
shutil.rmtree(pycache_path)
print(f" 删除 {pycache_path}/")
print(" ✓ 清理完成")
def build_executable():
"""使用 PyInstaller 打包"""
print("\n[3/4] 开始打包...")
# 打包命令
cmd = [
sys.executable, "-m", "PyInstaller",
"--name=LinuxGrubRepair",
"--onefile",
"--console",
"--clean",
"--noconfirm",
"frontend.py"
]
print(f" 执行: {' '.join(cmd)}")
try:
subprocess.check_call(cmd)
print(" ✓ 打包成功")
return True
except subprocess.CalledProcessError as e:
print(f" ✗ 打包失败: {e}")
return False
def show_result():
"""显示打包结果"""
print("\n[4/4] 打包完成!")
dist_path = os.path.join(os.getcwd(), "dist")
exe_name = "LinuxGrubRepair"
exe_path = os.path.join(dist_path, exe_name)
if os.path.exists(exe_path):
size = os.path.getsize(exe_path)
size_mb = size / (1024 * 1024)
print(f"\n 可执行文件: {exe_path}")
print(f" 文件大小: {size_mb:.2f} MB")
print("\n 使用方式:")
print(f" sudo ./{exe_name}")
else:
print(" 未找到生成的可执行文件")
def main():
"""主函数"""
print("=" * 50)
print("BootRepairTool 打包脚本")
print("=" * 50)
# 检查 Python 版本
if not check_python_version():
sys.exit(1)
# 安装依赖
if not install_dependencies():
print("依赖安装失败")
sys.exit(1)
# 清理旧构建
clean_build()
# 打包
if not build_executable():
print("打包失败")
sys.exit(1)
# 显示结果
show_result()
if __name__ == "__main__":
main()

BIN
dist/LinuxGrubRepair vendored Executable file

Binary file not shown.

View File

@@ -13,11 +13,11 @@ class GrubRepairApp:
def __init__(self, master): def __init__(self, master):
self.master = master self.master = master
master.title("Linux GRUB 引导修复工具 v2.0") master.title("Linux GRUB 引导修复工具 v2.0")
master.geometry("900x750") master.geometry("643x750")
master.resizable(True, True) master.resizable(True, True)
# 设置窗口最小大小 # 设置窗口最小大小
master.minsize(800, 600) master.minsize(643, 600)
self.mount_point = None self.mount_point = None
self.selected_root_partition_info = None self.selected_root_partition_info = None

40
linuxgrubrepair.spec Normal file
View File

@@ -0,0 +1,40 @@
# -*- mode: python ; coding: utf-8 -*-
block_cipher = None
a = Analysis(['frontend.py'],
pathex=[],
binaries=[],
datas=[],
hiddenimports=[],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
win_no_prefer_redirects=False,
win_private_assemblies=False,
cipher=block_cipher,
noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
cipher=block_cipher)
exe = EXE(pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
[],
name='linuxgrubrepair',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
upx_exclude=[],
runtime_tmpdir=None,
console=True,
disable_windowed_traceback=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None )