builder_peppermint_void/builder/core/bootloaders.py
2025-04-29 18:28:13 +00:00

661 lines
33 KiB
Python

"""
SPDX-FileCopyrightText: 2023-2025 PeppermintOS Team
(peppermintosteam@proton.me)
SPDX-License-Identifier: GPL-3.0-or-later
This module handles the creation of bootloaders for different architectures and boot methods.
Credits:
- PeppermintOS Team (peppermintosteam@proton.me) - Development and maintenance of the project.
License:
This code is distributed under the GNU General Public License version 3 or later (GPL-3.0-or-later).
For more details, please refer to the LICENSE file included in the project or visit:
https://www.gnu.org/licenses/gpl-3.0.html
"""
import os
import shutil
import subprocess
import tempfile
import logging
import sys
import contextlib
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, BASE_DIR)
try:
from builder.configs import logger_config
from builder.core.copy_customizations import copy_grub_theme
except ImportError as e:
print(f"Error importing necessary modules: {e}. Ensure your environment is set up correctly. Details: {e}")
sys.exit(1)
logger = logger_config.setup_logger('bootloaders')
def create_isolinux_boot(architecture, kernel_version, initrd_path, boot_title, paths, keymap="us", locale="en_US.UTF-8", boot_cmdline="", Volume_ID=""):
"""Generates Isolinux bootloader for BIOS."""
logger.info("=> Starting Isolinux bootloader generation...")
isolinux_dir = os.path.join(paths['IMAGEDIR'], 'boot', 'isolinux')
boot_dir = os.path.join(paths['IMAGEDIR'], 'boot')
os.makedirs(isolinux_dir, exist_ok=True)
boot_cat_path = os.path.join(isolinux_dir, "boot.cat")
open(boot_cat_path, 'w').close()
logger.info(f"=> Created empty boot.cat file: {boot_cat_path} (will be populated by mkisofs/xorriso)")
syslinux_data_dir = os.path.join(paths['PEPTARGETDIR'], "usr/lib/syslinux")
SPLASH_DIR = paths['SPLASH_DIR']
TEMPLATES_DIR = paths['TEMPLATES_DIR']
splash_image_path = os.path.join(SPLASH_DIR, "splash.png")
logger.info(f"=> Destination Isolinux directory: {isolinux_dir}")
logger.info(f"=> Syslinux data source directory: {syslinux_data_dir}")
logger.info(f"=> Splash image path: {splash_image_path}")
syslinux_files = [
"isolinux.bin", "ldlinux.c32", "libcom32.c32", "vesamenu.c32",
"libutil.c32", "chain.c32", "reboot.c32", "poweroff.c32"
]
logger.info("=> Copying Syslinux files...")
for file in syslinux_files:
source_file = os.path.join(syslinux_data_dir, file)
destination_file = os.path.join(isolinux_dir, file)
if os.path.exists(source_file):
shutil.copy2(source_file, destination_file)
logger.debug(f" Copied: {file}")
else:
logger.warning(f" Syslinux file not found: {source_file}. Ignoring.")
logger.info("=> Copying and configuring isolinux.cfg...")
isolinux_cfg_in_path = os.path.join(TEMPLATES_DIR, "isolinux", "isolinux.cfg.in")
isolinux_cfg_path = os.path.join(isolinux_dir, "isolinux.cfg")
try:
with open(isolinux_cfg_in_path, 'r') as f_in:
isolinux_cfg_content = f_in.read()
replacements = {
"@@SPLASHIMAGE@@": os.path.basename(splash_image_path),
"@@KERNVER@@": kernel_version,
"@@INITRD@@": os.path.basename(initrd_path),
"@@KEYMAP@@": keymap,
"@@ARCH@@": architecture,
"@@LOCALE@@": locale,
"@@BOOT_TITLE@@": boot_title,
"@@BOOT_CMDLINE@@": boot_cmdline,
"@@Volume_ID@@": Volume_ID
}
kernel_image_name = "vmlinuz" if architecture.startswith('x86') or architecture.startswith('i686') else "vmlinux"
replacements["@@KERNELIMAGE@@"] = kernel_image_name
for placeholder, value in replacements.items():
isolinux_cfg_content = isolinux_cfg_content.replace(placeholder, value)
with open(isolinux_cfg_path, 'w') as f_out:
f_out.write(isolinux_cfg_content)
logger.debug(f" isolinux.cfg file created at: {isolinux_cfg_path}")
except FileNotFoundError:
logger.error(f" isolinux.cfg.in template file not found: {isolinux_cfg_in_path}. Failed to create isolinux.cfg")
return None
except Exception as e:
logger.error(f" Error processing or creating isolinux.cfg: {e}")
return None
logger.info("=> Copying splash image...")
if os.path.exists(splash_image_path):
splash_destination_path = os.path.join(isolinux_dir, os.path.basename(splash_image_path))
shutil.copy2(splash_image_path, splash_destination_path)
logger.debug(f" Splash image copied to: {splash_destination_path}")
else:
logger.warning(f" Splash image not found: {splash_image_path}. Ignoring.")
memtest_files = {
"memtest.bin": os.path.join(paths['PEPTARGETDIR'], "boot", "memtest86+", "memtest.bin"),
"memtest.efi": os.path.join(paths['PEPTARGETDIR'], "boot", "memtest86+", "memtest.efi")
}
for filename, source_path in memtest_files.items():
dest_path = os.path.join(boot_dir, filename)
if os.path.exists(source_path):
logger.info(f"=> Copying {filename}...")
shutil.copy2(source_path, dest_path)
logger.debug(f" {filename} copied to: {dest_path}")
else:
logger.info(f"=> {filename} not found in PEPTARGETDIR. Ignoring.")
logger.info("=> Isolinux bootloader generated successfully.")
return isolinux_dir
def create_grub_efi_boot(architecture, kernel_version, initrd_path, boot_title, iso_build_config, repositories_data, paths, keymap="us", locale="en_US.UTF-8", boot_cmdline="", Volume_ID="", config=None, logger_instance=None, pep_target_path=None, splash_image=None):
"""
Generates GRUB EFI bootloader for UEFI systems.
Args:
architecture (str): Target architecture (e.g., 'x86_64', 'aarch64').
kernel_version (str): The full kernel version string (e.g., '6.1.130_1').
initrd_path (str): The path to the generated initramfs file (in the ISO BOOT directory).
boot_title (str): The title for the boot menu entry.
iso_build_config (dict): Dictionary containing ISO build configuration.
repositories_data (list): List of repository dictionaries.
paths (dict): Dictionary containing all build paths (IMAGEDIR, PEPTARGETDIR, etc.).
keymap (str, optional): Keymap to use (default: 'us').
locale (str, optional): Locale to use (default: 'en_US.UTF-8').
boot_cmdline (str, optional): Additional kernel command line arguments.
Volume_ID (str, optional): Volume ID for the ISO filesystem.
config (dict, optional): General configuration dictionary.
logger_instance (logging.Logger, optional): Logger instance to use.
pep_target_path (str, optional): Path to the PEPTARGETDIR (redundant if paths is used).
splash_image (str, optional): Path to the splash image (redundant if paths is used).
"""
# Use the passed logger instance if available, otherwise use the module logger
logger = logger_instance if logger_instance is not None else logging.getLogger('bootloaders')
logger.info("=> Starting GRUB EFI bootloader generation...")
# Use paths dictionary passed as argument for all path-related operations
grub_dir = os.path.join(paths['IMAGEDIR'], 'boot', 'grub')
pep_target_path = paths['PEPTARGETDIR'] # Use path from dictionary
iso_image_boot_dir = os.path.join(paths['IMAGEDIR'], 'boot') # Use path from dictionary
grub_data_dir = os.path.join(pep_target_path, "usr/share/grub") # Use path from dictionary
TEMPLATES_DIR = paths['TEMPLATES_DIR'] # Use path from dictionary
SPLASH_DIR = paths['SPLASH_DIR'] # Use path from dictionary
# SPLASH_THEMES_DIR = os.path.join(BASE_DIR, "peppermint/grub/themes") # This path seems relative to BASE_DIR, ensure it's correct
SPLASH_THEMES_DIR = os.path.join(paths['BUILDDIR'], "peppermint", "grub", "themes") # Assuming peppermint/grub/themes is within BUILDDIR or similar
os.makedirs(grub_dir, exist_ok=True)
# Copy GRUB theme - assuming copy_grub_theme is defined elsewhere
# The destination should be within the ISO's boot directory structure
copy_grub_theme(SPLASH_THEMES_DIR, os.path.join(paths['IMAGEDIR'], 'boot'))
# 1. Copy additional files from TEMPLATES_DIR
logger.info("=> Copying additional template files to the GRUB directory...")
# Source directory for templates
templates_grub_dir = os.path.join(TEMPLATES_DIR, "grub")
# List of files to copy (grub.cfg will be generated, not copied directly as a template)
# We will generate grub.cfg content later.
files_to_copy = [] # No files to copy directly, grub.cfg is generated
# If you have other static files in templates_grub_dir to copy, list them here
# for filename in files_to_copy:
# source_path = os.path.join(templates_grub_dir, filename)
# dest_path = os.path.join(grub_dir, filename)
# if os.path.exists(source_path):
# shutil.copy2(source_path, dest_path)
# logger.debug(f" Copied template file: {filename} to: {dest_path}")
# else:
# logger.warning(f" Template file not found: {filename} in: {source_path}")
# Copy splash image from SPLASH_DIR to grub_dir
# The splash image needs to be accessible by GRUB, usually in the same dir as grub.cfg or themes dir
splash_source = os.path.join(SPLASH_DIR, "splash.png")
splash_dest = os.path.join(grub_dir, "splash.png") # Copy to grub_dir
if os.path.exists(splash_source):
shutil.copy2(splash_source, splash_dest)
logger.debug(f" Copied splash image from: {splash_source} to: {splash_dest}")
else:
logger.warning(f" Splash image not found in: {splash_source}")
# Determine kernel image name based on architecture
kernel_img_name = "vmlinuz"
want_memtest = True # Memtest is typically for x86
if architecture == "aarch64":
kernel_img_name = "vmlinux"
want_memtest = False # Memtest86+ is for x86
elif architecture == "x86_64":
kernel_img_name = "vmlinuz"
want_memtest = True # Memtest86+ is for x86
elif architecture == "i686":
kernel_img_name = "vmlinuz"
want_memtest = True # Memtest86+ is for x86
logger.info(f"=> Destination GRUB directory: {grub_dir}")
logger.info(f"=> GRUB data source directory: {grub_data_dir}")
logger.info(f"=> Kernel image name for GRUB config: {kernel_img_name}")
logger.info(f"=> Include memtest86+ in GRUB config: {want_memtest}")
# 1. Prepare GRUB directory and copy font (Moved this up)
logger.info("=> Preparing GRUB directory and copying fonts...")
os.makedirs(os.path.join(grub_dir, "fonts"), exist_ok=True)
unicode_pf2_source = os.path.join(grub_data_dir, "unicode.pf2")
unicode_pf2_dest = os.path.join(grub_dir, "fonts", "unicode.pf2")
if os.path.exists(unicode_pf2_source):
shutil.copy2(unicode_pf2_source, unicode_pf2_dest)
logger.debug(f" Copied unicode.pf2 font to: {unicode_pf2_dest}")
else:
logger.warning(f" unicode.pf2 font not found in: {unicode_pf2_source}. Ignoring.")
# 2. Create EFI vfat image
# This image will contain the EFI bootloader (BOOT{arch}.EFI) and grub.cfg
logger.info("=> Creating EFI vfat image (efiboot.img)...")
efi_img_path = os.path.join(grub_dir, "efiboot.img")
efi_img_size_mb = 32 # Size in MB
try:
# Use a fixed size for the EFI image
subprocess.run(["truncate", "-s", f"{efi_img_size_mb}M", efi_img_path], check=True)
# Format as VFAT, label it
subprocess.run(["mkfs.vfat", "-F12", "-S", "512", "-n", "grub_uefi", efi_img_path], check=True)
logger.debug(f" EFI vfat image created: {efi_img_path} with size {efi_img_size_mb}MB")
except subprocess.CalledProcessError as e:
logger.error(f" Error creating EFI vfat image: {e.stderr.decode('utf-8', errors='ignore')}")
return None
except FileNotFoundError as e:
logger.error(f" Command not found: {e}. Make sure truncate and mkfs.vfat are installed on the host.")
return None
except Exception as e:
logger.error(f" Unexpected error during EFI vfat image creation: {e}")
return None
# 3. Mount vfat image and build GRUB image function
grub_efi_tmpdir = None # Initialize to None
loop_device = None # Initialize to None
try:
# Create a temporary directory to mount the vfat image
grub_efi_tmpdir = tempfile.mkdtemp(dir=paths['BUILDDIR'], prefix="grub-efi.")
# Find a free loop device and attach the vfat image
loop_device_output = subprocess.run(["losetup", "--show", "--find", efi_img_path], check=True, text=True, capture_output=True)
if loop_device_output.returncode != 0:
logger.error(f" Error attaching loop device for {efi_img_path}. Stderr: {loop_device_output.stderr.strip()}")
return None
loop_device = loop_device_output.stdout.strip()
logger.debug(f" Attached loop device: {loop_device}")
# Mount the vfat image to the temporary directory
subprocess.run(["mount", "-o", "rw,loop", loop_device, grub_efi_tmpdir], check=True) # Removed offset=0 as it's not always needed and can cause issues
logger.debug(f" EFI vfat image mounted at: {grub_efi_tmpdir}")
# Copy boot directory contents (kernel, initrd, etc.) to the mounted vfat image
# This step copies the kernel image, initramfs, and any other boot files
# from the ISO's boot staging directory to the EFI partition.
# The source is the directory where kernel/initrd were copied in iso_builder_main
# The destination is the root of the mounted EFI image
logger.info("=> Copying boot directory contents to the mounted EFI image...")
# Assuming iso_image_boot_dir is the directory containing vmlinuz/vmlinux and initrd
source_boot_contents = iso_image_boot_dir # This is the directory like /files/builder.../fusato/image/boot
destination_efi_root = grub_efi_tmpdir # This is the temporary mount point
# Copy all contents from source_boot_contents to destination_efi_root
# Use a robust copy method like rsync or shutil.copytree
# shutil.copytree(source_boot_contents, destination_efi_root, dirs_exist_ok=True) # Requires Python 3.8+
# Alternative using subprocess and rsync
try:
subprocess.run(["rsync", "-a", f"{source_boot_contents}/", destination_efi_root], check=True)
logger.debug(f" Boot directory contents copied from {source_boot_contents} to {destination_efi_root}")
except FileNotFoundError as e:
logger.error(f" Command not found: {e}. Make sure rsync is installed on the host.")
return None
except subprocess.CalledProcessError as e:
logger.error(f" Error copying boot directory contents with rsync: {e.stderr.decode('utf-8', errors='ignore')}")
return None
# Generate the content of grub_pep.cfg
# This file will be placed in the mounted EFI partition (/EFI/BOOT/grub.cfg or similar)
# The actual path where GRUB looks depends on the architecture and the grub-mkstandalone call
# Let's generate it in the grub_dir first and then decide where it goes in the EFI image
# It's common to place it in /EFI/BOOT/grub.cfg or /grub/grub.cfg within the EFI partition
# The grub-mkstandalone command below specifies the config file path within the EFI image.
# Let's generate it directly in the mounted EFI partition for simplicity.
grub_pep_cfg_path_in_efi = os.path.join(grub_efi_tmpdir, "grub", "grub.cfg") # Common path within EFI partition
os.makedirs(os.path.dirname(grub_pep_cfg_path_in_efi), exist_ok=True) # Ensure directory exists
# Ensure config is passed if needed by generate_grub_pep_cfg
desktop_environment = config.get("desktop", "XFCE") if config else "XFCE"
kernel_flavor_name = config.get("kernel_flavor", "current") if config else "current"
logger.debug(f" Value of boot_cmdline being passed to generate_grub_pep_cfg: '{boot_cmdline}'")
# Assuming generate_grub_pep_cfg is defined elsewhere and imported
grub_pep_cfg_content = generate_grub_pep_cfg(
kernel_version=kernel_version, # Use the kernel_version argument
initrd_filename=os.path.basename(initrd_path), # Pass just the filename
kernel_image_filename=kernel_img_name, # Pass just the filename
boot_cmdline=boot_cmdline, # Use the boot_cmdline argument
boot_title=boot_title, # Use the boot_title argument
keymap=keymap, # Use the keymap argument
locale=locale, # Use the locale argument
architecture=architecture, # Use the architecture argument
want_memtest=want_memtest, # Use the determined want_memtest
# splash_image and SPLASH_DIR might be used by generate_grub_pep_cfg
# Ensure they are passed if needed by that function
splash_image=splash_image, # Use the splash_image argument
SPLASH_DIR=SPLASH_DIR # Use the SPLASH_DIR argument
)
# Write the content to the grub.cfg file in the mounted EFI partition
with open(grub_pep_cfg_path_in_efi, "w") as f:
f.write(grub_pep_cfg_content)
logger.info(f" grub.cfg generated at: {grub_pep_cfg_path_in_efi} (inside mounted EFI image)")
def build_grub_image_internal(target_architecture, grub_efi_tmpdir, pep_target_path):
"""Internal function to build the GRUB EFI image for a specific architecture."""
logger.info(f"Building GRUB EFI image for architecture: {target_architecture}")
# Determine GRUB platform and EFI boot filename based on target architecture
grub_platform = ""
efi_boot_filename = ""
if target_architecture == "x86_64":
grub_platform = "x86_64-efi"
efi_boot_filename = "BOOTX64.EFI"
elif target_architecture == "aarch64":
grub_platform = "arm64-efi"
efi_boot_filename = "BOOTAA64.EFI"
elif target_architecture == "i686":
grub_platform = "i386-efi"
efi_boot_filename = "BOOTIA32.EFI"
else:
logger.error(f" Unsupported architecture for GRUB EFI build: {target_architecture}")
return None # Or raise an exception
logger.info(f" => Building GRUB EFI image for platform: {grub_platform}")
# grub-mkstandalone command to create the EFI bootloader
# Output is directed to a temporary file within the uchroot environment's /tmp
# The input config file path is relative to the root of the mounted EFI partition
# which is also the root of the uchroot environment for this command.
efi_file_tmp_path_in_uchroot = f"/tmp/{efi_boot_filename.lower()}" # Temporary path inside uchroot /tmp
grub_mkstandalone_command = [
"xbps-uchroot",
pep_target_path, # The rootfs to use for uchroot
"grub-mkstandalone",
"--", # Arguments for grub-mkstandalone start here
f"--directory=/usr/lib/grub/{grub_platform}", # GRUB modules directory in the target rootfs
f"--format={grub_platform}", # Output format
f"--output={efi_file_tmp_path_in_uchroot}", # Output path inside uchroot /tmp
"/grub/grub.cfg" # Path to the grub.cfg file relative to the uchroot root (mounted EFI partition root)
]
logger.debug(f" => Executing grub-mkstandalone command [UCHROOT: {pep_target_path}]: {' '.join(grub_mkstandalone_command)}")
try:
subprocess.run(grub_mkstandalone_command, check=True, text=True, capture_output=True)
logger.debug(f" grub-mkstandalone command executed successfully.")
# logger.debug(f" grub-mkstandalone stdout: {result.stdout.strip()}") # Uncomment for verbose debug
# logger.debug(f" grub-mkstandalone stderr: {result.stderr.strip()}") # Uncomment for verbose debug
except FileNotFoundError as e:
logger.error(f" Command not found: {e}. Make sure grub-mkstandalone is installed in the target rootfs.")
return None
except subprocess.CalledProcessError as e:
logger.error(f" Error generating EFI loader (grub-mkstandalone): Exit code {e.returncode}")
logger.error(f" Command: {' '.join(e.cmd)}")
logger.error(f" Stdout: {e.stdout.strip()}")
logger.error(f" Stderr: {e.stderr.strip()}")
return None
except Exception as e:
logger.error(f" Unexpected error during grub-mkstandalone: {e}")
return None
# Copy the generated EFI file from the uchroot environment's /tmp to the mounted EFI partition
# The file is generated at pep_target_path/tmp/boot{arch}.efi on the host filesystem
efi_source_path_on_host = os.path.join(pep_target_path, "tmp", os.path.basename(efi_file_tmp_path_in_uchroot))
efi_boot_dir_in_tmpfs = os.path.join(grub_efi_tmpdir, "EFI", "BOOT")
efi_file_destination_in_efi = os.path.join(efi_boot_dir_in_tmpfs, efi_boot_filename)
logger.debug(f" => Creating EFI/BOOT directory in mounted EFI image: {efi_boot_dir_in_tmpfs}")
os.makedirs(efi_boot_dir_in_tmpfs, exist_ok=True)
logger.debug(f" => Copying generated EFI loader from host path: {efi_source_path_on_host} to mounted EFI path: {efi_file_destination_in_efi}")
try:
shutil.copy2(efi_source_path_on_host, efi_file_destination_in_efi)
logger.debug(f" EFI loader copied successfully.")
except FileNotFoundError:
logger.error(f" Error: Generated EFI loader not found at host path: {efi_source_path_on_host}")
logger.error(f" grub-mkstandalone might have failed to create the output file.")
return None
except Exception as e:
logger.error(f" Unexpected error while copying EFI loader: {e}")
return None
return efi_file_destination_in_efi # Return path to the copied EFI file
# Build EFI images for relevant architectures
# For x86_64, you might want both BOOTX64.EFI and BOOTIA32.EFI for compatibility
# For aarch64, only BOOTAA64.EFI is needed
efi_images = {} # Dictionary to store paths to generated EFI files
if architecture == "x86_64":
# Build for x86_64 (BOOTX64.EFI)
efi_images["x86_64"] = build_grub_image_internal("x86_64", grub_efi_tmpdir, pep_target_path)
# Optionally build for i686 (BOOTIA32.EFI) for compatibility
# efi_images["i686"] = build_grub_image_internal("i686", grub_efi_tmpdir, pep_target_path)
elif architecture == "aarch64":
# Build only for aarch64 (BOOTAA64.EFI)
efi_images["aarch64"] = build_grub_image_internal("aarch64", grub_efi_tmpdir, pep_target_path)
elif architecture == "i686":
# Build only for i686 (BOOTIA32.EFI)
efi_images["i686"] = build_grub_image_internal("i686", grub_efi_tmpdir, pep_target_path)
else:
logger.warning(f"Unsupported architecture for GRUB EFI build: {architecture}. Skipping EFI bootloader generation.")
# Check if EFI images were successfully built
if not efi_images or any(img is None for img in efi_images.values()):
logger.error("=> Failed to build one or more GRUB EFI images.")
# Decide whether to return None or raise an exception based on how critical this is
return None
logger.info("=> GRUB EFI bootloader generated successfully.")
return grub_dir # Return the path to the grub directory containing efiboot.img
except subprocess.CalledProcessError as e:
logger.error(f"Error executing subprocess commands for GRUB EFI: Exit code {e.returncode}")
logger.error(f"Command: {' '.join(e.cmd)}")
logger.error(f"Stdout: {e.stdout.decode('utf-8', errors='ignore').strip()}")
logger.error(f"Stderr: {e.stderr.decode('utf-8', errors='ignore').strip()}")
# Decide whether to return None or raise based on criticality
return None
except FileNotFoundError as e:
logger.error(f"Command not found: {e}. Ensure necessary tools (losetup, mount, rsync, grub-mkstandalone) are installed.")
# Decide whether to return None or raise based on criticality
return None
except Exception as e:
logger.error(f"Unexpected error while generating GRUB EFI: {e}")
# Decide whether to return None or raise based on criticality
return None
finally:
# Clean up: Unmount loop device and remove temporary directory
if grub_efi_tmpdir and os.path.exists(grub_efi_tmpdir):
try:
# Attempt unmount multiple times if necessary
for _ in range(5):
subprocess.run(["umount", "-l", grub_efi_tmpdir], check=True) # Use -l for lazy unmount
logger.debug(f" EFI vfat image unmounted from: {grub_efi_tmpdir}")
break # Exit loop if unmount successful
else:
logger.warning(f" Warning: Failed to unmount EFI vfat image after multiple attempts: {grub_efi_tmpdir}")
except subprocess.CalledProcessError as e:
logger.warning(f" Warning: Failed to unmount EFI vfat image: {e}")
except Exception as e:
logger.warning(f" Warning: Unexpected error during unmount: {e}")
if loop_device:
try:
# Attempt detach multiple times if necessary
for _ in range(5):
subprocess.run(["losetup", "--detach", loop_device], check=True)
logger.debug(f" Loop device {loop_device} detached.")
break # Exit loop if detach successful
else:
logger.warning(f" Warning: Failed to detach loop device after multiple attempts: {loop_device}")
except subprocess.CalledProcessError as e:
logger.warning(f" Warning: Failed to detach loop device {loop_device}: {e}")
except Exception as e:
logger.warning(f" Warning: Unexpected error during loop device detach: {e}")
if grub_efi_tmpdir and os.path.exists(grub_efi_tmpdir):
# Use ignore_errors=True for rmtree as unmount might still fail
shutil.rmtree(grub_efi_tmpdir, ignore_errors=True)
logger.debug(f" Temporary directory {grub_efi_tmpdir} removed.")
def copy_boot_directory_contents(source_boot_dir, dest_boot_dir):
"""Copies contents of source boot dir to dest boot dir, merging if necessary."""
logger.info(f" => Copying boot directory contents from: {source_boot_dir} to: {dest_boot_dir} (merge)...")
os.makedirs(dest_boot_dir, exist_ok=True)
for item in os.listdir(source_boot_dir):
s_item = os.path.join(source_boot_dir, item)
d_item = os.path.join(dest_boot_dir, item)
if os.path.isdir(s_item):
logger.debug(f" Merging directory: {item}")
shutil.copytree(s_item, d_item, dirs_exist_ok=True)
else:
logger.debug(f" Copying file: {item}")
shutil.copy2(s_item, d_item)
logger.debug(" => Boot directory contents copied/merged successfully.")
def generate_grub_pep_cfg(kernel_version, boot_cmdline, boot_title, keymap, locale, architecture, want_memtest, splash_image, SPLASH_DIR):
"""Generates the content of grub_pep.cfg using the provided template."""
grub_pep_cfg_template = """
set pager="1"
set locale_dir="(${peppermintos})/boot/grub/locale"
set theme="(${peppermintos})/boot/grub/themes/peppermint/theme.txt"
if [ -e "${prefix}/${grub_cpu}-${grub_platform}/all_video.mod" ]; then
insmod all_video
else
insmod efi_gop
insmod efi_uga
insmod video_bochs
insmod video_cirrus
fi
insmod font
if loadfont "(${peppermintos})/boot/grub/fonts/unicode.pf2" ; then
insmod gfxterm
set gfxmode="auto"
terminal_input console
terminal_output gfxterm
insmod png
background_image "(${peppermintos})/boot/isolinux/@@SPLASHIMAGE@@"
fi
# Set default menu entry
default=linux
timeout=15
timeout_style=menu
# GRUB init tune for accessibility
play 600 988 1 1319 4
if [ cpuid -l ]; then
menuentry "@@BOOT_TITLE@@ @@KERNVER@@ (@@ARCH@@)" --id "linux" {
set gfxpayload="keep"
linux (${peppermintos})/boot/vmlinuz \\
root=live:CDLABEL=PEPPERMINTOS ro init=/sbin/init \\
rd.luks=0 rd.md=0 rd.dm=0 loglevel=4 gpt add_efi_memmap \\
vconsole.unicode=1 vconsole.keymap=@@KEYMAP@@ \\
locale.LANG=@@LOCALE@@ @@BOOT_CMDLINE@@
initrd (${peppermintos})/boot/initrd
}
menuentry "@@BOOT_TITLE@@ @@KERNVER@@ (@@ARCH@@) (RAM)" --id "linuxram" {
set gfxpayload="keep"
linux (${peppermintos})/boot/vmlinuz \\
root=live:CDLABEL=PEPPERMINTOS ro init=/sbin/init \\
rd.luks=0 rd.md=0 rd.dm=0 loglevel=4 gpt add_efi_memmap \\
vconsole.unicode=1 vconsole.keymap=@@KEYMAP@@ \\
locale.LANG=@@LOCALE@@ @@BOOT_CMDLINE@@ rd.live.ram
initrd (${peppermintos})/boot/initrd
}
menuentry "@@BOOT_TITLE@@ @@KERNVER@@ (@@ARCH@@) (graphics disabled)" --id "linuxnogfx" {
set gfxpayload="keep"
linux (${peppermintos})/boot/vmlinuz \\
root=live:CDLABEL=PEPPERMINTOS ro init=/sbin/init \\
rd.luks=0 rd.md=0 rd.dm=0 loglevel=4 gpt add_efi_memmap \\
vconsole.unicode=1 vconsole.keymap=@@KEYMAP@@ \\
locale.LANG=@@LOCALE@@ @@BOOT_CMDLINE@@ nomodeset
initrd (${peppermintos})/boot/initrd
}
menuentry "@@BOOT_TITLE@@ @@KERNVER@@ (@@ARCH@@)" with speech --hotkey s --id "linuxa11y" {
set gfxpayload="keep"
linux (${peppermintos})/boot/vmlinuz \\
root=live:CDLABEL=PEPPERMINTOS ro init=/sbin/init \\
rd.luks=0 rd.md=0 rd.dm=0 loglevel=4 gpt add_efi_memmap \\
vconsole.unicode=1 vconsole.keymap=@@KEYMAP@@ \\
locale.LANG=@@LOCALE@@ @@BOOT_CMDLINE@@ live.accessibility live.autologin
initrd (${peppermintos})/boot/initrd
}
menuentry "@@BOOT_TITLE@@ @@KERNVER@@ (@@ARCH@@) with speech (RAM)" --hotkey r --id "linuxa11yram" {
set gfxpayload="keep"
linux (${peppermintos})/boot/vmlinuz \\
root=live:CDLABEL=PEPPERMINTOS ro init=/sbin/init \\
rd.luks=0 rd.md=0 rd.dm=0 loglevel=4 gpt add_efi_memmap \\
vconsole.unicode=1 vconsole.keymap=@@KEYMAP@@ \\
locale.LANG=@@LOCALE@@ @@BOOT_CMDLINE@@ live.accessibility live.autologin rd.live.ram
initrd (${peppermintos})/boot/initrd
}
menuentry "@@BOOT_TITLE@@ @@KERNVER@@ (@@ARCH@@) with speech (graphics disabled)" --hotkey g --id "linuxa11ynogfx" {
set gfxpayload="keep"
linux (${peppermintos})/boot/vmlinuz \\
root=live:CDLABEL=PEPPERMINTOS ro init=/sbin/init \\
rd.luks=0 rd.md=0 rd.dm=0 loglevel=4 gpt add_efi_memmap \\
vconsole.unicode=1 vconsole.keymap=@@KEYMAP@@ \\
locale.LANG=@@LOCALE@@ @@BOOT_CMDLINE@@ live.accessibility live.autologin nomodeset
initrd (${peppermintos})/boot/initrd
}
if [ "${grub_platform}" == "efi" ]; then
menuentry "Run Memtest86+ (RAM test)" --hotkey m --id memtest {
set gfxpayload="keep"
linux (${peppermintos})/boot/memtest.efi
}
menuentry 'UEFI Firmware Settings' --hotkey f --id uefifw {
fwsetup
}
else
menuentry "Run Memtest86+ (RAM test)" --id memtest {
set gfxpayload="keep"
linux (${peppermintos})/boot/memtest.bin
}
fi
menuentry "System restart" --hotkey b --id restart {
echo "System rebooting..."
reboot
}
menuentry "System shutdown" --hotkey p --id poweroff {
echo "System shutting down..."
halt
}
fi
"""
splash_image_path = os.path.join(SPLASH_DIR, "splash.png") if SPLASH_DIR else " "
grub_pep_cfg_content = grub_pep_cfg_template.replace("@@BOOT_TITLE@@", boot_title)
grub_pep_cfg_content = grub_pep_cfg_content.replace("@@KERNVER@@", kernel_version)
grub_pep_cfg_content = grub_pep_cfg_content.replace("@@ARCH@@", architecture)
grub_pep_cfg_content = grub_pep_cfg_content.replace("@@KEYMAP@@", keymap)
grub_pep_cfg_content = grub_pep_cfg_content.replace("@@LOCALE@@", locale)
grub_pep_cfg_content = grub_pep_cfg_content.replace("@@BOOT_CMDLINE@@", boot_cmdline)
grub_pep_cfg_content = grub_pep_cfg_content.replace("@@SPLASHIMAGE@@", os.path.basename(splash_image_path) if splash_image_path and os.path.exists(splash_image_path) else " ")
return grub_pep_cfg_content