""" 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.core.bootstrap.paths import paths 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, keymap="us", locale="en_US.UTF-8", boot_title="Peppermint OS pep", 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) # Create boot.cat - usually created by mkisofs/xorriso 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}") # 1. Copy Syslinux files 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.") # 2. Copy and configure isolinux.cfg 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() # Perform replacements - consolidated replacements replacements = { "@@SPLASHIMAGE@@": os.path.basename(splash_image_path), "@@KERNVER@@": kernel_version, "@@KEYMAP@@": keymap, "@@ARCH@@": architecture, "@@LOCALE@@": locale, "@@BOOT_TITLE@@": boot_title, "@@BOOT_CMDLINE@@": boot_cmdline, "@@Volume_ID@@": Volume_ID } 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 # 3. Copy splash image 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.") # 4 & 5. Copy memtest86+ and memtest86+ EFI (if exist) - Combined similar logic 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, paths, grub_cfg_template_dir, memtest_wanted, config, logger, pep_target_path, platform_cmdline, splash_image, grub_themes, grub_modules, boot_dir, kernel_version, boot_cmdline="", boot_title="Peppermint OS pep", keymap="us", locale="en_US.UTF-8"): """Generates GRUB EFI bootloader.""" logger.info("=> Starting GRUB EFI bootloader generation...") grub_platform = f"{architecture}-efi" grub_dir = os.path.join(paths['IMAGEDIR'], 'boot', 'grub') pep_target_path = paths['PEPTARGETDIR'] iso_image_boot_dir = paths['IMAGEDIR'] grub_data_dir = pep_target_path + "/usr/share/grub" TEMPLATES_DIR = paths['TEMPLATES_DIR'] SPLASH_DIR = paths['SPLASH_DIR'] SPLASH_THEMES_DIR = os.path.join(BASE_DIR, "peppermint/grub/themes") iso_boot_path = os.path.join(paths['IMAGEDIR'], 'boot') copy_grub_theme(SPLASH_THEMES_DIR, iso_boot_path) os.makedirs(grub_dir, exist_ok=True) # 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 files_to_copy = ["grub.cfg"] logger.info("=> Copying template files to the GRUB directory...") 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 splash_source = os.path.join(SPLASH_DIR, "splash.png") splash_dest = os.path.join(grub_dir, "splash.png") 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}") kernel_img_name = "vmlinuz" want_memtest = True if architecture == "aarch64": kernel_img_name = "vmlinux" want_memtest = False elif architecture == "x86_64": kernel_img_name = "vmlinuz" logger.info(f"=> Destination GRUB directory: {grub_dir}") logger.info(f"=> GRUB data source directory: {grub_data_dir}") logger.info(f"=> Kernel image name: {kernel_img_name}") logger.info(f"=> Want memtest86+: {want_memtest}") # 1. Prepare GRUB directory and copy font 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 logger.info("=> Creating EFI vfat image...") efi_img_path = os.path.join(grub_dir, "efiboot.img") try: subprocess.run(["truncate", "-s", "32M", efi_img_path], check=True, text=True) subprocess.run(["mkfs.vfat", "-F12", "-S", "512", "-n", "grub_uefi", efi_img_path], check=True, text=True) logger.debug(f" EFI vfat image created: {efi_img_path}") except subprocess.CalledProcessError as e: logger.error(f" Error creating EFI vfat image: {e.stderr}") return None # 3. Mount vfat image and build GRUB image function grub_efi_tmpdir = tempfile.mkdtemp(dir=paths['BUILDDIR'], prefix="grub-efi.") loop_device = None try: loop_device_output = subprocess.run(["losetup", "--show", "--find", efi_img_path], check=True, text=True, capture_output=True) # Check if loop_device_output is None or has an error if loop_device_output is None or loop_device_output.returncode != 0: logger.error(f" Error: loop_device_output is None or has an error. losetup command failed? Stderr: {loop_device_output.stderr.decode()}") return None loop_device = loop_device_output.stdout.strip() subprocess.run(["mount", "-o", "rw,loop,offset=0", loop_device, grub_efi_tmpdir], check=True, text=True) logger.debug(f" EFI vfat image mounted at: {grub_efi_tmpdir}, loop device: {loop_device}") # Copy boot dir logger.info("=> Copying boot directory and building GRUB images for architecture...") copy_boot_directory_contents(os.path.join(iso_image_boot_dir, 'boot'), os.path.join(pep_target_path, 'boot')) logger.debug(f" => Boot directory copied to: {os.path.join(pep_target_path, 'boot')}") # Generate the content of grub_pep.cfg desktop_environment = config.get("desktop", "XFCE") kernel_flavor_name = config.get("kernel_flavor", "current") logger.debug(f" Value of boot_cmdline being passed to generate_grub_pep_cfg: '{boot_cmdline}'") grub_pep_cfg_content = generate_grub_pep_cfg( kernel_version=kernel_version, boot_cmdline=boot_cmdline, boot_title=boot_title, keymap=keymap, locale=locale, architecture=architecture, want_memtest=want_memtest, splash_image=splash_image, SPLASH_DIR=paths['SPLASH_DIR'] ) # Write the content to the grub_pep.cfg file grub_pep_cfg_path = os.path.join(grub_dir, "grub_pep.cfg") with open(grub_pep_cfg_path, "w") as f: f.write(grub_pep_cfg_content) logger.info(f" grub_pep.cfg generated at: {grub_pep_cfg_path}") def build_grub_image(architecture, grub_arch, efi_arch, grub_platform): """Internal function to build the GRUB EFI image for an architecture.""" logger.info(f"Building GRUB EFI image for architecture: {architecture}") logger.info(f" => Building GRUB EFI image for GRUB arch: {grub_arch}, EFI: {efi_arch}...") efi_file_tmp_path = os.path.join("/tmp", f"boot{efi_arch.lower()}.efi") grub_mkstandalone_command = [ "xbps-uchroot", pep_target_path, "grub-mkstandalone", "--", f"--directory=/usr/lib/grub/{grub_platform}", f"--format={grub_platform}", f"--output={efi_file_tmp_path}", f"boot/grub/grub.cfg" ] try: logger.debug(f" => Creating EFI/BOOT directory in: {os.path.join(grub_efi_tmpdir, 'EFI', 'BOOT')}") subprocess.run(["mkdir", "-p", os.path.join(grub_efi_tmpdir, "EFI", "BOOT")], check=True) logger.debug(f" EFI/BOOT directory created successfully.") logger.debug(f" => Executing grub-mkstandalone command [CHROOT: {pep_target_path}]: {' '.join(grub_mkstandalone_command)}") subprocess.run(grub_mkstandalone_command, check=True,text=True) logger.debug(f" grub-mkstandalone command executed successfully.") efi_boot_dir_in_tmpfs = os.path.join(grub_efi_tmpdir, "EFI", "BOOT") os.makedirs(efi_boot_dir_in_tmpfs, exist_ok=True) efi_source_path_on_host = os.path.join(pep_target_path, "tmp", f"boot{efi_arch.lower()}.efi") efi_file_destination = os.path.join(efi_boot_dir_in_tmpfs, f"BOOT{efi_arch.upper()}.EFI") shutil.copy2(efi_source_path_on_host, efi_file_destination) logger.debug(f" EFI loader copied to: {efi_file_destination}") except subprocess.CalledProcessError as e: logger.error(f" Error generating EFI loader (grub-mkstandalone): {e}") raise except OSError as e: logger.error(f" OSError during grub-mkstandalone: {e}") raise except Exception as e: logger.error(f" Unexpected error during grub-mkstandalone: {e}") raise efi_images = {} if architecture in ["i686", "x86_64"]: for arch in ["i686", "x86_64"]: if arch == "i686": grub_arch = "i386" efi_arch = "ia32" grub_platform = "i386-efi" elif arch == "x86_64": grub_arch = "x86_64" efi_arch = "x64" grub_platform = "x86_64-efi" efi_images[arch] = build_grub_image(architecture, grub_arch, efi_arch, grub_platform) elif architecture == "aarch64": grub_platform = "arm64-efi" build_grub_image(architecture, "arm64", "aarch64", grub_platform) logger.info("=> GRUB EFI bootloader generated successfully.") return grub_dir except subprocess.CalledProcessError as e: logger.error(f"Error executing subprocess commands for GRUB EFI: {e}") return None except Exception as e: logger.error(f"Unexpected error while generating GRUB EFI: {e}") return None finally: if grub_efi_tmpdir: try: subprocess.run(["umount", grub_efi_tmpdir], check=True) logger.debug(f" EFI vfat image unmounted from: {grub_efi_tmpdir}") except subprocess.CalledProcessError as e: logger.warning(f" Warning: Failed to unmount EFI vfat image: {e}") if loop_device: try: subprocess.run(["losetup", "--detach", loop_device], check=True) logger.debug(f" Loop device {loop_device} detached.") except subprocess.CalledProcessError as e: logger.warning(f" Warning: Failed to detach loop device {loop_device}: {e}") if grub_efi_tmpdir: 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