# Copyright (C) 2005 Mandriva # Olivier Blin # Copyright (C) 2017-2018 Mageia # Martin Whitaker # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA. # SYNOPSIS # -------- # This package provides a function to prepare the bootloader files that are # needed to boot the ISO. Both MBR and EFI boot are supported. It also # provides a function to prepare the kernel and initrd files needed to boot # a Live system. package MGA::DrakISO::BuildBoot; use strict; use MDK::Common; use File::Copy qw(mv); use Try::Tiny; use utf8; use MGA::DrakISO::LiveBuild; use MGA::DrakISO::Utils; use Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(prepare_live_system_boot prepare_iso_bootloader); ############################################################################### # Live System ############################################################################### # This is the top-level function called to prepare the kernel and initrd files # for booting the Live system. These files are built in and/or copied from the # Live root filesystem, so that must be prepared before calling this function. # sub prepare_live_system_boot { my ($build) = @_; my $root = $build->get_live_root; # Create a build directory. This will contain all the files we need to # exist in /boot on the ISO. my $boot_dir = $build->get_build_dir('boot'); mkdir_p($boot_dir); # Locate the kernel we want to boot. my $kernel = $build->find_kernel; print "Using kernel $kernel->{version}\n"; # Copy the kernel into the build directory. my $vmlinuz = '/boot/vmlinuz-' . $kernel->{version}; -e $root . $vmlinuz or die "ERROR: cannot find kernel $kernel->{version} in root system\n"; cp_f($root . $vmlinuz, $boot_dir . '/vmlinuz'); my $error_message; try { mk_dev_null($root); # Build an initrd suitable for Live boot. my $initrd = '/boot/' . $build->get_initrd_name; run_in_root($root, undef, 'mkinitrd', '-f', if_($::verbose < 3, '-q'), $initrd, $kernel->{version}) or die "ERROR: cannot create initrd\n"; run_as_root('chmod', '644', $root . $initrd) or die "ERROR: cannot chmod initrd\n"; # Copy the initrd into the build directory. cp_f($root . $initrd, $boot_dir . '/initrd.img'); } catch { $error_message = $_; } finally { rm_dev_null($root); }; defined $error_message && die $error_message; } ############################################################################### # ISO Bootloader ############################################################################### # This is the top-level function called to prepare the files needed by the # GRUB2 bootloader. It does not depend on any other preparatory steps. # sub prepare_iso_bootloader { my ($build) = @_; my $arch = $build->{settings}{arch}; # If the user has supplied a list of files to copy from the repository, # do that now. We may need some of those files to correctly auto-detect # the default kernel and initrd. my $repo_prefix = $build->{settings}{repository} . '/' . $arch . '/'; my $copy_prefix = $build->get_build_dir . '/'; foreach (group_by2(@{$build->{media}{copy_from_repo}})) { my ($src, $dst) = @$_; copy_or_link($repo_prefix . $src, $copy_prefix . $dst); } # Create a subdirectory to hold the grub2 bootloader. my $grub2_dir = $build->get_build_dir('boot/grub2'); mkdir_p($grub2_dir); # Locate and copy the default font for the bootloader. If we can't find a # font, don't worry - the bootloader will fall back to text mode. my $font = $build->get_absolute_path($build->{media}{bootloader_font}); if (defined $font) { -e $font or die "ERROR: cannot find bootloader font file $font\n"; } else { $font = '/usr/share/grub/unicode.pf2'; } if (-e $font) { my $fonts_dir = $grub2_dir . '/fonts'; mkdir_p($fonts_dir); cp_f($font, $fonts_dir); } # Locate and copy the bootloader theme. Default to the standard Mageia # theme if the user hasn't specified one. If that's not available either, # proceed without a theme. my $theme = $build->get_absolute_path($build->{media}{bootloader_theme}); if (defined $theme) { -d $theme or die "ERROR: cannot find bootloader theme directory $theme\n"; } else { $theme = '/boot/grub2/themes/maggy'; } my $theme_name = basename($theme); my @theme_fonts; if (-d $theme) { my $themes_dir = $grub2_dir . '/themes'; mkdir_p($themes_dir); cp_f($theme, $themes_dir); @theme_fonts = map { basename($_) } glob("$theme/*.pf2"); } # If the user has provided the necessary configuration data, construct # the bootloader language and keyboard selection submenus. my $add_lang_menu = defined $build->{media}{bootloader_langs}; my $add_kbd_menu = defined $build->{media}{bootloader_kbds}; if ($add_lang_menu) { my $lang_names = $build->get_absolute_path($build->{media}{bootloader_langs}); -e $lang_names or die "ERROR: cannot find bootloader language name file $lang_names\n"; my @langs = group_by2(eval(cat_($lang_names))) or die "ERROR: error in language name file $lang_names\n"; my $lang_kbds = dirname($lang_names) . '/lang-kbds.txt'; my $kbds; if ($add_kbd_menu && -e $lang_kbds) { $kbds = eval(cat_($lang_kbds)) or die "ERROR: error in language keyboard file $lang_kbds\n"; } MDK::Common::File::output_utf8($grub2_dir . '/lang-menu.cfg', build_lang_menu_cfg(\@langs, %$kbds)); } if ($add_kbd_menu) { my $kbd_names = $build->get_absolute_path($build->{media}{bootloader_kbds}); -e $kbd_names or die "ERROR: cannot find bootloader keyboard name file $kbd_names\n"; my @kbds = group_by2(eval(cat_($kbd_names))) or die "ERROR: error in keyboard name file $kbd_names\n"; MDK::Common::File::output_utf8($grub2_dir . '/kbd-menu.cfg', build_kbd_menu_cfg(\@kbds)); } # Copy any message translation files the user has provided. my $messages = $build->get_absolute_path($build->{media}{bootloader_messages}); if (defined $messages) { -d $messages or die "ERROR: cannot find bootloader messages directory $messages\n"; my $locale_dir = $grub2_dir . '/locale'; mkdir_p($locale_dir); cp_f(glob($messages . '/*.mo'), $locale_dir); } # If the user has supplied a grub2 image for non-EFI boot, copy that, # otherwise build one. my $eltorito_img = $build->get_absolute_path($build->{media}{eltorito_img}); if (defined $eltorito_img) { -e $eltorito_img or die "ERROR: cannot find El Torito boot image $eltorito_img\n"; cp_f($eltorito_img, $grub2_dir . '/eltorito.img'); } else { build_grub2_eltorito_img($grub2_dir . '/eltorito.img'); } my $label = $build->{media}{label}; # If the user has supplied a top-level grub2 configuration file, copy that # (replacing the "VOLUME_LABEL" template with the actual label for the ISO # image), otherwise build one. my $grub2_cfg = $grub2_dir . '/grub.cfg'; if (defined $build->{media}{grub2_cfg}) { my $grub_cfg_template = $build->get_absolute_path($build->{media}{grub2_cfg}); -e $grub_cfg_template or die "ERROR: cannot find grub2 config file $grub_cfg_template\n"; cp_f($grub_cfg_template, $grub2_cfg); run_('sed', '-i', "s/VOLUME_LABEL/$label/g", $grub2_cfg); } else { output($grub2_cfg, build_grub2_cfg($build, $theme_name, \@theme_fonts, $add_lang_menu, $add_kbd_menu)); } my $title = $build->{media}{bootloader_title} || $label =~ s/-/ /gr; my $mode = $arch eq 'x86_64' ? '64-bit' : '32-bit'; # If we have a theme, replace the menu title with the name of the ISO # (extracted from the disk label). my $base_theme_txt = $grub2_dir . "/themes/$theme_name/theme.txt"; if (-e $base_theme_txt) { run_('sed', '-i', qq(s/title-text:.*/title-text: "$title ($mode)"/), $base_theme_txt); } my $efi_type = $build->{media}{efi_type} || 'none'; member($efi_type, qw(none 32bit 64bit all)) or die "ERROR: unrecognised EFI type '$efi_type'\n"; # If we are building a legacy-boot only ISO, we are done. return if $efi_type eq 'none'; # Create another build directory. This will contain all the files we need # to exist in the /EFI directory on the ISO. my $efi_root_dir = $build->get_build_dir('EFI'); my $efi_boot_dir = $efi_root_dir . '/BOOT'; mkdir_p($efi_boot_dir); if ($efi_type ne '64bit') { # If the user has supplied a grub2 image for 32-bit EFI boot, copy that, # otherwise build one. my $boot_efi = $build->get_absolute_path($build->{media}{boot32_efi}); if (defined $boot_efi) { -e $boot_efi or die "ERROR: cannot find EFI boot image $boot_efi\n"; cp_f($boot_efi, $efi_boot_dir . '/'); } else { build_grub2_boot_efi($efi_boot_dir . '/bootia32.efi', 'i386-efi'); } } if ($efi_type ne '32bit') { # If the user has supplied a grub2 image for 64-bit EFI boot, copy that, # otherwise build one. my $boot_efi = $build->get_absolute_path($build->{media}{boot64_efi}); if (defined $boot_efi) { -e $boot_efi or die "ERROR: cannot find EFI boot image $boot_efi\n"; cp_f($boot_efi, $efi_boot_dir . '/'); } else { build_grub2_boot_efi($efi_boot_dir . '/bootx64.efi', 'x86_64-efi'); } } # Build a grub2 configuration file for EFI boot. This just chains to the # main grub2 configuration file. output($efi_boot_dir . '/grub.cfg', build_efi_grub2_cfg($build)); # If we have a theme, duplicate the theme configuration file and modify the # title string to indicate we are doing a EFI boot. This is useful when # dealing with user bug reports... if (-e $base_theme_txt) { my $efi_theme_txt = $grub2_dir . "/themes/$theme_name/theme-efi.txt"; cp_f($base_theme_txt, $efi_theme_txt); run_('sed', '-i', qq(s/title-text:.*/title-text: "$title ($mode EFI)"/), $efi_theme_txt); } # Create another build directory for temporarily storing the ESP image. my $images_dir = $build->get_build_dir('images'); mkdir_p($images_dir); # Construct an ESP image. This is needed for USB boot. # Give it a label, to try to work around mga#23939. my $esp_image = $images_dir . '/esp.img'; eval { rm_rf($esp_image) }; run_('/sbin/mkdosfs', '-n', 'MGALIVE-ESP', '-F12', '-C', $esp_image, '4096'); run_('mcopy', '-s', '-i', $esp_image, $efi_root_dir, '::'); # Now we've built the ESP image, we can delete the grub2 image. We need # to leave the grub2.cfg file, as DVD boot sets the initial grub2 root # location to the iso9960 partition, not the ESP. rm_rf(glob($efi_boot_dir . '/*.efi')); } sub build_grub2_eltorito_img { my ($output) = @_; my @modules = qw(biosdisk iso9660 fat part_msdos all_video font gfxterm gfxmenu png configfile echo gettext linux linux16 ls search test); run_('grub2-mkimage', '--output', $output, '--prefix', '/boot/grub2', '--format', 'i386-pc-eltorito', @modules ); } sub build_grub2_boot_efi { my ($output, $format) = @_; my @modules = qw(iso9660 fat part_msdos all_video font gfxterm gfxmenu png configfile echo gettext linux linuxefi ls search test); run_('grub2-mkimage', '--output', $output, '--prefix', '/EFI/BOOT', '--format', $format, @modules ); } sub build_grub2_cfg { my ($build, $theme_name, $theme_fonts, $add_lang_menu, $add_kbd_menu) = @_; my @loadfonts; if (defined $theme_name) { @loadfonts = map { " loadfont \$grub2/themes/$theme_name/$_" } @$theme_fonts; } my $gettext = $add_lang_menu ? '$' : ''; my %initrd_command = ( 'linuxefi' => 'initrdefi', 'linux16' => 'initrd16', 'linux' => 'initrd', '$linux' => '$initrd' ); join("\n", "if [ -z \$initialised ] ; then", " set grub2=/boot/grub2", " export grub2", "", if_($build->{media}{overlay_label}, " set kernel=no_choice", " export kernel", "", " search --no-floppy --set=overlay -l '" . $build->{media}{overlay_label} . "'", " export overlay", " if [ ! -z \$overlay ] ; then", " search --no-floppy --set=partition -f /memory/boot/vmlinuz --hint \$overlay", " if [ x\$partition == x\$overlay ] ; then", " set kernel=latest", " fi", " fi", "", ), " if loadfont \$grub2/fonts/unicode.pf2 ; then", " set gfxmode=1024x768,800x600,auto", " set gfxpayload=keep", " terminal_output gfxterm", " fi", if_($theme_name, "", " if [ \$grub_platform == 'efi' ] ; then", " set theme=\$grub2/themes/$theme_name/theme-efi.txt", " else", " set theme=\$grub2/themes/$theme_name/theme.txt", " fi", " export theme", @loadfonts, "", ), " if [ \$grub_platform == 'efi' ] ; then", " set linux=linuxefi", " set initrd=initrdefi", " else", " set linux=linux16", " set initrd=initrd16", " fi", " export linux", " export initrd", "", " set initialised=true", " export initialised", "fi", "", "set boot=/boot", if_($build->{media}{overlay_label}, "if [ \$kernel == 'latest' ] ; then", " set boot=(\$overlay)/memory/boot", "fi", ), "", "set default=" . get_bootloader_default($build), "set timeout=" . get_bootloader_timeout($build), "", if_($add_lang_menu, "export lang", "export lkbd", "", ), if_($add_kbd_menu, "export kbd", "", ), (map { my ($name, $options) = @$_; my $command = $options->{command} || '$linux'; my $image = $options->{image} || get_default_image($build, $name); my $initrd = $options->{initrd} || get_default_initrd($build); my $append = $options->{append}; join("\n", if_($command eq 'linuxefi', "if [ \$grub_platform == 'efi' ] ; then", # EFI only ), if_($command eq 'linux16', "if [ \$grub_platform != 'efi' ] ; then", # EFI doesn't support 16-bit ), "menuentry $gettext\"$name\" {", " $command $image " . join(' ', get_default_append($build), $append), if_($initrd ne 'none' && defined $initrd_command{$command}, " $initrd_command{$command} $initrd", ), "}", if_($command eq 'linuxefi' || $command eq 'linux16', "fi", ), ); } group_by2(@{$build->{media}{bootloader_entries}})), if_($build->{media}{overlay_label} || $add_lang_menu || $add_kbd_menu, # this acts as a spacer "menuentry '________________________' {", " set dummy=true", "}", ), if_($build->{media}{overlay_label}, "if [ \$kernel != 'no_choice' ] ; then", " if [ \$kernel == 'latest' ] ; then", " menuentry \"F1: \"$gettext\"Kernel [latest]\" --id kernel --hotkey f1 {", " set kernel=original", " configfile \$grub2/grub.cfg", " }", " else", " menuentry \"F1: \"$gettext\"Kernel [original]\" --id kernel --hotkey f1 {", " set kernel=latest", " configfile \$grub2/grub.cfg", " }", " fi", "fi", ), if_($add_lang_menu, "submenu \"F2: \"$gettext\"Language [\$lang]\" --id language --hotkey f2 {", " source \$grub2/lang-menu.cfg", "}", ), if_($add_kbd_menu, "submenu \"F3: \"$gettext\"Keyboard [\$kbd]\" --id keyboard --hotkey f3 {", " source \$grub2/kbd-menu.cfg", "}", ), "", ); } sub get_bootloader_default { my ($build) = @_; defined $build->{media}{bootloader_default} ? $build->{media}{bootloader_default} : 0; } sub get_bootloader_timeout { my ($build) = @_; defined $build->{media}{bootloader_timeout} ? $build->{media}{bootloader_timeout} : 4; } sub get_default_image { my ($build, $name) = @_; -e ($build->get_build_dir('boot') . '/vmlinuz') && '$boot/vmlinuz' or die "ERROR: no boot image found for '$name' boot entry\n"; } sub get_default_initrd { my ($build) = @_; -e ($build->get_build_dir('boot') . '/initrd.img') && '$boot/initrd.img' || -e ($build->get_build_dir('boot') . '/all.rdz') && '$boot/all.rdz'; } sub get_default_append { my ($build) = @_; join(" ", "lang=\$lang kbd=\$kbd", if_($build->{system}{append}, $build->{system}{append}), if_($build->{system}{vga_mode}, "vga=" . $build->{system}{vga_mode}), ); } sub build_lang_menu_cfg { my ($langs, %kbds) = @_; join("\n", "function set_language {", " set lang=\$1", " set lkbd=\$2", " configfile \$grub2/grub.cfg", "}", "", "set default=\$lang", "set timeout=-1", "", "menuentry \$\"[more options after boot]\" { set_language '' '' }", (map { my ($id, $name) = @$_; my $kbd = $kbds{$id}; "menuentry '$name' --id $id { set_language $id $kbd }"; } (@$langs)), "", ); } sub build_kbd_menu_cfg { my ($kbds) = @_; join("\n", "function set_keyboard {", " set kbd=\$1", " set lkbd=", " configfile \$grub2/grub.cfg", "}", "", "if [ -z \$kbd ] ; then", " set default=\$lkbd", "else", " set default=\$kbd", "fi", "", "set timeout=-1", "", (map { my ($id, $name) = @$_; $name =~ s/"/\\"/g; "menuentry \$\"$name\" --id $id { set_keyboard $id }"; } (@$kbds)), "", ); } sub build_efi_grub2_cfg { my ($build) = @_; join("\n", "search --no-floppy --set=root -l '" . $build->{media}{label} . "'", "set prefix=(\$root)/boot/grub2", "", ". \$prefix/grub.cfg", "", ); } 1;