diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82f4bb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/build/ +/__pycache__/ diff --git a/LinuxGrubRepair.spec b/LinuxGrubRepair.spec new file mode 100644 index 0000000..8c1fb34 --- /dev/null +++ b/LinuxGrubRepair.spec @@ -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, +) diff --git a/__pycache__/backend.cpython-39.pyc b/__pycache__/backend.cpython-39.pyc deleted file mode 100644 index 6b5029b..0000000 Binary files a/__pycache__/backend.cpython-39.pyc and /dev/null differ diff --git a/__pycache__/frontend.cpython-39.pyc b/__pycache__/frontend.cpython-39.pyc deleted file mode 100644 index 8ea6667..0000000 Binary files a/__pycache__/frontend.cpython-39.pyc and /dev/null differ diff --git a/backend.py b/backend.py index 5d6d42f..efa6ae4 100644 --- a/backend.py +++ b/backend.py @@ -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" # 构建 chroot 内的输出路径 - chroot_output_path = f"/boot/efi/EFI/{bootloader_id}/{efi_grub_file}" - - # 检测是否有独立的 /boot 分区 - has_separate_boot = os.path.ismount(os.path.join(mount_point, "boot")) - - # 确定正确的 prefix - # 对于独立 /boot 分区,使用 '()/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}") + chroot_output_path = f"/boot/efi/EFI/{efi_bootloader_id}/{efi_grub_file}" # 尝试使用 grub-mkimage 生成 EFI 文件 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", "-o", chroot_output_path, "-O", efi_target, - "-p", grub_prefix, + "-p", "/boot/grub2", ] + modules.split() 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('/')) if success and os.path.exists(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.cfg(GRUB 首先查找这里) - 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: log_error(f"生成 EFI 文件失败: {stderr}") return False @@ -1515,54 +1454,6 @@ def install_efi_fallback(mount_point: str, efi_target: str, efi_grub_file: str, try: shutil.copy2(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 except Exception as 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"]: # 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( - chroot_cmd_prefix + ["mkinitcpio", "-p", kernel_ver], - f"生成 initramfs for {kernel_ver}", - 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}") + success, _, stderr = run_command( + chroot_cmd_prefix + ["mkinitcpio", "-p", kernel_ver], + f"生成 initramfs for {kernel_ver}", + timeout=120 + ) else: # 通用方法:尝试 dracut success, _, stderr = run_command( chroot_cmd_prefix + ["dracut", "-f", f"/boot/initramfs-{kernel_ver}.img", kernel_ver], f"生成 initramfs for {kernel_ver}", timeout=120 - ) if success: @@ -1829,11 +1676,6 @@ def restore_bls_entries(mount_point: str, distro_type: str) -> Tuple[bool, str]: except Exception as 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: log_debug("系统不使用 BLS,跳过 BLS 恢复") return True, "" @@ -2103,80 +1945,6 @@ def chroot_and_repair_grub(mount_point: str, target_disk: str, grub_install_success = False 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 安装 ===== if is_uefi or install_hybrid: log_step("安装 UEFI GRUB") @@ -2193,75 +1961,6 @@ def chroot_and_repair_grub(mount_point: str, target_disk: str, else: 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, f"--target={efi_target}", @@ -2437,32 +2136,20 @@ fi 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 = [ - f"/boot/{preferred_grub_dir}/grub.cfg", - f"/boot/{fallback_grub_dir}/grub.cfg", + "/boot/grub/grub.cfg", + "/boot/grub2/grub.cfg", ] config_path = "" for cfg_path in possible_config_paths: full_path = os.path.join(mount_point, cfg_path.lstrip('/')) - # 优先检查目录是否存在,但也考虑发行版偏好 if os.path.exists(os.path.dirname(full_path)): config_path = cfg_path break - # 如果没有找到,使用发行版偏好路径 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"]: @@ -2478,188 +2165,6 @@ fi log_info(f"使用命令: {' '.join(grub_update_cmd)}") 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( chroot_cmd_prefix + grub_update_cmd, @@ -2669,163 +2174,7 @@ fi if not success: log_error(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" + return False, f"GRUB配置文件更新失败: {stderr}" log_success("✓ GRUB 配置文件更新成功") @@ -2839,30 +2188,10 @@ menuentry 'CentOS Linux (Rescue)' {{ content = f.read() menu_entries = content.count('menuentry') 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: log_warning(f" 无法读取配置文件: {e}") else: log_warning(f" 配置文件未找到: {full_config_path}") - return False, f"grub.cfg 文件未生成: {full_config_path}" # ===== 输出启动提示 ===== if is_uefi or install_hybrid: diff --git a/build.py b/build.py new file mode 100755 index 0000000..6c74985 --- /dev/null +++ b/build.py @@ -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() diff --git a/dist/LinuxGrubRepair b/dist/LinuxGrubRepair new file mode 100755 index 0000000..73cd65c Binary files /dev/null and b/dist/LinuxGrubRepair differ diff --git a/frontend.py b/frontend.py index d6c1c53..d5c7480 100644 --- a/frontend.py +++ b/frontend.py @@ -13,11 +13,11 @@ class GrubRepairApp: def __init__(self, master): self.master = master master.title("Linux GRUB 引导修复工具 v2.0") - master.geometry("900x750") + master.geometry("643x750") master.resizable(True, True) # 设置窗口最小大小 - master.minsize(800, 600) + master.minsize(643, 600) self.mount_point = None self.selected_root_partition_info = None diff --git a/linuxgrubrepair.spec b/linuxgrubrepair.spec new file mode 100644 index 0000000..8a5348b --- /dev/null +++ b/linuxgrubrepair.spec @@ -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 )