diff --git a/contrib/gen-hybrid-uefi-bios-iso b/contrib/gen-hybrid-uefi-bios-iso new file mode 100755 index 0000000..d7f4771 --- /dev/null +++ b/contrib/gen-hybrid-uefi-bios-iso @@ -0,0 +1,345 @@ +#!/usr/bin/python3 +""" +Build hybrid BIOS/UEFI bootable ISO with GRUB and background image. +""" + +import os +import shutil +import subprocess +import sys +import tempfile +from os.path import abspath, exists, join + + +class ISOBuilder: + """Builder for hybrid BIOS/UEFI bootable ISO with GRUB.""" + + def __init__( + self, + iso_dir: str, + output_iso: str, + volume_id: str ="TurnKey GNU/Linux", + ) -> None: + self.iso_dir = abspath(iso_dir) + self.output_iso = abspath(output_iso) + self.volume_id = volume_id + self.grub_dir = join(self.iso_dir, "boot", "grub") + + # Validate paths + if not exists(self.iso_dir): + raise FileNotFoundError(f"ISO directory not found: {self.iso_dir}") + + def create_directories(self) -> None: + """Create necessary directory structure.""" + print("Creating directory structure...") + os.makedirs(self.grub_dir, exist_ok=True) + os.makedirs(join(self.iso_dir, "EFI", "boot"), exist_ok=True) + + def copy_background_image(self) -> bool: + """Copy background image from isolinux directory.""" + background_source = join(self.iso_dir, "isolinux", "splash.png") + background_dest = join(self.grub_dir, "background.png") + + if exists(background_source): + print(f"Copying background image from {background_source}...") + shutil.copy2(background_source, background_dest) + return True + else: + print( + f"Warning: Background image not found at {background_source}", + ) + return False + + def create_grub_config(self) -> None: + """Create GRUB configuration file with background support.""" + print("Creating GRUB configuration...") + + grub_cfg = join(self.grub_dir, "grub.cfg") + + config_content = """\ +set timeout=5 +set default=0 + +# Load graphics modules +if [ "${grub_platform}" == "efi" ]; then + insmod efi_gop + insmod efi_uga +else + insmod vbe + insmod vga +fi + +# Enable graphical terminal and load PNG support +insmod gfxterm +insmod png +set gfxmode=auto +terminal_output gfxterm + +# Set background image +if background_image /boot/grub/background.png; then + set color_normal=light-gray/black + set color_highlight=white/blue + set menu_color_normal=light-gray/black + set menu_color_highlight=white/blue +else + set color_normal=white/black + set color_highlight=black/light-gray +fi + +menuentry "Live System" { + linux /live/vmlinuz boot=live quiet splash + initrd /live/initrd.img +} + +menuentry "Install System" { + linux /live/vmlinuz boot=live quiet splash auto-installer + initrd /live/initrd.img +} + +menuentry "Live System (Safe Mode)" { + linux /live/vmlinuz boot=live quiet splash nomodeset + initrd /live/initrd.img +} +""" + with open(grub_cfg, 'w') as fob: + fob.write(config_content) + + def create_bios_boot_image(self) -> None: + """Create GRUB BIOS boot image.""" + print("Creating GRUB BIOS boot image...") + + bios_img = join(self.grub_dir, "bios.img") + core_img = join(self.grub_dir, "core.img") + + # List of modules for BIOS boot + modules = [ + "biosdisk", "iso9660", "part_gpt", "part_msdos", + "normal", "boot", "linux", "configfile", "search", + "search_fs_uuid", "search_fs_file", "loadenv", + "ls", "cat", "echo", "test", "true", "help", "gzio", + "png", "vbe", "vga", "gfxterm", "gfxterm_background" + ] + + # Create core image + cmd = [ + "grub-mkimage", + "--format=i386-pc", + f"--output={core_img}", + "--prefix=/boot/grub", + "--compression=auto", + *modules, + ] + + subprocess.run(cmd, check=True) + + # Combine boot.img and core.img + boot_img_source = "/usr/lib/grub/i386-pc/boot.img" + + with open(bios_img, "wb") as out_file: + with open(boot_img_source, "rb") as boot_file: + out_file.write(boot_file.read()) + with open(core_img, "rb") as core_file: + out_file.write(core_file.read()) + + # Clean up temporary core image + os.remove(core_img) + + # Copy GRUB modules + grub_modules_dir = join(self.grub_dir, "i386-pc") + os.makedirs(grub_modules_dir, exist_ok=True) + + source_modules = "/usr/lib/grub/i386-pc" + if exists(source_modules): + for pattern in ["*.mod", "*.lst"]: + import glob + for src_file in glob.glob(join(source_modules, pattern)): + try: + shutil.copy2(src_file, grub_modules_dir) + except Exception: + pass # Ignore copy errors for individual modules + + def create_efi_boot_image(self) -> None: + """Create GRUB EFI boot image.""" + print("Creating GRUB EFI boot image...") + + efi_img = join(self.grub_dir, "efi.img") + + # Create FAT filesystem image + subprocess.run([ + "dd", + "if=/dev/zero", + f"of={efi_img}", + "bs=1M", + "count=10" + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + subprocess.run([ + "mkfs.vfat", + efi_img + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + # Mount EFI image + efi_mount = tempfile.mkdtemp() + + try: + subprocess.run( + [ + "mount", + "-o", "loop", + efi_img, + efi_mount + ], + check=True, + ) + + try: + # Create directory structure + efi_boot_dir = join(efi_mount, "EFI", "boot") + os.makedirs(efi_boot_dir, exist_ok=True) + + # Copy background image into EFI image + grub_dir_in_efi = join(efi_mount, "boot", "grub") + os.makedirs(grub_dir_in_efi, exist_ok=True) + + background_src = join(self.grub_dir, "background.png") + if exists(background_src): + shutil.copy2( + background_src, + join(grub_dir_in_efi, "background.png"), + ) + grub_cfg = join(self.grub_dir, "grub.cfg") + + + # Create standalone GRUB EFI bootloader (64-bit) + subprocess.run( + [ + "grub-mkstandalone", + "--format=x86_64-efi", + f"--output={join(efi_boot_dir, 'bootx64.efi')}", + "--locales=", + "--fonts=", + f"boot/grub/grub.cfg={grub_cfg}", + ], + check=True, + ) + + # Create standalone GRUB EFI bootloader (32-bit) + subprocess.run( + [ + "grub-mkstandalone", + "--format=i386-efi", + f"--output={join(efi_boot_dir, 'bootia32.efi')}", + "--locales=", + "--fonts=", + f"boot/grub/grub.cfg={grub_cfg}", + ], check=True) + + finally: + # Unmount EFI image + subprocess.run(["umount", efi_mount], check=True) + + finally: + # Clean up temporary directory + os.rmdir(efi_mount) + + def create_iso(self) -> None: + """Create the hybrid ISO using xorriso.""" + print("Creating hybrid ISO with xorriso...") + + cmd = [ + "xorriso", + "-as", "mkisofs", + "-iso-level", "3", + "-full-iso9660-filenames", + "-volid", self.volume_id, + "-appid", "Custom Debian Installer", + "-publisher", "Your Organization", + "-preparer", "GRUB Hybrid Build Script", + # BIOS boot + "-eltorito-boot", "boot/grub/bios.img", + "-no-emul-boot", + "-boot-load-size", "4", + "-boot-info-table", + "--grub2-boot-info", + "--grub2-mbr", "/usr/lib/grub/i386-pc/boot_hybrid.img", + # UEFI boot + "-eltorito-alt-boot", + "-e", "boot/grub/efi.img", + "-no-emul-boot", + "-isohybrid-gpt-basdat", + # Output + "-output", self.output_iso, + self.iso_dir + ] + + subprocess.run(cmd, check=True) + + def build(self) -> None: + """Execute the complete build process.""" + print("=" * 70) + print( + "Creating hybrid BIOS/UEFI bootable ISO with GRUB and background", + ) + print("=" * 70) + + self.create_directories() + self.copy_background_image() + self.create_grub_config() + self.create_bios_boot_image() + self.create_efi_boot_image() + self.create_iso() + + divider = "=" * 70 + print("\n" + divider) + print("- Hybrid BIOS/UEFI ISO created successfully!") + print(f" Output: {self.output_iso}") + print(divider + "\n") + print("You can now:") + print( + f" 1. Write to USB: dd if={self.output_iso}" + " of=/dev/sdX bs=4M status=progress", + ) + print(" 2. Test with QEMU:") + print( + f" BIOS: qemu-system-x86_64 -cdrom {self.output_iso} -m 2048", + ) + print( + " UEFI: qemu-system-x86_64 -bios /usr/share/ovmf/OVMF.fd" + f" -cdrom {self.output_iso} -m 2048\n", + ) + + +def main() -> None: + """Main entry point.""" + # Configuration + ISO_DIR = "iso_root" + OUTPUT_ISO = "custom-debian.iso" + VOLUME_ID = "CUSTOM_DEBIAN" + + # Parse command line arguments (basic implementation) + if len(sys.argv) > 1: + ISO_DIR = sys.argv[1] + if len(sys.argv) > 2: + OUTPUT_ISO = sys.argv[2] + if len(sys.argv) > 3: + VOLUME_ID = sys.argv[3] + + try: + builder = ISOBuilder(ISO_DIR, OUTPUT_ISO, VOLUME_ID) + builder.build() + except FileNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + except subprocess.CalledProcessError as e: + print(f"Error: Command failed: {e.cmd}", file=sys.stderr) + sys.exit(1) + except KeyboardInterrupt: + print("\nBuild cancelled by user", file=sys.stderr) + sys.exit(130) + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main()