diff options
Diffstat (limited to 'perl-install/bootloader.pm')
-rw-r--r-- | perl-install/bootloader.pm | 3414 |
1 files changed, 2632 insertions, 782 deletions
diff --git a/perl-install/bootloader.pm b/perl-install/bootloader.pm index c2a75e8dc..d1d757c3d 100644 --- a/perl-install/bootloader.pm +++ b/perl-install/bootloader.pm @@ -1,1004 +1,2854 @@ -package bootloader; # $Id$ +package bootloader; use diagnostics; use strict; -use vars qw(%vga_modes); #-###################################################################################### #- misc imports #-###################################################################################### use common; -use partition_table qw(:types); +use fs::type; +use fs::get; +use fs::loopback; +use fs::proc_partitions; use log; use any; -use fsedit; use devices; -use loopback; use detect_devices; -use partition_table_raw; +use partition_table::raw; use run_program; use modules; +=head1 SYNOPSYS -%vga_modes = ( - "Ask at boot" => 'ask', - "Normal" => 'normal', - "80x50" => '0x0f01', - "80x43" => '0x0f02', - "80x28" => '0x0f03', - "80x30" => '0x0f04', - "80x34" => '0x0f05', - "80x60" => '0x0f06', - "100x30" => '0x0122', - "640x480 in 16 bits (FrameBuffer only)" => 785, - "800x600 in 16 bits (FrameBuffer only)" => 788, - "1024x768 in 16 bits (FrameBuffer only)" => 791, - "1280x1024 in 16 bits (FrameBuffer only)" => 794, -); - -my %vga_modes2nb = ( - 'ask' => -3, - 'normal' => -1, -#- other entries are identity -); - - -#-##################################################################################### -#- Functions -#-##################################################################################### +B<bootloader> enables to configure various boot loaders (LILO, GRUB Legacy, GRUB2, ...) + +Example of usage: + + $all_hds = fsedit::get_hds(); + fs::get_raw_hds('', $all_hds); + fs::get_info_from_fstab($all_hds); + $fstab = [ fs::get::fstab($all_hds) ]; + $bootloader = bootloader::read($all_hds); + (...) + bootloader::action($bootloader, 'write', $all_hds); + +A particular bootloader is handled in separate typical functions that can be called by action() by /sbin/bootloader-config: + + +=head1 VFS aka how to support a bootloader + +Most of the code is generic. + +A particular bootloader implementation/support is done through a couple functions: + +=over 4 + +=item * read_FOOBAR() + +read the config file(s) of the FOOBAR bootloader, returning a $bootloader object + +=item * install_FOOBAR($bootloader, $all_hds) + +usually call write_FOOBAR() & install_raw_FOOBAR() if existing, aka: + +=over 4 + +=item * updating config files + +=item * then reinstalling the bootloader + +=back + +=item * install_raw_FOOBAR() + +Optionnal: Low level installation of the stage1 of the bootloader, usually embedding it in MBR or on ESP, ... (eg: running lilo or /boot/grub*/install.sh) + +=item * write_FOOBAR($bootloader, $all_hds) + +update the config files, ie when a kernel was installed or removed. +when_config_changed_FOOBAR() is called right after in case something is needed when config is done (eg: lilo needs to be reinstalled) + +=item * when_config_changed_FOOBAR() + +Run needed action when config has changed or when initrds were regenerated, ... +Usually nothing for intelligent bootloaders such as grub-legacy or grub2. +But eg: lilo needs to rewrite its stage1 (thus it needs to rerun lilo). + +=back + +=head1 Functions + +=over + +=cut + +my $vmlinuz_regexp = 'vmlinu[xz]|win4lin|uImage'; +my $decompose_vmlinuz_name = qr/((?:$vmlinuz_regexp).*?)-(\d+\.\d+.*)/; +my $liblinux_path = '/usr/lib/linux'; + +sub expand_vmlinuz_symlink { + my ($vmlinuz) = @_; + my $f = $::prefix . ($vmlinuz =~ m!^/! ? $vmlinuz : "/boot/$vmlinuz"); + -l $f ? readlink($f) : $vmlinuz; +} + +sub installed_vmlinuz_raw() { grep { /^($vmlinuz_regexp)/ } all("$::prefix/boot") } +sub installed_vmlinuz() { grep { ! -l "$::prefix/boot/$_" } installed_vmlinuz_raw() } +sub vmlinuz2version { + my ($vmlinuz) = @_; + expand_vmlinuz_symlink($vmlinuz) =~ /$decompose_vmlinuz_name/ && $2; +} +sub vmlinuz2kernel_str { + my ($vmlinuz) = @_; + my ($basename, $version) = expand_vmlinuz_symlink($vmlinuz) =~ /$decompose_vmlinuz_name/ or return; + { + basename => $basename, + version => $version, + $version =~ /([\d.]*)-(\D.*)-((\d+|0\.rc\d+.*)\.mga.*)$/ ? #- eg: 3.0.0-1.mga2 + (ext => $2, version_no_ext => "$1-$3") : + $version =~ /(.*mga)-?(.*)/ ? #- (old) eg: 2.6.17-13mdventerprise + (ext => $2, version_no_ext => $1) : (version_no_ext => $version), + }; +} + +sub kernel_str2short_name { + my ($kernel) = @_; + $kernel->{basename}; +} + +sub basename2initrd_basename { + my ($basename) = @_; + $basename =~ s!(vmlinu[zx]|uImage)-?!!; #- here we do not use $vmlinuz_regexp since we explicitly want to keep all that is not "vmlinuz" + 'initrd' . ($basename ? "-$basename" : ''); +} +sub kernel_str2vmlinuz_long { + my ($kernel) = @_; + $kernel->{basename} . '-' . $kernel->{version}; +} +sub kernel_str2initrd_long { + my ($kernel) = @_; + basename2initrd_basename(kernel_str2short_name($kernel)) . '-' . $kernel->{version} . '.img'; +} +sub kernel_str2liblinux_long { + my ($kernel) = @_; + $liblinux_path . '-' . $kernel->{version}; +} +sub kernel_str2vmlinuz_short { + my ($kernel) = @_; + if ($kernel->{use_long_name}) { + kernel_str2vmlinuz_long($kernel); + } else { + kernel_str2short_name($kernel); + } +} +sub kernel_str2initrd_short { + my ($kernel) = @_; + if ($kernel->{use_long_name}) { + kernel_str2initrd_long($kernel); + } else { + basename2initrd_basename(kernel_str2short_name($kernel)) . '.img'; + } +} +sub kernel_str2liblinux_short() { + $liblinux_path; +} + +sub kernel_str2label { + my ($kernel, $o_use_long_name) = @_; + if ($o_use_long_name || $kernel->{use_long_name}) { + _sanitize_ver($kernel); + } else { + my $short_name = kernel_str2short_name($kernel); + $kernel->{ext} =~ /^xen/ ? 'xen' : ($short_name eq 'vmlinuz' ? 'linux' : $short_name); + } +} sub get { - my ($kernel, $bootloader) = @_; - $_->{kernel_or_dev} && $_->{kernel_or_dev} eq $kernel and return $_ foreach @{$bootloader->{entries}}; + my ($vmlinuz, $bootloader) = @_; + $_->{kernel_or_dev} && $_->{kernel_or_dev} eq $vmlinuz and return $_ foreach @{$bootloader->{entries}}; undef; } sub get_label { my ($label, $bootloader) = @_; - $_->{label} && $_->{label} eq $label and return $_ foreach @{$bootloader->{entries}}; + $_->{label} && lc(make_label_lilo_compatible($_->{label})) eq lc(make_label_lilo_compatible($label)) and return $_ foreach @{$bootloader->{entries}}; undef; } -sub mkinitrd($$$) { - my ($prefix, $kernelVersion, $initrdImage) = @_; +=item mkinitrd($kernel_version, $bootloader, $entry, $initrd) + +Regenerates kernel's initrd. - $::testing || -e "$prefix/$initrdImage" and return; +=cut - my $loop_boot = loopback::prepare_boot($prefix); +sub mkinitrd { + my ($kernel_version, $bootloader, $entry, $initrd) = @_; + + my $dir = dirname($initrd); + if ($::testing) { + log::l("Skipping initrd generation: testing mode"); + return $initrd; + } elsif (-e "$::prefix/$initrd") { + log::l("Skipping initrd generation: already exists"); + return $initrd; + } elsif ($initrd =~ /\(hd/) { + log::l("Skipping initrd generation: unrecognized partition"); + return $initrd; + } elsif (!-d "$::prefix/$dir") { + log::l("Skipping initrd generation: dir doesn't exist (probably !mounted foreign part)"); + return $initrd; + } + + # for /boot on dos partitions when installing on loopback file on dos partition + my $loop_boot = fs::loopback::prepare_boot(); modules::load('loop'); - run_program::rooted($prefix, "mkinitrd", "-v", "-f", $initrdImage, "--ifneeded", $kernelVersion) or unlink("$prefix/$initrdImage"); + my @options = ( + if_($::isInstall, "-v"), "-f", $initrd, $kernel_version, + if_($entry->{initrd_options}, split(' ', $entry->{initrd_options})), + ); + + my $err; + if (!run_program::rooted($::prefix, 'mkinitrd', '2>', \$err, @options)) { + unlink("$::prefix/$initrd"); + log::explanations("mkinitrd failed:\n(mkinitrd @options)\nError: <$err>"); + die "mkinitrd failed:\n(mkinitrd @options)\nError: $err"; + } + add_boot_splash($initrd, $entry->{vga} || $bootloader->{vga}); + + fs::loopback::save_boot($loop_boot); + + -e "$::prefix/$initrd" && $initrd; +} + +=item rebuild_initrd($kernel_version, $bootloader, $entry, $initrd) + +Saves the old initrd then regenerate it. +If it fails, restore the old initrd. + +=cut + +sub rebuild_initrd { + my ($kernel_version, $bootloader, $entry, $initrd) = @_; + + my $old = $::prefix . $entry->{initrd} . '.old'; + unlink $old; + rename "$::prefix$initrd", $old; + if (!mkinitrd($kernel_version, $bootloader, $entry, $initrd)) { + log::l("rebuilding initrd failed, putting back the old one"); + rename $old, "$::prefix$initrd"; + } +} + +sub remove_boot_splash { + my ($initrd) = @_; + run_program::rooted($::prefix, '/usr/share/bootsplash/scripts/remove-boot-splash', $initrd); +} +sub add_boot_splash { + my ($initrd, $vga) = @_; + + $vga or return; + + eval { require Xconfig::resolution_and_depth } or return; + if (my $res = Xconfig::resolution_and_depth::from_bios($vga)) { + run_program::rooted($::prefix, '/usr/share/bootsplash/scripts/make-boot-splash', $initrd, $res->{X}); + } else { + log::l("unknown vga bios mode $vga"); + } +} +sub update_splash { + my ($bootloader) = @_; + + my %real_initrd_entries; + foreach (@{$bootloader->{entries}}) { + if ($_->{initrd} && $_->{vga}) { + my $initrd = expand_symlinks($_->{initrd}); + $real_initrd_entries{$initrd} = $_; + } + } + foreach (values %real_initrd_entries) { + log::l("add boot splash to $_->{initrd}\n"); + add_boot_splash($_->{initrd}, $_->{vga} || $bootloader->{vga}); + } +} + +=item read($all_hds) + +Reads bootloader config by calling the proper read_XYZ function. + +=cut + +sub read { + my ($all_hds) = @_; + my $fstab = [ fs::get::fstab($all_hds) ]; + foreach my $main_method (main_method_choices()) { + my $f = $bootloader::{"read_$main_method"} or die "unknown bootloader method $main_method (read)"; + my $bootloader = $f->($fstab); + + cleanup_entries($bootloader) if $main_method ne 'refind'; + + # handle raid-extra-boot (lilo) + my @devs = $bootloader->{boot}; + if ($bootloader->{'raid-extra-boot'} =~ /mbr/ && + (my $md = fs::get::device2part($bootloader->{boot}, $all_hds->{raids}))) { + @devs = map { $_->{rootDevice} } @{$md->{disks}}; + } elsif ($bootloader->{'raid-extra-boot'} =~ m!/dev/!) { + @devs = split(',', $bootloader->{'raid-extra-boot'}); + } + + #- if $bootloader->{boot} is undefined, @devs contains a single undef element + my ($type) = map { + if (is_uefi()) { + if (-e "$::prefix/boot/refind_linux.conf") { + 'refind'; + } elsif (-f "$::prefix/boot/EFI/EFI/mageia/grub" . uefi_type() . ".efi" || + $main_method eq 'grub2' && $bootloader->{removable} && -f "$::prefix/boot/EFI/EFI/BOOT/BOOT" . uc(uefi_type()) . ".EFI") { + 'grub2'; + } else { + () + } + } elsif (m!/fd\d+$!) { + warn "not checking the method on floppy, assuming $main_method is right\n"; + $main_method; + } elsif (member($main_method, qw(cromwell uboot))) { + #- not checking, there's only one bootloader anyway :) + $main_method; + } elsif (my $type = partition_table::raw::typeOfMBR($_)) { + warn "typeOfMBR $type on $_ for method $main_method\n" if $ENV{DEBUG}; + $type; + } else { () } + } @devs; + + if ($type eq $main_method) { + return $bootloader if read_($bootloader); + } + } +} + +sub read_ { + my ($bootloader) = @_; + my @prefered_entries = map { get_label($_, $bootloader) } $bootloader->{default}, 'linux'; + + if (my $default = find { $_ && $_->{type} eq 'image' } (@prefered_entries, @{$bootloader->{entries}})) { + $bootloader->{default_options} = $default; + $bootloader->{perImageAppend} ||= $default->{append}; + log::l("perImageAppend is now $bootloader->{perImageAppend}"); + $bootloader->{default_vga} ||= $default->{vga}; + log::l("default_vga is now $bootloader->{default_vga}"); + } else { + $bootloader->{default_options} = {}; + } + return $bootloader; +} + + +=item is_grub2_already_crypted($password) - loopback::save_boot($loop_boot); +Returns whether grub2 password is already encrypted or not - -e "$prefix/$initrdImage" or die "mkinitrd failed"; +=cut + +sub is_grub2_already_crypted { + my ($password) = @_; + $password =~ /grub.pbkdf2.sha512/; +} + +=item read_grub2 ($o_fstab) + +Read back GRUB2 config + +=cut + +sub read_grub2() { + my %bootloader = read_grub2_install_sh() if -e get_grub2_install_sh(); + return if is_empty_hash_ref(\%bootloader) & !-s "$::prefix/boot/grub2/grub.cfg"; + my %h = getVarsFromSh("$::prefix/etc/default/grub"); + $bootloader{timeout} = $h{GRUB_TIMEOUT}; + # keep suggested perImageAppend and default_vga on first run (during installer) or when migrating from grub-legacy or lilo: + my ($vga, $other) = partition { /^vga=/ } split(' ', $h{GRUB_CMDLINE_LINUX_DEFAULT}); + $bootloader{perImageAppend} ||= join(' ', @$other) if @$other; + $bootloader{default_vga} ||= $vga->[0] =~ /vga=(.*)/ && $1 if @$vga; + $bootloader{entries} = []; + my $entry; + my $f = "$::prefix/boot/grub2/grub.cfg"; + my @menus; + foreach (cat_utf8($f)) { + next if /^#/; + if (/menuentry\s+['"]([^']+)["']/) { + $entry = { label => $1, real_label => join('>', @menus, $1) }; + } elsif (/linux(?:16)?\s+([^-]\S+)\s+(.*)?/ || /module\s+(\S+vmlinu\S+)\s+(.*)?/) { + $entry->{type} = 'image'; + @$entry{qw(kernel_or_dev append)} = ($1, $2); + my ($vga, $other) = partition { /^vga=/ } split(' ', $entry->{append}); + if (@$vga) { + $entry->{vga} = $vga->[0] =~ /vga=(.*)/ && $1; + $entry->{append} = join(' ', @$other); + } + } elsif (/initrd(?:16)?\s+(\S+)/ || /module\s+(\S+initrd\S+)\s+(.*)?/) { + $entry->{initrd} = $1; + } elsif (/submenu\s+['"]([^']+)["']/) { + push @menus, $1; + } elsif (/^\s*}/) { + if ($entry) { + push @{$bootloader{entries}}, $entry; + undef $entry; + } else { + pop @menus; + } + } + } + + # get default entry: + foreach (run_program::rooted_get_stdout($::prefix, qw(grub2-editenv list))) { + $bootloader{default} = $1 if /saved_entry=(.*)/; + $bootloader{default} =~ s/.*>//; # strip full menu entry path + } + + # Get password prior to run update-grub2: + $bootloader{password} = { getVarsFromSh(get_grub2_users()) }->{GRUB2_PASSWORD}; + + $bootloader{method} = cat_($f) =~ /set theme=.*maggy/ ? 'grub2-graphic' : 'grub2'; + \%bootloader; +} + +sub read_grub2_install_sh() { + my $s = cat_(get_grub2_install_sh()); + my %h; + if ($s =~ m!(/dev/\S+)!m) { + $h{boot} = $1; + } + $h{no_esp_or_mbr} = $s =~ m!--grub-setup=/bin/true!; + $h{removable} = $s =~ m!--removable!; + %h; +} + +=item read_grub($fstab) + +Reads back Grub Legacy config. + +=cut + +sub read_grub { + my ($fstab) = @_; + + my $grub2dev = read_grub_device_map(); + my $boot_root = read_grub_install_sh(); + _may_fix_grub2dev($fstab, $grub2dev, $boot_root->{boot_part}); + + my $bootloader = read_grub_menu_lst($fstab, $grub2dev) or return; + + if ($boot_root->{boot}) { + $bootloader->{boot} = grub2dev($boot_root->{boot}, $grub2dev); + } + + $bootloader; } -sub mkbootdisk($$$;$) { - my ($prefix, $kernelVersion, $dev, $append) = @_; - modules::load_multi(arch() =~ /sparc/ ? 'romfs' : (), 'loop'); - my @l = qw(mkbootdisk --noprompt); - push @l, "--appendargs", $append if $append; - eval { modules::load('vfat') }; - run_program::rooted_or_die($prefix, @l, "--device", "/dev/$dev", $kernelVersion); +=item _may_fix_grub2dev($fstab, $grub2dev, $boot_part) + +Adapts device.map (aka $grub2dev) when for example hda is now sda. +nb: + +=over 4 + +=item * $boot_part comes from C</boot/grub/install.sh> C<root (hd...)> line + +=item * $grub2dev is C</boot/grub/device.map> + +=back + +=cut + +sub _may_fix_grub2dev { + my ($fstab, $grub2dev, $boot_part) = @_; + + $boot_part or log::l("install.sh does not contain 'root (hd...)' line, no way to magically adapt device.map"), return; + + my $real_boot_part = fs::get::root_($fstab, 'boot') or + log::l("argh... the fstab given is useless, it doesn't contain '/'"), return; + + my $real_boot_dev = $real_boot_part->{rootDevice} or return; # if /boot is on Linux RAID 1, hope things are all right... + + if (my $prev_boot_part = fs::get::device2part(grub2dev($boot_part, $grub2dev), $fstab)) { # the boot_device as far as grub config files say + $real_boot_part == $prev_boot_part and return; + } + + log::l("WARNING: we have detected that device.map is inconsistent with the system"); + + my ($hd_grub, undef, undef) = parse_grub_file($boot_part); # extract hdX + if (my $prev_hd_grub = find { $grub2dev->{$_} eq $real_boot_dev } keys %$grub2dev) { + $grub2dev->{$prev_hd_grub} = $grub2dev->{$hd_grub}; + log::l("swapping result: $hd_grub/$real_boot_dev and $prev_hd_grub/$grub2dev->{$hd_grub}"); + } else { + log::l("argh... can't swap, setting $hd_grub to $real_boot_dev anyway"); + } + $grub2dev->{$hd_grub} = $real_boot_dev; +} + +=item read_grub_install_sh() { + +Reads "config" from /boot/grub/install.sh (mainly used partitions) + +=cut + +sub read_grub_install_sh() { + my $s = cat_("$::prefix/boot/grub/install.sh"); + my %h; + + #- matches either: + #- setup (hd0) + #- install (hd0,0)/boot/grub/stage1 d (hd0) (hd0,0)/boot/grub/stage2 p (hd0,0)/boot/grub/menu.lst + if ($s =~ /^(?:setup.*|install\s.*\sd)\s+(\(.*?\))/m) { + $h{boot} = $1; + } + if ($s =~ /^root\s+(\(.*?\))/m) { + $h{boot_part} = $1; + } + \%h; } -sub read($$) { - my ($prefix, $file) = @_; +sub _parse_grub_menu_lst() { my $global = 1; - my ($e, $v, $f); - my %b; - foreach (cat_("$prefix$file")) { - ($_, $v) = /^\s*(.*?)\s*(?:=\s*(.*?))?\s*$/; + my ($e, %b); + + my $menu_lst_file = "$::prefix/boot/grub/menu.lst"; + -e $menu_lst_file or return; + + foreach (MDK::Common::File::cat_utf8($menu_lst_file)) { + my $verbatim = $_; + chomp; + s/^\s*//; s/\s*$//; + next if /^#/ || /^$/; + my ($keyword, $v) = split('[ \t=]+', $_, 2) or + warn qq(unknown line in /boot/grub/menu.lst: "$_"\n), next; + + if ($keyword eq 'root') { + #- rename to avoid name conflict + $keyword = 'grub_root'; + } - if (/^(image|other)$/) { - push @{$b{entries}}, $e = { type => $_, kernel_or_dev => $v }; + if ($keyword eq 'title') { + push @{$b{entries}}, $e = { label => $v }; + $global = 0; + } elsif ($global) { + $b{$keyword} = $v; + } else { + if ($keyword eq 'kernel') { + $e->{type} = 'image'; + $e->{kernel} = $v; + } elsif ($keyword eq 'chainloader') { + $e->{type} = 'other'; + $e->{append} = ""; + } elsif ($keyword eq 'configfile') { + $e->{type} = 'grub_configfile'; + $e->{configfile} = $v; + } elsif ($keyword eq 'map') { + $e->{mapdrive}{$2} = $1 if $v =~ m/\((.*)\) \((.*)\)/; + } elsif ($keyword eq 'module') { + push @{$e->{modules}}, $v; + } else { + $e->{$keyword} = $v eq '' ? 1 : $v; + } + } + $e and $e->{verbatim} .= $verbatim; + } + + %b; +} + + +=item is_already_crypted($password) + +Returns whether grub password is already encrypted or not + +=cut + +sub is_already_crypted { + my ($password) = @_; + $password =~ /^--md5 (.*)/; +} + +=item read_grub_menu_lst($fstab, $grub2dev) + +Read config from /boot/grub/menu.lst + +=cut + +sub read_grub_menu_lst { + my ($fstab, $grub2dev) = @_; + + my %b = _parse_grub_menu_lst(); + + foreach my $keyword (grep { $_ ne 'entries' } keys %b) { + $b{$keyword} = $b{$keyword} eq '' ? 1 : grub2file($b{$keyword}, $grub2dev, $fstab, \%b); + } + + #- sanitize + foreach my $e (@{$b{entries}}) { + if (member($e->{type}, 'other', 'grub_configfile')) { + eval { $e->{kernel_or_dev} = grub2dev($e->{rootnoverify} || $e->{grub_root}, $grub2dev) }; + $e->{keep_verbatim} = 1 unless $e->{kernel_or_dev}; + } elsif ($e->{initrd}) { + my $initrd; + eval { $initrd = grub2file($e->{initrd}, $grub2dev, $fstab, $e) }; + if ($initrd) { + $e->{initrd} = $initrd; + } else { + $e->{keep_verbatim} = 1; + } + } + + if ($e->{kernel} =~ /xen/ && @{$e->{modules} || []} == 2 && $e->{modules}[1] =~ /initrd/) { + (my $xen, $e->{xen_append}) = split(' ', $e->{kernel}, 2); + ($e->{kernel}, my $initrd) = @{delete $e->{modules}}; + $e->{xen} = grub2file($xen, $grub2dev, $fstab, $e); + $e->{initrd} = grub2file($initrd, $grub2dev, $fstab, $e); + } + if (my $v = delete $e->{kernel}) { + (my $kernel, $e->{append}) = split(' ', $v, 2); + $e->{append} = join(' ', grep { !/^BOOT_IMAGE=/ } split(' ', $e->{append})); + $e->{root} = $1 if $e->{append} =~ s/root=(\S*)\s*//; + eval { $e->{kernel_or_dev} = grub2file($kernel, $grub2dev, $fstab, $e) }; + $e->{keep_verbatim} = 1 if !$e->{kernel_or_dev} || dirname($e->{kernel_or_dev}) ne '/boot'; + } + my ($vga, $other) = partition { /^vga=/ } split(' ', $e->{append}); + if (@$vga) { + $e->{vga} = $vga->[0] =~ /vga=(.*)/ && $1; + $e->{append} = join(' ', @$other); + } + } + + $b{nowarn} = 1; + # handle broken installkernel -r: + if (@{$b{entries}}) { + $b{default} = min($b{default}, scalar(@{$b{entries}}) - 1); + $b{default} = $b{entries}[$b{default}]{label}; + } + $b{method} = $b{gfxmenu} ? 'grub-graphic' : 'grub-menu'; + + \%b; +} + +sub _parse_extlinux_conf() { + my $global = 1; + my ($e, %b); + + my $f = "$::prefix/boot/extlinux/extlinux.conf"; + -e $f or return; + + foreach (cat_utf8($f)) { + chomp; + s/^\s*//; s/\s*$//; + next if /^#/ || /^$/; + my ($keyword, $v) = split('[ \t=]+', $_, 2) or + warn qq(unknown line in $f: "$_"\n), next; + + $keyword = lc($keyword); + + if ($keyword eq 'label') { + push @{$b{entries}}, $e = { label => $v }; $global = 0; } elsif ($global) { - $b{$_} = $v || 1; + $b{$keyword} = $v; } else { - if ((/map-drive/ .. /to/) && /to/) { + if ($keyword eq 'kernel') { + $e->{type} = 'image'; + $e->{kernel_or_dev} = $v; + } else { + $e->{$keyword} = $v; + } + } + } + + %b; +} + +sub read_uboot() { + my %b = _parse_extlinux_conf(); + + $b{method} = 'uboot'; + $b{timeout} //= 30; + + my $is_raspberry = cat_("/proc/device-tree/model") =~ /^Raspberry Pi/; + if ($is_raspberry) { + $b{perImageAppend} //= "8250.nr_uarts=1 console=ttyS0,115200 console=tty1 elevator=deadline cma=256M\@512M"; + } + + $b{perImageAppend} //= $b{entries}[0]{append}; + + \%b; +} + +=item read_refind ($fstab) + +Read back rEFInd config + C</boot/refind_linux.conf> + +=cut + +sub read_refind() { + my %bootloader = (entries => []); + foreach (cat_utf8("$::prefix/boot/refind_linux.conf")) { + next if /^#/; + my ($label, $append) = /"(.*)"\s*"(.*)"/; + my $root = $1 if $append =~ s/root=(\S*)\s*//; + my $vga = $1 if $append =~ s/vga=(\S*)\s*//; + if ($label && $root) { + push @{$bootloader{entries}}, { + type => 'image', + kernel_or_dev => '/boot/vmlinuz', + label => $label, + root => $root, + append => $append, + vga => $vga + }; + } + } + $bootloader{method} = 'refind'; + read_refind_config(\%bootloader); + \%bootloader; +} + +sub read_refind_config { + my ($bootloader) = @_; + + #- These are the rEFInd default values. + $bootloader->{use_nvram} = 1; + $bootloader->{banner_path} = ''; + $bootloader->{banner_scale} = 'noscale'; + + if (-r "$::prefix/boot/EFI/EFI/refind/refind.conf") { + read_refind_conf($bootloader, "$::prefix/boot/EFI/EFI/refind/refind.conf"); + } elsif (-r "$::prefix/boot/EFI/EFI/BOOT/refind.conf") { + read_refind_conf($bootloader, "$::prefix/boot/EFI/EFI/BOOT/refind.conf"); + } else { + #- This is the preferred value if rEFInd is not yet installed, + $bootloader->{banner_path} = 'refind_banner.png'; + } +} + +sub read_refind_conf { + my ($bootloader, $config_file) = @_; + foreach (cat_utf8($config_file)) { + if ($_ =~ /^\s*use_nvram\s+(false|off|0)/) { + $bootloader->{use_nvram} = 0; + } elsif ($_ =~ /^\s*banner\s+(\S*)/) { + $bootloader->{banner_path} = $1; + } elsif ($_ =~ /^\s*banner_scale\s+(\S*)/) { + $bootloader->{banner_scale} = $1; + } + } +} + +# FIXME: actually read back previous conf +sub read_cromwell() { + +{ method => 'cromwell' }; +} + + +sub read_lilo() { + my $bootloader = read_lilo_like("/etc/lilo.conf", sub { $_[0] }); + + delete $bootloader->{timeout} unless $bootloader->{prompt}; + $bootloader->{timeout} = $bootloader->{timeout} / 10 if $bootloader->{timeout}; + + my $submethod = member($bootloader->{install}, 'text', 'menu') ? $bootloader->{install} : 'menu'; + $bootloader->{method} = "lilo-$submethod"; + + $bootloader; +} +sub read_lilo_like { + my ($file, $filter_file) = @_; + + my $global = 1; + my ($e); + my %b; + -e "$::prefix$file" or return; + foreach my $line (cat_("$::prefix$file")) { + next if $line =~ /^\s*#/ || $line =~ /^\s*$/; + my ($cmd, $v) = $line =~ /^\s*([^=\s]+)\s*(?:=\s*(.*?))?\s*$/ or log::l("unknown line in $file: $line"), next; + + if ($cmd =~ /^(?:image|other|macos|macosx|bsd|darwin)$/) { + $v = $filter_file->($v); + push @{$b{entries}}, $e = { type => $cmd, kernel_or_dev => $v }; + $global = 0; + } elsif ($global) { + if ($cmd eq 'disk' && $v =~ /(\S+)\s+bios\s*=\s*(\S+)/) { + $b{bios}{$1} = $2; + } elsif ($cmd eq 'bios') { + $b{bios}{$b{disk}} = $v; + } elsif ($cmd eq 'init-message') { + $v =~ s/\\n//g; + $v =~ s/"//g; + $b{'init-message'} = $v; + } else { + $b{$cmd} = $v eq '' ? 1 : $v; + } + } else { + if (($cmd eq 'map-drive' .. $cmd eq 'to') && $cmd eq 'to') { $e->{mapdrive}{$e->{'map-drive'}} = $v; } else { - $e->{$_} = $v || 1; + if ($cmd eq 'initrd') { + $v = $filter_file->($v); + } + $e->{$cmd} = $v || 1; } } } - delete $b{timeout} unless $b{prompt}; - $_->{append} =~ s/^\s*"?(.*?)"?\s*$/$1/ foreach \%b, @{$b{entries}}; - $b{timeout} = $b{timeout} / 10 if $b{timeout}; - $b{message} = cat_("$prefix$b{message}") if $b{message}; + + sub remove_quotes_and_spaces { + local ($_) = @_; + s/^\s*//; s/\s*$//; + s/^"(.*?)"$/$1/; + s/\\"/"/g; + s/^\s*//; s/\s*$//; #- do it again for append=" foo" + $_; + } + + foreach ('append', 'root', 'default', 'raid-extra-boot') { + $b{$_} = remove_quotes_and_spaces($b{$_}) if $b{$_}; + } + foreach my $entry (@{$b{entries}}) { + foreach ('append', 'root', 'label') { + $entry->{$_} = remove_quotes_and_spaces($entry->{$_}) if $entry->{$_}; + } + if ($entry->{kernel_or_dev} =~ /\bmbootpack\b/) { + $entry->{initrd} = $entry->{kernel_or_dev}; + $entry->{initrd} =~ s/\bmbootpack/initrd/; + $entry->{kernel_or_dev} =~ s/\bmbootpack/vmlinuz/; + $entry->{kernel_or_dev} =~ s/.img$//; + #- assume only xen is configured with mbootpack + $entry->{xen} = '/boot/xen.gz'; + $entry->{root} = $1 if $entry->{append} =~ s/root=(\S*)\s*//; + ($entry->{xen_append}, $entry->{append}) = split '\s*--\s*', $entry->{append}, 2; + } + } + + # cleanup duplicate labels (in case file is corrupted) + @{$b{entries}} = uniq_ { $_->{label} } @{$b{entries}}; + \%b; } +sub cleanup_entries { + my ($bootloader) = @_; + + #- cleanup bad entries (in case file is corrupted) + @{$bootloader->{entries}} = + grep { + my $pb = $_->{type} eq 'image' && !$_->{keep_verbatim} && ! -e "$::prefix$_->{kernel_or_dev}"; + log::l("dropping bootloader entry $_->{label} since $_->{kernel_or_dev} doesn't exist") if $pb; + !$pb; + } @{$bootloader->{entries}}; +} + sub suggest_onmbr { - my ($hds) = @_; + my ($hd) = @_; - my $type = partition_table_raw::typeOfMBR($hds->[0]{device}); - !$type || member($type, qw(dos dummy lilo grub empty)), !$type; + my ($onmbr, $unsafe) = (1, 1); + + if (my $type = partition_table::raw::typeOfMBR($hd->{device})) { + if (member($type, qw(dos dummy empty))) { + $unsafe = 0; + } elsif (!member($type, qw(lilo grub grub2))) { + $onmbr = 0; + } + log::l("bootloader::suggest_onmbr: type $type, onmbr $onmbr, unsafe $unsafe"); + } + ($onmbr, $unsafe); } -sub compare_entries ($$) { + +=item allowed_boot_disks($bootloader, $all_hds) + +Returns list of disks where we can install the bootloader when not in UEFI mode +(accounting for misssing BIOS boot partitions on GPT disks) +in UEFI mode, grub2 automatically look for the ESP). + +=cut + +sub allowed_boot_disks { + my ($all_hds) = @_; + # GPT disks w/o a BIOS boot partition do not have free space for grub2 to embed: + grep { $_->{pt_table_type} ne 'gpt' || + any { isBIOS_GRUB($_) } map { partition_table::get_normal_parts($_) } $_; + } @{$all_hds->{hds}}; +} + +=item allowed_boot_parts($bootloader, $all_hds) + +Returns list of places where we can install the bootloader when not in UEFI mode +(in UEFI mode, grub2 automatically look for the ESP). + +=cut + +sub allowed_boot_parts { + my ($bootloader, $all_hds) = @_; + ( + allowed_boot_disks($all_hds), # MBR + + if_($bootloader->{method} =~ /lilo/, + grep { $_->{level} eq '1' } @{$all_hds->{raids}} + ), + (if_(main_method($bootloader->{method}) ne 'grub2', + grep { !isFat_or_NTFS($_) } fs::get::fstab($all_hds)), # filesystems except those who do not leave space for our bootloaders + ), + detect_devices::floppies(), + ); +} + +sub same_entries { my ($a, $b) = @_; - my %entries; - @entries{keys %$a, keys %$b} = (); - $a->{$_} eq $b->{$_} and delete $entries{$_} foreach keys %entries; - scalar keys %entries; + foreach (uniq(keys %$a, keys %$b)) { + if (member($_, 'label', 'append', 'mapdrive', 'readonly', 'makeactive', 'verbatim')) { + next; + } elsif ($_ eq 'grub_root' && (!$a->{$_} || !$b->{$_})) { + #- grub_root is mostly internal stuff. if it misses, it's ok + next; + } else { + next if $a->{$_} eq $b->{$_}; + + my ($inode_a, $inode_b) = map { (stat "$::prefix$_")[1] } ($a->{$_}, $b->{$_}); + next if $inode_a && $inode_b && $inode_a == $inode_b; + } + + log::l("entries $a->{label} do not have same $_: $a->{$_} ne $b->{$_}"); + return; + } + 1; } -sub add_entry($$) { - my ($entries, $v) = @_; - my (%usedold, $freeold); +sub add_entry { + my ($bootloader, $v) = @_; + + my $to_add = $v; + my $label = $v->{label}; + for (my $i = 0; $i < 100;) { + my $conflicting = get_label($label, $bootloader); + + $to_add->{label} = $label; - do { $usedold{$1 || 0} = 1 if $_->{label} =~ /^old([^_]*)_/ } foreach @$entries; - foreach (0..scalar keys %usedold) { exists $usedold{$_} or $freeold = $_ || '', last } + if ($conflicting) { + #- replacing $conflicting with $to_add + @{$bootloader->{entries}} = map { $_ == $conflicting ? $to_add : $_ } @{$bootloader->{entries}}; - foreach (@$entries) { - if ($_->{label} eq $v->{label}) { - compare_entries($_, $v) or return; #- avoid inserting it twice as another entry already exists ! - $_->{label} = "old${freeold}_$_->{label}"; + #- we will keep $conflicting, but not with same symlinks if used by the entry to add + expand_entry_symlinks($bootloader, $conflicting); + } else { + #- we have found an unused label + push @{$bootloader->{entries}}, $to_add; + } + + if (!$conflicting || same_entries($conflicting, $to_add)) { + log::l("current labels: " . join(" ", map { $_->{label} } @{$bootloader->{entries}})); + return $v; + } + $to_add = $conflicting; + + if ($to_add->{label} eq 'linux') { + $label = kernel_str2label(vmlinuz2kernel_str($to_add->{kernel_or_dev}), 'use_long_name'); + } else { + $label =~ s/^alt\d*_//; + $label = 'alt' . ($i++ ? $i : '') . "_$label"; } } - push @$entries, $v; + die 'add_entry'; } -sub add_kernel($$$$$) { - my ($prefix, $lilo, $kernelVersion, $specific, $v) = @_; - my $ext = $specific && "-$specific"; $specific =~ s/\d+\.\d+|hack//; - my ($vmlinuz, $image, $initrdImage) = ("vmlinuz-$kernelVersion$specific", "/boot/vmlinuz$ext", "/boot/initrd$ext.img"); - -e "$prefix/boot/$vmlinuz" or log::l("unable to find kernel image $prefix/boot/$vmlinuz"), return; - { - my $f = "initrd-$kernelVersion$specific.img"; - eval { mkinitrd($prefix, "$kernelVersion$specific", "/boot/$f") }; - undef $initrdImage if $@; - symlinkf $f, "$prefix$initrdImage" or $initrdImage = "/boot/$f" - if $initrdImage; +sub expand_entry_symlinks { + my ($bootloader, $entry) = @_; + + foreach my $kind ('kernel_or_dev', 'initrd') { + my $old_long_name = $bootloader->{old_long_names} && $bootloader->{old_long_names}{$entry->{$kind}} or next; + + #- replace all the {$kind} using this symlink to the real file + log::l("replacing $entry->{$kind} with $old_long_name for bootloader label $entry->{label}"); + $entry->{$kind} = $old_long_name; + } +} + +sub _do_the_symlink { + my ($bootloader, $link, $long_name) = @_; + + my $existing_link = readlink("$::prefix$link"); + if ($existing_link && $existing_link eq $long_name) { + #- nothing to do :) + return; + } + + if ($existing_link) { + #- the symlink is going to change! + #- replace all the {$kind} using this symlink to the real file + my $old_long_name = $existing_link =~ m!^/! ? $existing_link : "/boot/$existing_link"; + if (-e "$::prefix$old_long_name") { + $bootloader->{old_long_names}{$link} = $old_long_name; + } else { + log::l("ERROR: $link points to $old_long_name which does not exist"); + } + } elsif (-e "$::prefix$link") { + log::l("ERROR: $link is not a symbolic link"); + } + + #- changing the symlink + symlinkf($long_name, "$::prefix$link") + or cp_af("$::prefix/boot/$long_name", "$::prefix$link"); +} + +# for lilo & xen +sub get_mbootpack_filename { + my ($entry) = @_; + my $mbootpack_file = $entry->{initrd}; + $mbootpack_file =~ s/\binitrd/mbootpack/; + $entry->{xen} && $mbootpack_file; +} + +# for lilo & xen +sub build_mbootpack { + my ($entry) = @_; + + my $mbootpack = '/usr/bin/mbootpack'; + -f $::prefix . $entry->{kernel_or_dev} && -f $::prefix . $entry->{initrd} or return; + + my $mbootpack_file = get_mbootpack_filename($entry); + -f ($::prefix . $mbootpack_file) and return 1; + + my $error; + my $xen_kernel = '/tmp/xen_kernel'; + my $xen_vmlinux = '/tmp/xen_vmlinux'; + my $_b = before_leaving { unlink $::prefix . $_ foreach $xen_kernel, $xen_vmlinux }; + run_program::rooted($::prefix, '/bin/gzip', '>', $xen_kernel, '2>', \$error, '-dc', $entry->{xen}) + or die "unable to uncompress xen kernel"; + run_program::rooted($::prefix, '/bin/gzip', '>', $xen_vmlinux, '2>', \$error, '-dc', $entry->{kernel_or_dev}) + or die "unable to uncompress xen vmlinuz"; + + run_program::rooted($::prefix, $mbootpack, + "2>", \$error, + '-o', $mbootpack_file, + '-m', $xen_vmlinux, + '-m', $entry->{initrd}, + $xen_kernel) + or die "mbootpack failed: $error"; + + 1; +} + +sub add_kernel { + my ($bootloader, $kernel_str, $v, $b_nolink, $b_no_initrd) = @_; + + #- eg: for /boot/vmlinuz-2.6.17-13mdvxen0 (pkg kernel-xen0-xxx) + #- or /boot/vmlinuz-2.6.18-xen (pkg kernel-xen-uptodate) + if ($kernel_str->{version} =~ /xen/ && -f '/boot/xen.gz') { + $v->{xen} = '/boot/xen.gz'; } - symlinkf "$vmlinuz", "$prefix/$image" or $image = "/boot/$vmlinuz"; + add2hash($v, { type => 'image', - label => 'linux', - kernel_or_dev => $image, - initrd => $initrdImage, - append => $lilo->{perImageAppend}, + label => kernel_str2label($kernel_str), }); - add_entry($lilo->{entries}, $v); - $v; + + #- normalize append and handle special options + { + my ($simple, $dict) = unpack_append("$bootloader->{perImageAppend} $v->{append}"); + if ($v->{label} eq 'failsafe') { + #- perImageAppend contains resume=/dev/xxx which we don't want + @$dict = grep { $_->[0] ne 'resume' } @$dict; + } + $v->{append} = pack_append($simple, $dict); + } + + $b_nolink ||= $kernel_str->{use_long_name}; + + #- do not link /boot/vmlinuz to xen + $b_nolink ||= $v->{xen}; + + my $vmlinuz_long = kernel_str2vmlinuz_long($kernel_str); + my $initrd_long = kernel_str2initrd_long($kernel_str); + $v->{kernel_or_dev} = "/boot/$vmlinuz_long"; + -e "$::prefix$v->{kernel_or_dev}" or log::l("unable to find kernel image $::prefix$v->{kernel_or_dev}"), return; + log::l("adding $v->{kernel_or_dev}"); + + if (!$b_no_initrd) { + $v->{initrd} = mkinitrd($kernel_str->{version}, $bootloader, $v, "/boot/$initrd_long"); + } + + my $liblinux_long = kernel_str2liblinux_long($kernel_str); + if (-d "$::prefix$liblinux_long") { + $v->{liblinux} = $v->{fdtdir} = $liblinux_long; + } + + if (!$b_nolink) { + $v->{kernel_or_dev} = '/boot/' . kernel_str2vmlinuz_short($kernel_str); + _do_the_symlink($bootloader, $v->{kernel_or_dev}, $vmlinuz_long); + + if ($v->{initrd}) { + $v->{initrd} = '/boot/' . kernel_str2initrd_short($kernel_str); + _do_the_symlink($bootloader, $v->{initrd}, $initrd_long); + } + + if ($v->{liblinux}) { + $v->{liblinux} = kernel_str2liblinux_short(); + _do_the_symlink($bootloader, $v->{liblinux}, basename($liblinux_long)); + } + } + + add_entry($bootloader, $v); +} + +=item rebuild_initrds($bootloader) + +Rebuilds all initrds + +=cut + +sub rebuild_initrds { + my ($bootloader) = @_; + + my %done; + foreach my $v (grep { $_->{initrd} } @{$bootloader->{entries}}) { + my $kernel_str = vmlinuz2kernel_str($v->{kernel_or_dev}) or next; + my $initrd_long = '/boot/' . kernel_str2initrd_long($kernel_str); + next if $done{$initrd_long}++; + + rebuild_initrd($kernel_str->{version}, $bootloader, $v, $initrd_long); + } } -sub get_append { +# unused (?) +sub duplicate_kernel_entry { + my ($bootloader, $new_label) = @_; + + get_label($new_label, $bootloader) and return; + + my $entry = { %{ get_label('linux', $bootloader) }, label => $new_label }; + add_entry($bootloader, $entry); +} + +my $uniq_dict_appends = join('|', qw(acpi pci resume PROFILE XFree)); + +sub unpack_append { + my ($s) = @_; + my @l = "$s " =~ /((?:[^"\s]+|".*?")*)\s+/g; + [ grep { !/=/ } @l ], [ map { if_(/(.*?)=(.*)/, [$1, $2]) } @l ]; +} +sub pack_append { + my ($simple, $dict) = @_; + + #- normalize + $simple = [ reverse(uniq(reverse @$simple)) ]; + $dict = [ reverse(uniq_ { + my ($k, $v) = @$_; + $k =~ /^($uniq_dict_appends)$/ ? $k : "$k=$v"; + } reverse @$dict) ]; + + join(' ', @$simple, map { "$_->[0]=$_->[1]" } @$dict); +} + +sub modify_append { + my ($b, $f) = @_; + + my @l = grep { $_->{type} eq 'image' && !$_->{keep_verbatim} && !($::isStandalone && $_->{label} eq 'failsafe') } @{$b->{entries}}; + + foreach (\$b->{perImageAppend}, map { \$_->{append} } @l) { + my ($simple, $dict) = unpack_append($$_); + $f->($simple, $dict); + $$_ = pack_append($simple, $dict); + log::l("modify_append: $$_"); + } +} + +sub get_append_simple { + my ($b, $key) = @_; + my ($simple, $_dict) = unpack_append($b->{perImageAppend}); + member($key, @$simple); +} +sub get_append_with_key { + my ($b, $key) = @_; + my ($_simple, $dict) = unpack_append($b->{perImageAppend}); + my @l = map { $_->[1] } grep { $_->[0] eq $key } @$dict; + + log::l("more than one $key in $b->{perImageAppend}") if @l > 1; + $l[0]; +} +sub remove_append_simple { + my ($b, $key) = @_; + modify_append($b, sub { + my ($simple, $_dict) = @_; + @$simple = grep { $_ ne $key } @$simple; + }); +} +sub set_append_with_key { + my ($b, $key, $val) = @_; + + modify_append($b, sub { + my ($_simple, $dict) = @_; + + if ($val eq '') { + @$dict = grep { $_->[0] ne $key } @$dict; + } else { + push @$dict, [ $key, $val ]; + } + }); +} +sub set_append_simple { my ($b, $key) = @_; - ($b->{perImageAppend} =~ /\b$key=(\S*)/)[0]; + + modify_append($b, sub { + my ($simple, $_dict) = @_; + @$simple = uniq(@$simple, $key); + }); } -sub add_append { +sub may_append_with_key { my ($b, $key, $val) = @_; + set_append_with_key($b, $key, $val) if !get_append_with_key($b, $key); +} - foreach ({ append => $b->{perImageAppend} }, @{$b->{entries}}) { - $_->{append} =~ s/\b$key=\S*\s*//; - $_->{append} =~ s/\s*$/ $key=$val/ if $val; +sub get_append_netprofile { + my ($e) = @_; + my ($simple, $dict) = unpack_append($e->{append}); + my ($p, $dict_) = partition { $_->[0] eq 'PROFILE' } @$dict; + pack_append($simple, $dict_), $p->[0][1]; +} +sub set_append_netprofile { + my ($e, $append, $profile) = @_; + my ($simple, $dict) = unpack_append($append); + push @$dict, [ 'PROFILE', $profile ] if $profile; + $e->{append} = pack_append($simple, $dict); +} + +=item configure_entry($bootloader, $entry) + +Used when a bootloader $entry has been modified (eg: $entry->{vga}) + +=cut + +sub configure_entry { + my ($bootloader, $entry) = @_; + $entry->{type} eq 'image' or return; + + if (my $kernel_str = vmlinuz2kernel_str($entry->{kernel_or_dev})) { + $entry->{initrd} = + mkinitrd($kernel_str->{version}, $bootloader, $entry, + $entry->{initrd} || '/boot/' . kernel_str2initrd_short($kernel_str)); } } -sub configure_entry($$) { - my ($prefix, $entry) = @_; - if ($entry->{type} eq 'image') { - my $specific_version; - $entry->{kernel_or_dev} =~ /vmlinu.-(.*)/ and $specific_version = $1; - readlink("$prefix/$entry->{kernel_or_dev}") =~ /vmlinu.-(.*)/ and $specific_version = $1; +sub get_kernels_and_labels_before_kernel_remove { + my ($to_remove_kernel) = @_; + my @kernels = grep { $_ ne $to_remove_kernel } installed_vmlinuz(); + map { kernel_str2label($_) => $_ } get_kernel_labels(\@kernels); +} - if ($specific_version) { - $entry->{initrd} or $entry->{initrd} = "/boot/initrd-$specific_version.img"; - unless (-e "$prefix/$entry->{initrd}") { - eval { mkinitrd($prefix, $specific_version, "$entry->{initrd}") }; - undef $entry->{initrd} if $@; - } +sub get_kernels_and_labels() { + get_kernel_labels([ installed_vmlinuz() ]); +} + +sub get_kernel_labels { + my ($kernels) = @_; + + my @kernels_str = + sort { common::cmp_kernel_versions($b->{version_no_ext}, $a->{version_no_ext}) } + grep { -d "$::prefix/lib/modules/$_->{version}" } + map { vmlinuz2kernel_str($_) } @$kernels; + + my %labels; + foreach (@kernels_str) { + if ($labels{$_->{ext}}) { + $_->{use_long_name} = 1; + } else { + $labels{$_->{ext}} = 1; } } - $entry; + + $kernels_str[0]{ext} = ''; + + @kernels_str; } -sub dev2prompath { #- SPARC only - my ($dev) = @_; - my ($wd, $num) = $dev =~ /^(.*\D)(\d*)$/; - require c; - $dev = c::disk2PromPath($wd) and $dev = $dev =~ /^sd\(/ ? "$dev$num" : "$dev;$num"; - $dev; +sub short_ext { + my ($kernel_str) = @_; + + my $short_ext = { + 'xen0' => 'xen', + }->{$kernel_str->{ext}}; + + $short_ext || $kernel_str->{ext}; } -sub suggest { - my ($prefix, $lilo, $hds, $fstab, $kernelVersion, $vga_fb) = @_; - my $root_part = fsedit::get_root($fstab); - my $root = isLoopback($root_part) ? "loop7" : $root_part->{device}; - my $boot = fsedit::get_root($fstab, 'boot')->{device}; - my $partition = first($boot =~ /\D*(\d*)/); +sub _sanitize_ver { + my ($kernel_str) = @_; - require c; c::initSilo() if arch() =~ /sparc/; + my $name = $kernel_str->{basename}; + $name = '' if $name eq 'vmlinuz'; - my ($onmbr, $unsafe) = $lilo->{crushMbr} ? (1, 0) : suggest_onmbr($hds); - add2hash_($lilo, arch() =~ /sparc/ ? - { - default => "linux", - entries => [], - timeout => 5, - use_partition => 0, #- we should almost always have a whole disk partition. - root => "/dev/$root", - partition => $partition || 1, - boot => $root eq $boot && "/boot", #- this helps for getting default partition for silo. - } : arch =~ /ppc/ ? - { - defaultos => "linux", - default => "linux", - entries => [], - initmsg => "Welcome to Mandrake Linux!", - delay => 30, #- OpenFirmware delay - timeout => 50, - enableofboot => 1, - enablecdboot => 1, - useboot => $boot, - } : - { - bootUnsafe => $unsafe, - default => "linux", - entries => [], - timeout => $onmbr && 5, - if_(arch() !~ /ia64/, - lba32 => 1, - boot => "/dev/" . ($onmbr ? $hds->[0]{device} : fsedit::get_root($fstab, 'boot')->{device}), - map => "/boot/map", - install => "/boot/boot.b", - ), - }); + my $v = $kernel_str->{version_no_ext}; + + $v =~ s!(md[kv]|mnb)$!!; + $v =~ s!-0\.(pre|rc)(\d+)\.!$1$2-!; + + my $return = join(' ', grep { $_ } $name, short_ext($kernel_str), $v); + + $return; +} + +=item suggest_message_text($bootloader) + +Provides a description text for Lilo + +=cut + +sub suggest_message_text { + my ($bootloader) = @_; - if (!$lilo->{message} || $lilo->{message} eq "1") { - $lilo->{message} = join('', cat_("$prefix/boot/message")); - if (!$lilo->{message}) { - my $msg_en = + if (!$bootloader->{message} && !$bootloader->{message_text}) { + my $msg_en = #-PO: these messages will be displayed at boot time in the BIOS, use only ASCII (7bit) -__("Welcome to %s the operating system chooser! +N_("Welcome to the operating system chooser! -Choose an operating system in the list above or -wait %d seconds for default boot. +Choose an operating system from the list above or +wait for default boot. "); - my $msg = translate($msg_en); - #- use the english version if more than 20% of 8bits chars - $msg = $msg_en if int(grep { $_ & 0x80 } unpack "c*", $msg) / length($msg) > 0.2; - $lilo->{message} = sprintf $msg, arch() =~ /sparc/ ? "SILO" : "LILO", $lilo->{timeout}; + my $msg = translate($msg_en); + #- use the English version if more than 40% of 8bits chars + #- else, use the translation but force a conversion to ascii + #- to be sure there won't be undisplayable characters + if (int(grep { $_ & 0x80 } unpack "c*", $msg) / length($msg) > 0.4) { + $msg = $msg_en; + } else { + $msg = Locale::gettext::iconv($msg, "utf-8", "ascii//TRANSLIT"); } + $bootloader->{message_text} = $msg; } +} +sub suggest { + my ($bootloader, $all_hds, %options) = @_; + my $fstab = [ fs::get::fstab($all_hds) ]; + my $root_part = fs::get::root($fstab); + my $root = isLoopback($root_part) ? '/dev/loop7' : fs::wild_device::from_part('', $root_part); + my $boot = fs::get::root($fstab, 'boot')->{device}; + my $mbr; + + # If installing onto an USB drive, put the MBR there, else on the first non removable drive + if ($root_part->{is_removable}) { + $mbr = fs::get::part2hd($root_part, $all_hds); + } else { + $mbr = find { !$_->{is_removable} } allowed_boot_disks($all_hds); + } - add2hash_($lilo, { getVarsFromSh("$prefix/etc/sysconfig/system") }); #- for CLEAN_TMP - add2hash_($lilo, { memsize => $1 }) if cat_("/proc/cmdline") =~ /mem=(\S+)/; + my ($onmbr, $unsafe) = $bootloader->{crushMbr} ? (1, 0) : suggest_onmbr($mbr); + add2hash_($bootloader, + { + bootUnsafe => $unsafe, + entries => [], + timeout => $onmbr && 10, + nowarn => 1, + boot => "/dev/" . ($onmbr ? $mbr->{device} : $boot), + map => "/boot/map", + compact => 1, + 'large-memory' => 1, + color => 'black/cyan yellow/cyan', + 'menu-scheme' => 'wb:bw:wb:bw' + }); - #- give more priority to secure kernel because if the user want security, he will got it... - my $isSecure = -e "$prefix/boot/vmlinuz-${kernelVersion}secure"; - my $isEnterprise = -e "$prefix/boot/vmlinuz-${kernelVersion}enterprise"; + suggest_message_text($bootloader); - my $isSMP = detect_devices::hasSMP(); - if ($isSMP && !-e "$prefix/boot/vmlinuz-${kernelVersion}smp") { - log::l("SMP machine, but no SMP kernel found") unless $isSecure; - $isSMP = 0; + add2hash_($bootloader, { memsize => $1 }) if cat_("/proc/cmdline") =~ /\bmem=(\d+[KkMm]?)(?:\s.*)?$/; + if (my ($s, $port, $speed) = cat_("/proc/cmdline") =~ /console=(ttyS(\d),(\d+)\S*)/) { + log::l("serial console $s $port $speed"); + set_append_with_key($bootloader, console => $s); + any::set_login_serial_console($port, $speed); } - my $entry = add_kernel($prefix, $lilo, $kernelVersion, - $isSecure ? 'secure' : $isEnterprise ? 'enterprise' : $isSMP ? 'smp' : '', - { - label => 'linux', - root => "/dev/$root", - if_($vga_fb, vga => $vga_fb), #- using framebuffer - }); - add_kernel($prefix, $lilo, $kernelVersion, '', - { - label => $isSecure || $isEnterprise || $isSMP ? 'linux-up' : 'linux-nonfb', - root => "/dev/$root", - }) if $isSecure || $isEnterprise || $isSMP || $vga_fb; - my $failsafe = add_kernel($prefix, $lilo, $kernelVersion, '', + + my @kernels = get_kernels_and_labels() or die "no kernel installed"; + log::l("found kernels: ", join(', ', map { $_->{version} } @kernels)); + + my %old_kernels = map { vmlinuz2version($_->{kernel_or_dev}) => 1 } @{$bootloader->{entries}}; + @kernels = grep { !$old_kernels{$_->{version}} } @kernels; + + #- remove existing failsafe and linux-nonfb, do not care if the previous one was modified by the user? + #- but only if we are going to add new ones. + @{$bootloader->{entries}} = grep { !member($_->{label}, qw(failsafe linux-nonfb)) } @{$bootloader->{entries}} + if @kernels; + + foreach my $kernel (@kernels) { + my $e = add_kernel($bootloader, $kernel, { - label => 'failsafe', - root => "/dev/$root", + root => $root, + if_($options{vga_fb}, vga => $options{vga_fb}), #- using framebuffer + if_($options{vga_fb} && $options{splash}, append => "splash noiswmd audit=0"), + if_($options{quiet}, append => "splash quiet noiswmd audit=0"), }); - $entry->{append} .= " quiet" if $vga_fb && !$isSMP && !$isEnterprise; - $failsafe->{append} .= " failsafe" if $failsafe && !$lilo->{password}; - - #- manage prioritary default kernel (given as /boot/vmlinuz-default). - if (-e "$prefix/boot/vmlinuz-default") { - #- we use directly add_entry as no initrd should be done. - add_entry($lilo->{entries}, - { - type => 'image', - label => 'default', - root => "/dev/$root", - kernel_or_dev => '/boot/vmlinuz-default', - append => $lilo->{perImageAppend}, - }); - $lilo->{default} = 'default'; #- this one should be booted by default now. - } - - #- manage older kernel if installed. - foreach (qw(2.2 hack)) { - my $f = "$prefix/boot/vmlinuz-$_"; - if (-e $f) { - my $oldVersion = first(readlink($f) =~ /vmlinuz-(.*mdk)/); - my $oldSecure = -e "$prefix/boot/vmlinuz-${oldVersion}secure"; - my $oldSMP = -e "$prefix/boot/vmlinuz-${oldVersion}smp"; - - add_kernel($prefix, $lilo, $oldVersion, $_ . ($oldSecure ? 'secure' : $oldSMP ? 'smp' : ''), - { - label => "linux-$_", - root => "/dev/$root", - $vga_fb ? ( vga => $vga_fb) : (), #- using framebuffer - }); - add_kernel($prefix, $lilo, $oldVersion, $_, - { - label => $oldSecure || $oldSMP ? "linux-${_}up" : "linux-${_}nonfb", - root => "/dev/$root", - }) if $oldSecure || $oldSMP || $vga_fb; - my $entry = add_kernel($prefix, $lilo, $oldVersion, $_, - { - label => "failsafe-$_", - root => "/dev/$root", - }); - $entry->{append} .= " failsafe" if $entry && !$lilo->{password}; - } - } - - if (arch() =~ /sparc/) { - #- search for SunOS, it could be a really better approach to take into account - #- partition type for mounting point. - my $sunos = 0; - foreach (@$hds) { - foreach (@{$_->{primary}{normal}}) { - my $path = $_->{device} =~ m|^/| && $_->{device} !~ m|^/dev/| ? $_->{device} : dev2prompath($_->{device}); - add_entry($lilo->{entries}, - { - type => 'other', - kernel_or_dev => $path, - label => "sunos" . ($sunos++ ? $sunos : ''), - }) if $path && isSunOS($_) && type2name($_->{type}) =~ /root/i; - } + + if ($options{vga_fb} && $e->{label} eq 'linux') { + add_kernel($bootloader, $kernel, { root => $root, label => 'linux-nonfb' }); } - } elsif (arch() =~ /ppc/) { - #- if we identified a MacOS partition earlier - add it - if (defined $partition_table_mac'macos_part) { - add_entry($lilo->{entries}, + } + + add_kernel($bootloader, $kernels[0], + { root => $root, label => 'failsafe', append => 'failsafe noiswmd audit=0' }) + if @kernels; + + #- search for dos (or windows) boot partition. Do not look in extended partitions! + my @windows_boot_parts = + grep { $_->{active} + && isFat_or_NTFS($_) && member(fs::type::fs_type_from_magic($_), qw(vfat ntfs ntfs-3g)) + && !$_->{is_removable} + && !isRecovery($_); + } + map { @{$_->{primary}{normal}} } @{$all_hds->{hds}}; + each_index { + add_entry($bootloader, { - label => "macos", - kernel_or_dev => $partition_table_mac'macos_part + type => 'other', + kernel_or_dev => "/dev/$_->{device}", + label => 'windows' . ($::i || ''), + table => "/dev/$_->{rootDevice}", + makeactive => 1, }); - } - } elsif (arch() !~ /ia64/) { - #- search for dos (or windows) boot partition. Don't look in extended partitions! - my %nbs; - foreach (@$hds) { - foreach (@{$_->{primary}{normal}}) { - my $label = isNT($_) ? 'NT' : isDos($_) ? 'dos' : 'windows'; - add_entry($lilo->{entries}, - { - type => 'other', - kernel_or_dev => "/dev/$_->{device}", - label => $label . ($nbs{$label}++ ? $nbs{$label} : ''), - if_($_->{device} =~ /[1-4]$/, - table => "/dev/$_->{rootDevice}" - ), - unsafe => 1 - }) if isNT($_) || isFat($_) && isFat({ type => fsedit::typeOfPart($_->{device}) }); - } - } + } @windows_boot_parts; + + my @preferred = map { "linux-$_" } 'server'; + if (my $preferred = find { get_label($_, $bootloader) } @preferred) { + $bootloader->{default} ||= $preferred; } - my %l = ( - yaboot => to_bool(arch() =~ /ppc/), - silo => to_bool(arch() =~ /sparc/), - lilo => to_bool(arch() !~ /sparc|ppc/) && !isLoopback(fsedit::get_root($fstab)), - grub => to_bool(arch() !~ /sparc|ppc/ && !isRAID(fsedit::get_root($fstab))), - loadlin => to_bool(arch() !~ /sparc|ppc/) && -e "/initrd/loopfs/lnx4win", - ); - unless ($lilo->{methods}) { - $lilo->{methods} ||= { map { $_ => 1 } grep { $l{$_} } keys %l }; - if ($lilo->{methods}{lilo} && -e "$prefix/boot/lilo-graphic") { - $lilo->{methods}{lilo} = "lilo-graphic"; - exists $lilo->{methods}{grub} and $lilo->{methods}{grub} = undef; + $bootloader->{default} ||= "linux"; + $bootloader->{method} ||= first(method_choices($all_hds, 1), # best installed + method_choices($all_hds, 0)); # or best if no valid one is installed + + $bootloader->{perImageAppend} = $bootloader->{entries}[0]{append}; + $bootloader->{default_vga} = $options{vga_fb}; + + if (main_method($bootloader->{method}) eq 'grub') { + my %processed_entries = {}; + foreach my $c (find_other_distros_grub_conf($fstab)) { + my %h = ( + type => 'grub_configfile', + label => $c->{name}, + kernel_or_dev => "/dev/$c->{bootpart}{device}", + configfile => $c->{grub_conf}, + ); + if ($c->{root}) { + my $key = "$c->{name} - $c->{linux} - $c->{initrd}"; + next if $processed_entries{$key}; + $processed_entries{$key} = 1; + add_entry($bootloader, { + %h, + linux => $c->{linux}, + initrd => $c->{initrd}, + root => $c->{root}, + }); + } else { + add_entry($bootloader, \%h); + } } } } -sub suggest_floppy { - my ($bootloader) = @_; +sub detect_main_method { + my ($all_hds) = @_; + my $bootloader = &read($all_hds); + $bootloader && main_method($bootloader->{method}); +} - my $floppy = detect_devices::floppy() or return; - $floppy eq 'fd0' or log::l("suggest_floppy: not adding $floppy"), return; +sub main_method { + my ($method) = @_; + $method =~ /(\w+)/ && $1; +} - add_entry($bootloader->{entries}, - { - type => 'other', - kernel_or_dev => '/dev/fd0', - label => 'floppy', - unsafe => 1 - }); +sub config_files() { + my %files = ( + lilo => '/etc/lilo.conf', + grub => '/boot/grub/menu.lst', + grub_install => '/boot/grub/install.sh', + uboot => '/boot/extlinux/extlinux.conf', + ); + + map_each { + my $content = cat_("$::prefix/$::b"); + { main_method => main_method($::a), name => $::a, file => $::b, content => $content }; + } %files; } -sub keytable($$) { - my ($prefix, $f) = @_; - local $_ = $f; - if ($_ && !/\.klt$/) { - $f = "/boot/$_.klt"; - run_program::rooted($prefix, "keytab-lilo.pl", ">", $f, $_) or undef $f; - } - $f && -r "$prefix/$f" && $f; +sub method2text { + my ($method) = @_; + +{ + 'lilo-menu' => N("LILO with text menu"), + 'grub2-graphic' => N("GRUB2 with graphical menu"), + 'grub2' => N("GRUB2 with text menu"), + 'grub-graphic' => N("GRUB with graphical menu"), + 'grub-menu' => N("GRUB with text menu"), + 'refind' => N("rEFInd with graphical menu"), + 'uboot' => N("U-Boot/Extlinux with text menu"), + }->{$method}; } -sub has_profiles { to_bool(get_label("office", $b)) } -sub set_profiles { - my ($b, $want_profiles) = @_; - my $office = get_label("office", $b); - if ($want_profiles xor $office) { - my $e = get_label("linux", $b); - if ($want_profiles) { - push @{$b->{entries}}, { %$e, label => "office", append => "$e->{append} prof=Office" }; - $e->{append} .= " prof=Home"; - } else { - # remove profiles - $e->{append} =~ s/\s*prof=\w+//; - @{$b->{entries}} = grep { $_ != $office } @{$b->{entries}}; - } - } +=item method_choices_raw($b_prefix_mounted) +Returns list of bootloaders. + +method_choices_raw(1) will return the list of installed boot loaders. + +method_choices_raw(0) will return the list of all boot loaders supported by drakboot. + +Returns: ("grub2", "grub2-graphic") + +=cut + +sub method_choices_raw { + my ($b_prefix_mounted) = @_; + detect_devices::is_xbox() ? 'cromwell' : + arch() =~ /arm/ ? 'uboot' : + if_(!$b_prefix_mounted || whereis_binary('grub2-reboot', $::prefix), + 'grub2-graphic', 'grub2'), + if_(uefi_type() eq kernel_uefi_type() && (!$b_prefix_mounted || whereis_binary('refind-install', $::prefix)), 'refind'), + # only grub2 & rEFInd work on UEFI: + # lilo & grub-legacy do not suppport new ext4/xfs format and are unmainted so only allow them on upgrade: + if_(!is_uefi() && !($::isInstall && !$::o->{isUpgrade} || $::isLiveInstall), ( + if_(!$b_prefix_mounted || whereis_binary('grub', $::prefix) && -f "$::prefix/boot/grub/install.sh", + 'grub-graphic', 'grub-menu'), + if_(!$b_prefix_mounted || whereis_binary('lilo', $::prefix) && -f "$::prefix/etc/lilo.conf", + 'lilo-menu'), + )); } -sub get_of_dev($$) { - my ($prefix, $unix_dev) = @_; - #- don't care much for this - need to run ofpath rooted, and I need the result - #- In test mode, just run on "/", otherwise you can't get to the /proc files - if ($::testing) { - $prefix = ""; - } - run_program::rooted_or_die($prefix, "/usr/local/sbin/ofpath $unix_dev", ">", "/tmp/ofpath"); - open(FILE, "$prefix/tmp/ofpath") || die "Can't open $prefix/tmp/ofpath"; - my $of_dev = ""; - local $_ = ""; - while (<FILE>){ - $of_dev = $_; - } - chop($of_dev); - my @del_file = ($prefix . "/tmp/ofpath"); - unlink (@del_file); - log::l("OF Device: $of_dev"); - $of_dev; +=item method_choices($all_hds, $b_prefix_mounted) + +Returns list of supported bootloaders according to what is detected. + +Like method_choices_raw(), the $b_prefix_mounted parameter enables to return the list of either installed supported methods or the list of all supported boot loaders. + +=cut + +sub method_choices { + my ($all_hds, $b_prefix_mounted) = @_; + my $fstab = [ fs::get::fstab($all_hds) ]; + my $root_part = fs::get::root($fstab); + my $boot_part = fs::get::root($fstab, 'boot'); + my $have_dmraid = find { fs::type::is_dmraid($_) } @{$all_hds->{hds}}; + + grep { + !(/lilo/ && (isLoopback($root_part) || $have_dmraid)) # LILO doesn't work fake raid + && (/grub2/ || $boot_part->{fs_type} ne 'btrfs') # Only grub2 works on btrfs + && !(/grub2?-graphic/ && cat_("/proc/cmdline") =~ /console=ttyS/); # No Gfx mode on console + } method_choices_raw($b_prefix_mounted); } -sub install_yaboot($$$) { - my ($prefix, $lilo, $fstab, $hds) = @_; - $lilo->{prompt} = $lilo->{timeout}; +=item main_method_choices($b_prefix_mounted) - if ($lilo->{message}) { - local *F; - open F, ">$prefix/boot/message" and print F $lilo->{message} or $lilo->{message} = 0; - } - { - local *F; - local $\ = "\n"; - my $f = "$prefix/etc/yaboot.conf"; - open F, ">$f" or die "cannot create yaboot config file: $f"; - log::l("writing yaboot config to $f"); - - print F "#yaboot.conf - generated by DrakX"; - print F "init-message=\"\\n$lilo->{initmsg}\\n\"" if $lilo->{initmsg}; - - if ($lilo->{boot}) { - print F "boot=$lilo->{boot}"; - my $of_dev = get_of_dev($prefix, $lilo->{boot}); - print F "ofboot=$of_dev"; - } else { - die "no bootstrap partition defined." - } - - $lilo->{$_} and print F "$_=$lilo->{$_}" foreach qw(delay timeout); - print F "install=/usr/local/lib/yaboot/yaboot"; - print F "magicboot=/usr/local/lib/yaboot/ofboot"; - $lilo->{$_} and print F $_ foreach qw(enablecdboot enableofboot); - $lilo->{$_} and print F "$_=$lilo->{$_}" foreach qw(defaultos default); - print F "nonvram"; - my $boot = "/dev/" . $lilo->{useboot} if $lilo->{useboot}; - - foreach (@{$lilo->{entries}}) { +Returns list of supported bootloaders, not distinging text/gfx mode. - if ($_->{type} eq "image") { - my $of_dev = ''; - if ($boot !~ /$_->{root}/) { - $of_dev = get_of_dev($prefix, $boot); - print F "$_->{type}=$of_dev," . substr($_->{kernel_or_dev}, 5); - } else { - $of_dev = get_of_dev($prefix, $_->{root}); - print F "$_->{type}=$of_dev,$_->{kernel_or_dev}"; - } - print F "\tlabel=", substr($_->{label}, 0, 15); #- lilo doesn't handle more than 15 char long labels - print F "\troot=$_->{root}"; - if ($boot !~ /$_->{root}/) { - print F "\tinitrd=$of_dev," . substr($_->{initrd}, 5) if $_->{initrd}; - } else { - print F "\tinitrd=$of_dev,$_->{initrd}" if $_->{initrd}; - } - print F "\tappend=\"$_->{append}\"" if $_->{append}; - print F "\tread-write" if $_->{'read-write'}; - print F "\tread-only" if !$_->{'read-write'}; - } else { - my $of_dev = get_of_dev($prefix, $_->{kernel_or_dev}); - print F "$_->{label}=$of_dev"; - } - } - } - log::l("Installing boot loader..."); - my $f = "$prefix/tmp/of_boot_dev"; - my $of_dev = get_of_dev($prefix, $lilo->{boot}); - output($f, "$of_dev\n"); - $::testing and return; - if (defined $install_steps_interactive::new_bootstrap) { - run_program::run("hformat", "$lilo->{boot}") or die "hformat failed"; - } - run_program::rooted_or_die($prefix, "/sbin/ybin", "2>", "/tmp/.error"); - unlink "$prefix/tmp/.error"; -} - -sub install_silo($$$) { - my ($prefix, $silo, $fstab) = @_; - my $boot = fsedit::get_root($fstab, 'boot')->{device}; - my ($wd, $num) = $boot =~ /^(.*\D)(\d*)$/; - - #- setup boot promvars for. - require c; - if ($boot =~ /^md/) { - #- get all mbr devices according to /boot are listed, - #- then join all zero based partition translated to prom with ';'. - #- keep bootdev with the first of above. - log::l("/boot is present on raid partition which is not currently supported for promvars"); - } else { - if (!$silo->{use_partition}) { - foreach (@$fstab) { - if (!$_->{start} && $_->{device} =~ /$wd/) { - $boot = $_->{device}; - log::l("found a zero based partition in $wd as $boot"); - last; - } - } - } - $silo->{bootalias} = c::disk2PromPath($boot); - $silo->{bootdev} = $silo->{bootalias}; - log::l("preparing promvars for device=$boot"); +Like method_choices_raw(), the $b_prefix_mounted parameter enables to return the list of either installed supported methods or the list of all supported boot loaders. + +=cut + +sub main_method_choices { + my ($b_prefix_mounted) = @_; + uniq(map { main_method($_) } method_choices_raw($b_prefix_mounted)); +} +sub configured_main_methods() { + my @bad_main_methods = map { if_(!$_->{content}, $_->{main_method}) } config_files(); + difference2([ main_method_choices(1) ], \@bad_main_methods); +} + +# for lilo +sub keytable { + my ($f) = @_; + $f or return; + + if ($f !~ /\.klt$/) { + my $file = "/boot/$f.klt"; + run_program::rooted($::prefix, "keytab-lilo.pl", ">", $file, $f) or return; + $f = $file; } - c::hasAliases() or log::l("clearing promvars alias as non supported"), $silo->{bootalias} = ''; + -r "$::prefix/$f" && $f; +} + +sub check_enough_space() { + my $e = "$::prefix/boot/.enough_space"; + output $e, 1; -s $e or die N("not enough room in /boot"); + unlink $e; +} + +sub install_uboot { + my ($bootloader, $all_hds) = @_; + write_uboot($bootloader, $all_hds); + when_config_changed_uboot($bootloader); +} + +sub _write_extlinux_conf { + my ($bootloader, $_all_hds) = @_; - if ($silo->{message}) { - local *F; - open F, ">$prefix/boot/message" and print F $silo->{message} or $silo->{message} = 0; + if (!get_label($bootloader->{default}, $bootloader)) { + log::l("default bootloader entry $bootloader->{default} is invalid, choosing another one"); + $bootloader->{default} = $bootloader->{entries}[0]{label}; } - { - local *F; - local $\ = "\n"; - my $f = "$prefix/boot/silo.conf"; #- always write the silo.conf file in /boot ... - symlinkf "../boot/silo.conf", "$prefix/etc/silo.conf"; #- ... and make a symlink from /etc. - open F, ">$f" or die "cannot create silo config file: $f"; - log::l("writing silo config to $f"); - - $silo->{$_} and print F "$_=$silo->{$_}" foreach qw(partition root default append); - $silo->{$_} and print F $_ foreach qw(restricted); - print F "password=", $silo->{password} if $silo->{restricted} && $silo->{password}; #- also done by msec - print F "timeout=", round(10 * $silo->{timeout}) if $silo->{timeout}; - print F "message=$silo->{boot}/message" if $silo->{message}; - - foreach (@{$silo->{entries}}) {#my ($v, $e) = each %{$silo->{entries}}) { - my $type = "$_->{type}=$_->{kernel_or_dev}"; $type =~ s|/boot|$silo->{boot}|; - print F $type; - print F "\tlabel=$_->{label}"; - - if ($_->{type} eq "image") { - my $initrd = $_->{initrd}; $initrd =~ s|/boot|$silo->{boot}|; - print F "\tpartition=$_->{partition}" if $_->{partition}; - print F "\troot=$_->{root}" if $_->{root}; - print F "\tinitrd=$initrd" if $_->{initrd}; - print F "\tappend=\"$1\"" if $_->{append} =~ /^\s*"?(.*?)"?\s*$/; - print F "\tread-write" if $_->{'read-write'}; - print F "\tread-only" if !$_->{'read-write'}; - } - } + + my @conf; + push @conf, "# File generated by DrakX/drakboot"; + push @conf, "default " . $bootloader->{default} if $bootloader->{default}; + push @conf, "timeout " . $bootloader->{timeout} if $bootloader->{timeout}; + # needed to show boot menu and enable timeout + push @conf, "menu title Boot Menu" if $bootloader->{timeout}; + + foreach my $entry (@{$bootloader->{entries}}) { + push @conf, "\nlabel $entry->{label}"; + push @conf, " kernel $entry->{kernel_or_dev}"; + push @conf, " initrd $entry->{initrd}" if $entry->{initrd}; + push @conf, " fdtdir $entry->{fdtdir}" if $entry->{fdtdir}; + + my @append; + push @append, "root=" . $entry->{root} if $entry->{root}; + push @append, $entry->{append} if $entry->{append}; + push @conf, " append " . join(' ', @append) if @append; } - log::l("Installing boot loader..."); - $::testing and return; - run_program::rooted($prefix, "silo", "2>", "/tmp/.error", $silo->{use_partition} ? ("-t") : ()) or - run_program::rooted_or_die($prefix, "silo", "2>", "/tmp/.error", "-p", "2", $silo->{use_partition} ? ("-t") : ()); - unlink "$prefix/tmp/.error"; - #- try writing in the prom. - log::l("setting promvars alias=$silo->{bootalias} bootdev=$silo->{bootdev}"); - require c; - c::setPromVars($silo->{bootalias}, $silo->{bootdev}); + my $f = "$::prefix/boot/extlinux/extlinux.conf"; + log::l("writing extlinux config to $f"); + renamef($f, $f . '.old'); + output_with_perm($f, 0600, map { "$_\n" } @conf); +} + +sub write_uboot { + my ($bootloader, $all_hds) = @_; + _write_extlinux_conf($bootloader, $all_hds); +} + +sub when_config_changed_uboot { + my ($_bootloader) = @_; + #- do not do anything +} + +sub install_cromwell { + my ($_bootloader, $_all_hds) = @_; + log::l("XBox/Cromwell - nothing to install..."); +} +sub write_cromwell { + my ($_bootloader, $_all_hds) = @_; + log::l("XBox/Cromwell - nothing to write..."); +} +sub when_config_changed_cromwell { + my ($_bootloader) = @_; + #- do not do anything +} + +sub simplify_label { + my ($label) = @_; + + length($label) < 31 or $label =~ s/\.//g; + + $label = substr($label, 0, 31); #- lilo does not handle more than 31 char long labels + $label =~ s/ /_/g; #- lilo does not support blank character in image names, labels or aliases + $label; } -sub write_lilo_conf { - my ($prefix, $lilo, $fstab, $hds) = @_; - $lilo->{prompt} = $lilo->{timeout}; +sub make_label_lilo_compatible { + my ($label) = @_; + '"' . simplify_label($label) . '"'; +} - delete $lilo->{linear} if $lilo->{lba32}; +sub write_lilo { + my ($bootloader, $all_hds, $o_backup_extension) = @_; + $bootloader->{prompt} ||= $bootloader->{timeout}; my $file2fullname = sub { my ($file) = @_; if (arch() =~ /ia64/) { - (my $part, $file) = fsedit::file2part($prefix, $fstab, $file); + my $fstab = [ fs::get::fstab($all_hds) ]; + (my $part, $file) = fs::get::file2part($fstab, $file); my %hds = map_index { $_ => "hd$::i" } map { $_->{device} } - sort { isFat($b) <=> isFat($a) || $a->{device} cmp $b->{device} } fsedit::get_fstab(@$hds); - %hds->{$part->{device}} . ":" . $file; + sort { + my ($a_is_fat, $b_is_fat) = ($a->{fs_type} eq 'vfat', $b->{fs_type} eq 'vfat'); + $a_is_fat <=> $b_is_fat || $a->{device} cmp $b->{device}; + } @$fstab; + $hds{$part->{device}} . ":" . $file; } else { - $file + $file; } }; - if ($lilo->{message}) { - local *F; - -d "$prefix/boot/lilo-menu" and open F, ">$prefix/boot/lilo-menu/message" and print F $lilo->{message}; - -d "$prefix/boot/lilo-text" and open F, ">$prefix/boot/lilo-text/message" and print F $lilo->{message}; - -d "$prefix/boot/lilo-graphic" || -d "$prefix/boot/lilo-menu" || -d "$prefix/boot/lilo-text" or - open F, ">$prefix/boot/message" and print F $lilo->{message}; #- fallback in case of another lilo. + my $quotes = sub { + my ($s) = @_; + $s =~ s/"/\\"/g; + qq("$s"); + }; + + my $quotes_if_needed = sub { + my ($s) = @_; + $s =~ /["=\s]/ ? $quotes->($s) : $s; + }; + + + my @sorted_hds = sort_hds_according_to_bios($bootloader, $all_hds); + + if (is_empty_hash_ref($bootloader->{bios} ||= {}) && $all_hds->{hds}[0] != $sorted_hds[0]) { + log::l("Since we're booting on $sorted_hds[0]{device}, make it bios=0x80"); + $bootloader->{bios} = { "/dev/$sorted_hds[0]{device}" => '0x80' }; } - foreach ($lilo->{methods}{lilo}, "lilo-menu", "lilo-graphic", "lilo-text") { - if (-e "$prefix/boot/$_/boot.b" && -e "$prefix/boot/$_/message") { - symlinkf $_, "$prefix/boot/lilo"; - symlinkf "lilo/boot.b", "$prefix/boot/boot.b"; - symlinkf "lilo/message", "$prefix/boot/message"; - log::l("stage2 of lilo used is " . readlink "$prefix/boot/lilo"); - last; - } + + my @conf; + + #- normalize: RESTRICTED and MANDATORY are only valid if PASSWORD is set + if ($bootloader->{password}) { + # lilo defaults to mandatory, use restricted by default to have + # the same behaviour as with grub + $bootloader->{restricted} = 1; + } else { + delete $bootloader->{mandatory} if !$bootloader->{password}; + delete $bootloader->{restricted} if !$bootloader->{password}; + } + foreach my $entry (@{$bootloader->{entries}}) { + delete $entry->{mandatory} if !$entry->{password} && !$bootloader->{password}; + delete $entry->{restricted} if !$entry->{password} && !$bootloader->{password}; + } + if (get_append_with_key($bootloader, 'console') =~ /ttyS(.*)/) { + $bootloader->{serial} ||= $1; + } + + if (!get_label($bootloader->{default}, $bootloader)) { + log::l("default bootloader entry $bootloader->{default} is invalid, choosing another one"); + $bootloader->{default} = $bootloader->{entries}[0]{label}; } - if (arch() !~ /ia64/) { - -e "$prefix/boot/boot.b" && -e "$prefix/boot/message" or die "unable to get right lilo configuration in $prefix/boot"; + push @conf, "# File generated by DrakX/drakboot"; + push @conf, "# WARNING: do not forget to run lilo after modifying this file\n"; + push @conf, "default=" . make_label_lilo_compatible($bootloader->{default}) if $bootloader->{default}; + push @conf, map { $_ . '=' . $quotes_if_needed->($bootloader->{$_}) } grep { $bootloader->{$_} } qw(boot root map install serial vga keytable raid-extra-boot menu-scheme vmdefault); + push @conf, grep { $bootloader->{$_} } qw(linear geometric compact prompt mandatory nowarn restricted static-bios-codes large-memory); + push @conf, "append=" . $quotes->($bootloader->{append}) if $bootloader->{append}; + push @conf, "password=" . $bootloader->{password} if $bootloader->{password}; #- also done by msec + push @conf, "timeout=" . round(10 * $bootloader->{timeout}) if $bootloader->{timeout}; + + push @conf, "message=$bootloader->{message}" if $bootloader->{message}; + + push @conf, "ignore-table" if any { $_->{unsafe} && $_->{table} } @{$bootloader->{entries}}; + + push @conf, map_each { "disk=$::a bios=$::b" } %{$bootloader->{bios}}; + + foreach my $entry (@{$bootloader->{entries}}) { + my $mbootpack_file = get_mbootpack_filename($entry); + if ($mbootpack_file && !build_mbootpack($entry)) { + warn "mbootpack is required for xen but unavailable, skipping\n"; + next; + } + if ($entry->{type} eq 'grub_configfile') { + next; } - { - local *F; - local $\ = "\n"; - my $f = arch() =~ /ia64/ ? "$prefix/boot/efi/elilo.conf" : "$prefix/etc/lilo.conf"; - - open F, ">$f" or die "cannot create lilo config file: $f"; - log::l("writing lilo config to $f"); - - $lilo->{$_} and print F "$_=$lilo->{$_}" foreach qw(boot map install vga default append keytable); - $lilo->{$_} and print F $_ foreach qw(linear lba32 compact prompt restricted); - print F "password=", $lilo->{password} if $lilo->{restricted} && $lilo->{password}; #- also done by msec - print F "timeout=", round(10 * $lilo->{timeout}) if $lilo->{timeout}; - - my $dev = $hds->[0]{device}; - my %dev2bios = map_index { $_ => $::i } dev2bios($hds, $lilo->{boot}); - if ($dev2bios{$dev}) { - my %bios2dev = reverse %dev2bios; - print F "disk=/dev/$bios2dev{0} bios=0x80"; - printf F "disk=/dev/$dev bios=0x%x", 0x80 + $dev2bios{$dev}; - } elsif ($dev =~ /hd[bde]/) { - print F "disk=/dev/$dev bios=0x80"; - } - - print F "message=/boot/message" if (arch() !~ /ia64/); - print F "menu-scheme=wb:bw:wb:bw" if (arch() !~ /ia64/); - - foreach (@{$lilo->{entries}}) { - print F "$_->{type}=", $file2fullname->($_->{kernel_or_dev}); - my $label = substr($_->{label}, 0, 15); #- lilo doesn't handle more than 15 char long labels - $label =~ s/\s/_/g; #- lilo doesn't like spaces - print F "\tlabel=$label"; - - if ($_->{type} eq "image") { - print F "\troot=$_->{root}"; - print F "\tinitrd=", $file2fullname->($_->{initrd}) if $_->{initrd}; - print F "\tappend=\"$_->{append}\"" if $_->{append}; - print F "\tvga=$_->{vga}" if $_->{vga}; - print F "\tread-write" if $_->{'read-write'}; - print F "\tread-only" if !$_->{'read-write'}; - } else { - print F "\ttable=$_->{table}" if $_->{table}; - print F "\tunsafe" if $_->{unsafe} && !$_->{table}; + push @conf, "$entry->{type}=" . $file2fullname->($mbootpack_file || $entry->{kernel_or_dev}); + my @entry_conf; + push @entry_conf, "label=" . make_label_lilo_compatible($entry->{label}) if $entry->{label}; + + if ($entry->{type} eq "image") { + push @entry_conf, 'root=' . $quotes_if_needed->($entry->{root}) if $entry->{root} && !$entry->{xen}; + push @entry_conf, "initrd=" . $file2fullname->($entry->{initrd}) if $entry->{initrd} && !$mbootpack_file; + my $append = join(' ', if_($entry->{xen_append}, $entry->{xen_append}), + if_($entry->{xen}, '--', 'root=' . $entry->{root}), + if_($entry->{append}, $entry->{append})); + push @entry_conf, "append=" . $quotes->($append) if $append; + push @entry_conf, "vga=$entry->{vga}" if $entry->{vga}; + push @entry_conf, grep { $entry->{$_} } qw(read-write read-only optional); + push @entry_conf, "mandatory" if $entry->{lock}; + } else { + delete $entry->{unsafe} if $entry->{table}; #- we can't have both + push @entry_conf, map { "$_=$entry->{$_}" } grep { $entry->{$_} } qw(table boot-as); + push @entry_conf, grep { $entry->{$_} } qw(unsafe master-boot); - if (my ($dev) = $_->{table} =~ m|/dev/(.*)|) { - if ($dev2bios{$dev}) { - #- boot off the nth drive, so reverse the BIOS maps - my $nb = sprintf("0x%x", 0x80 + $dev2bios{$dev}); - $_->{mapdrive} ||= { '0x80' => $nb, $nb => '0x80' }; - } - } - while (my ($from, $to) = each %{$_->{mapdrive} || {}}) { - print F "\tmap-drive=$from"; - print F "\t to=$to"; + if ($entry->{table}) { + #- hum, things like table=c: are needed for some os2 cases, + #- in that case $hd below is undef + my $hd = fs::get::device2part($entry->{table}, $all_hds->{hds}); + if ($hd && $hd != $sorted_hds[0]) { + #- boot off the nth drive, so reverse the BIOS maps + my $nb = sprintf("0x%x", 0x80 + (find_index { $hd == $_ } @sorted_hds)); + $entry->{mapdrive} ||= { '0x80' => $nb, $nb => '0x80' }; } } + if ($entry->{mapdrive}) { + push @entry_conf, map_each { "map-drive=$::a", " to=$::b" } %{$entry->{mapdrive}}; + } } + push @entry_conf, "password=$entry->{password}" if $entry->{password}; + push @entry_conf, grep { $entry->{$_} } qw(mandatory vmwarn vmdisable); + + push @conf, map { "\t$_" } @entry_conf; } + my $f = arch() =~ /ia64/ ? "$::prefix/boot/efi/elilo.conf" : "$::prefix/etc/lilo.conf"; + + log::l("writing lilo config to $f"); + renamef($f, $f . ($o_backup_extension || '.old')); + output_with_perm($f, $bootloader->{password} ? 0600 : 0644, map { "$_\n" } @conf); } sub install_lilo { - my ($prefix, $lilo, $fstab, $hds) = @_; + my ($bootloader, $all_hds) = @_; - write_lilo_conf($prefix, $lilo, $fstab, $hds); + if (my ($install) = $bootloader->{method} =~ /lilo-(text|menu)/) { + $bootloader->{install} = $install; + } else { + delete $bootloader->{install}; + } + if ($bootloader->{message_text}) { + output("$::prefix/boot/message-text", $bootloader->{message_text}); + } + my $message = "message-text"; + if (-r "$::prefix/boot/$message") { + symlinkf $message, "$::prefix/boot/message"; + $bootloader->{message} = '/boot/message'; + } - log::l("Installing boot loader..."); - $::testing and return; - run_program::rooted_or_die($prefix, "lilo", "2>", "/tmp/.error") if (arch() !~ /ia64/); - unlink "$prefix/tmp/.error"; + #- ensure message does not contain the old graphic format + if ($bootloader->{message} && -s "$::prefix$bootloader->{message}" > 65_000) { + output("$::prefix$bootloader->{message}", ''); + } + + write_lilo($bootloader, $all_hds); + + when_config_changed_lilo($bootloader); + + configure_kdm_BootManager('Lilo'); } -sub dev2bios { - my ($hds, $where) = @_; - my @dev = map { $_->{device} } @$hds; - member($where, @dev) or ($where) = @dev; #- if not on mbr, +sub install_raw_lilo { + my ($o_force_answer) = @_; - s/h(d[e-g])/x$1/ foreach $where, @dev; #- emulates ultra66 as xd_ + my $error; + my $answer = $o_force_answer || ''; + run_program::rooted($::prefix, "echo $answer | lilo", '2>', \$error) or die "lilo failed: $error"; +} - my $start = substr($where, 0, 2); +sub when_config_changed_lilo { + my ($bootloader) = @_; + if (!$::testing && $bootloader->{method} =~ /lilo/) { + log::l("Installing boot loader on $bootloader->{boot}..."); + install_raw_lilo($bootloader->{force_lilo_answer}); + } +} + +#- NB: ide is lower than scsi, this is important for sort_hds_according_to_bios() +sub hd2bios_kind { + my ($hd) = @_; + lc(join('_', $hd->{bus}, $hd->{host})); +} + +sub ensafe_first_bios_drive { + my ($hds) = @_; + mixed_kind_of_disks($hds) || @$hds > 1 && _not_first_bios_drive($hds->[0]); +} +sub mixed_kind_of_disks { + my ($hds) = @_; + (uniq_ { hd2bios_kind($_) } @$hds) > 1; +} +sub _not_first_bios_drive { + my ($hd) = @_; + my $bios = $hd && $hd->{bios_from_edd}; + $bios && $bios ne '80'; +} + +sub sort_hds_according_to_bios { + my ($bootloader, $all_hds) = @_; + my $boot_hd = fs::get::device2part($bootloader->{first_hd_device} || $bootloader->{boot}, $all_hds->{hds}); #- $boot_hd is undefined when installing on floppy + my $boot_kind = $boot_hd && hd2bios_kind($boot_hd); my $translate = sub { - $_ eq $where ? "aaa" : #- if exact match, value it first - /^$start(.*)/ ? "ad$1" : #- if same class (ide/scsi/ultra66), value it before other classes - $_; + my ($hd) = @_; + my $kind = hd2bios_kind($hd); + $boot_hd ? ($hd == $boot_hd ? 0 : $kind eq $boot_kind ? 1 : 2) . "_$kind" : $kind; }; - @dev = map { $_->[0] } - sort { $a->[1] cmp $b->[1] } - map { [ $_, &$translate ] } @dev; + sort { $translate->($a) cmp $translate->($b) } @{$all_hds->{hds}}; +} - s/x(d.)/h$1/ foreach @dev; #- switch back; +sub device_string2grub { + my ($dev, $legacy_floppies, $sorted_hds) = @_; + if (my $device = fs::get::device2part($dev, [ @$sorted_hds, fs::get::hds_fstab(@$sorted_hds) ])) { + device2grub($device, $sorted_hds); + } elsif (my $floppy = fs::get::device2part($dev, $legacy_floppies)) { + my $bios = find_index { $floppy eq $_ } @$legacy_floppies; + "(fd$bios)"; + } else { + internal_error("unknown device $dev"); + } +} +sub device2grub { + my ($device, $sorted_hds) = @_; - @dev; + if (isRAID($device) && $device->{level} == 1) { + #- we can take any disk + $device = $device->{disks}[0]; + } + my ($hd, $part_nb) = + $device->{rootDevice} ? + (fs::get::device2part($device->{rootDevice}, $sorted_hds), $device->{device} =~ /(\d+)$/) : + $device; + my $bios = eval { find_index { $hd eq $_ } @$sorted_hds }; + if (defined $bios) { + my $part_string = defined $part_nb ? ',' . ($part_nb - 1) : ''; + "(hd$bios$part_string)"; + } else { + undef; + } } -sub dev2grub { - my ($dev, $dev2bios) = @_; - $dev =~ m|^(/dev/)?(...)(.*)$| or die "dev2grub (bad device $dev), caller is " . join(":", caller()); - my $grub = $dev2bios->{$2} or die "dev2grub ($2)"; - "($grub" . ($3 && "," . ($3 - 1)) . ")"; +sub read_grub_device_map() { + my %grub2dev = map { m!\((.*)\)\s+/dev/(.*)$! } cat_("$::prefix/boot/grub/device.map"); + \%grub2dev; +} +sub write_grub_device_map { + my ($legacy_floppies, $sorted_hds) = @_; + my $f = "$::prefix/boot/grub/device.map"; + renamef($f, "$f.old"); + output($f, + (map_index { "(fd$::i) /dev/$_->{device}\n" } @$legacy_floppies), + (map_index { "(hd$::i) /dev/$_->{device}\n" } @$sorted_hds)); } -sub write_grub_config { - my ($prefix, $lilo, $fstab, $hds) = @_; - my %dev2bios = ( - (map_index { $_ => "fd$::i" } detect_devices::floppies_dev()), - (map_index { $_ => "hd$::i" } dev2bios($hds, $lilo->{boot})), - ); +=item parse_grub_file($grub_file) - { - my %bios2dev = reverse %dev2bios; - output "$prefix/boot/grub/device.map", - join '', map { "($_) /dev/$bios2dev{$_}\n" } sort keys %bios2dev; - } - my $bootIsReiser = isThisFs("reiserfs", fsedit::get_root($fstab, 'boot')); - my $file2grub = sub { - my ($part, $file) = fsedit::file2part($prefix, $fstab, $_[0], 'keep_simple_symlinks'); - dev2grub($part->{device}, \%dev2bios) . $file; +Parses things like C<(hd0,4)/boot/vmlinuz> + +Returns: ("hd0", 4, "boot/vmlinuz") + +=cut + +sub parse_grub_file { + my ($grub_file) = @_; + my ($grub_dev, $rel_file) = $grub_file =~ m!\((.*?)\)/?(.*)! or return; + my ($hd, $part) = split(',', $grub_dev); + ($hd, $part, $rel_file); +} + +=item grub2dev_and_file($grub_file, $grub2dev, $o_block_device) + +Takes things like C<(hd0,4)/boot/vmlinuz> + +Returns: ("/dev/sda5", "boot/vmlinuz") + +=cut + +sub grub2dev_and_file { + my ($grub_file, $grub2dev, $o_block_device) = @_; + my ($hd, $part, $rel_file) = parse_grub_file($grub_file) or return; + $grub2dev->{$hd} or internal_error("$hd has no mapping in device.map (when translating $grub_file)"); + $part = $o_block_device ? '' : defined $part && $part + 1; #- grub wants "(hdX,Y)" where lilo just want "hdY+1" + my $device = '/dev/' . ($part eq '' ? $grub2dev->{$hd} : devices::prefix_for_dev($grub2dev->{$hd}) . $part); + $device, $rel_file; +} + +=item grub2devd($grub_file, $grub2dev, $o_block_device) + +Takes things like C<(hd0,4)/boot/vmlinuz> + +Returns: "/dev/sda5" + +=cut + +sub grub2dev { + my ($grub_file, $grub2dev, $o_block_device) = @_; + first(grub2dev_and_file($grub_file, $grub2dev, $o_block_device)); +} + +=item grub2file($grub_file, $grub2dev, $fstab, $o_entry) + +Replaces + +=over 4 + +=item * C</vmlinuz> with C</boot/vmlinuz> when "root" or "rootnoverify" is set for the entry + +=item * C<(hdX,Y)> in C<(hdX,Y)/boot/vmlinuz...> by appropriate path if possible/needed + +=back + +=cut + +sub grub2file { + my ($grub_file, $grub2dev, $fstab, $o_entry) = @_; + + if ($grub_file =~ m!^/!) { + my $root = $o_entry && ($o_entry->{rootnoverify} || $o_entry->{grub_root}); + $root and $grub_file = "$root$grub_file"; + } + + if (my ($device, $rel_file) = grub2dev_and_file($grub_file, $grub2dev)) { + my $part = fs::get::device2part($device, $fstab); + if (my $mntpoint = $part && $part->{mntpoint}) { + ($mntpoint eq '/' ? '' : $mntpoint) . '/' . $rel_file; + } else { + log::l("ERROR: unknown device $device (computed from $grub_file)"); + $grub_file; + } + } else { + $grub_file; + } +} + +sub boot_copies_dir() { '/boot/copied' } +sub create_copy_in_boot { + my ($file) = @_; + + my $s = $file; + $s =~ s!/!_!g; + my $file2 = boot_copies_dir() . "/$s"; + + log::l("$file is not available at boot time, creating a copy ($file2)"); + mkdir_p(boot_copies_dir()); + output("$file2.link", $file . "\n"); + update_copy_in_boot("$file2.link"); + + $file2; +} +sub update_copy_in_boot { + my ($link) = @_; + my $orig = chomp_(cat_("$::prefix$link")); + (my $dest = $link) =~ s/\.link$// or internal_error("update_copy_in_boot: $link"); + if (-e "$::prefix$orig") { + log::l("updating $dest from $orig"); + cp_af("$::prefix$orig", "$::prefix$dest"); + } else { + log::l("removing $dest since $orig does not exist anymore"); + unlink "$::prefix$link", "$::prefix$orig"; + } +} + +sub crypt_grub_password { + my ($password) = @_; + require IPC::Open2; + local $ENV{LC_ALL} = 'C'; + my ($his_out, $his_in); + my $cmd = ($::prefix ? "chroot $::prefix " : "") . "/sbin/grub-md5-crypt"; + + my $pid = IPC::Open2::open2($his_out, $his_in, $cmd); + + my ($line, $res); + while (sysread($his_out, $line, 100)) { + if ($line =~ /Password/i) { + syswrite($his_in, "$password\n"); + } else { + $res = $line; + } + } + waitpid($pid, 0); + my $status = $? >> 8; + die "failed to encrypt password (status=$status)" if $status != 0; + chomp_($res); +} + +sub get_grub2_first_entry { + my ($bootloader) = @_; + # set default parameters: + my ($entry) = grep { $_->{kernel_or_dev} =~ /vmlin/ } @{$bootloader->{entries}}; + $entry; +} + +sub get_grub2_append { + my ($bootloader) = @_; + # get default parameters from first entry: + my ($entry) = get_grub2_first_entry($bootloader); + my $append = $entry->{append}; + if (my $vga = $entry->{vga} || $bootloader->{vga}) { + $append .= " vga=$vga"; + } + $append =~ s/root=\S+//g; + $append =~ s/\bro\b//g; + $append =~ s/\s+/ /g; + $append; +} + +sub crypt_grub2_password { + my ($password) = @_; + require IPC::Open2; + local $ENV{LC_ALL} = 'C'; + my ($his_out, $his_in); + my $pid = IPC::Open2::open2($his_out, $his_in, "$::prefix/bin/grub2-mkpasswd-pbkdf2"); + + my ($line, $res); + while (sysread($his_out, $line, 100)) { + if ($line =~ /enter.*password:/i) { + syswrite($his_in, "$password\n"); + } else { + chomp($line); + $res .= $line if $line; + } + } + $res =~ s/^PBKDF2 hash of your password is //; + waitpid($pid, 0); + my $status = $? >> 8; + die "failed to encrypt password (status=$status)" if $status != 0; + chomp_($res); +} + +sub write_grub2_sysconfig { + my ($bootloader, $_all_hds, $o_backup_extension) = @_; + + # Set password prior to run update-grub2: + my $pw_f = get_grub2_users(); + if ($bootloader->{password}) { + if (!is_grub2_already_crypted($bootloader->{password})) { + $bootloader->{password} = crypt_grub2_password($bootloader->{password}); + } + output_with_perm($pw_f, 0600, "GRUB2_PASSWORD=$bootloader->{password}"); + } else { + unlink($pw_f); + } + + my $f = "$::prefix/etc/default/grub"; + my %conf = getVarsFromSh($f); + + my $append = $bootloader->{perImageAppend} || get_grub2_append($bootloader); + my $vga = $bootloader->{default_vga}; + $append .= " vga=$vga" if $append !~ /vga=/ && $vga && $vga ne "normal"; + + $conf{GRUB_CMDLINE_LINUX_DEFAULT} = $append; + $conf{GRUB_GFXPAYLOAD_LINUX} = 'auto' if is_uefi(); + $conf{GRUB_DISABLE_RECOVERY} = 'false'; # for 'failsafe' entry + $conf{GRUB_DEFAULT} //= 'saved'; # for default entry but do not overwrite user choice + $conf{GRUB_SAVEDEFAULT} //= 'true'; # for default entry but do not overwrite user choice + # special case so that setVarsInSh() doesn't emit the line when timeout is 0 + $conf{GRUB_TIMEOUT} = $bootloader->{timeout} eq 0 ? "0 " : $bootloader->{timeout}; + renamef($f, $f . ($o_backup_extension || '.old')); + setVarsInSh($f, \%conf); +} + +sub write_grub2_default_entry { + my ($bootloader, $_all_hds, $o_backup_extension) = @_; + + my $default = $bootloader->{default}; + # menu entry must be identified by its full path. eg: "submenu1>submenu2>title": + if (my $def = find { $_->{label} eq $bootloader->{default} } @{$bootloader->{entries}}) { + $default = $def->{real_label} if $def->{real_label}; + } + + # set default entry: + eval { + my $f2 = "$::prefix/boot/grub2/grubenv"; + cp_af($f2, $f2 . ($o_backup_extension || '.old')); + my $error; + run_program::rooted($::prefix, 'grub2-set-default', '2>', \$error, $default) or die "grub2-set-default failed: $error"; + }; + if (my $err = $@) { + log::l("error while running grub2-set-default: $err"); + } +} + +sub write_grub2 { + my ($bootloader, $o_all_hds, $o_backup_extension) = @_; + my $error; + + write_grub2_sysconfig($bootloader, $o_all_hds, $o_backup_extension); + + my $f1 = "$::prefix/boot/grub2/grub.cfg"; + #- we won't just rename as grub2-mkconfig (more likely os-prober) may fail: + cp_af($f1, $f1 . '.old') if -e $f1; + #- don't use the update-grub2 script here, it hangs when run by service_harddrake + #- during boot because it causes systemd to try to start a user session + run_program::rooted($::prefix, 'grub2-mkconfig', '2>', \$error, '-o', '/boot/grub2/grub.cfg') or die "grub2-mkconfig failed: $error"; + log::l("grub2-mkconfig logs: $error"); + + write_grub2_default_entry($bootloader, $o_all_hds, $o_backup_extension); + check_enough_space(); +} + +sub get_grub2_users() { + "$::prefix/boot/grub2/user.cfg"; +} + +sub get_grub2_install_sh() { + "$::prefix/boot/grub2/install.sh"; +} + +sub write_grub2_install_sh { + my ($bootloader, $o_backup_extension) = @_; + my $f = get_grub2_install_sh(); + my $boot = $bootloader->{boot}; + my @options; + if (is_uefi()) { + if ($bootloader->{no_esp_or_mbr}) { + push @options, qw(--bootloader-id=tmp --no-nvram); + } elsif ($bootloader->{removable}) { + push @options, '--removable'; + } + } else { + @options = $bootloader->{no_esp_or_mbr} ? ('--grub-setup=/bin/true', $boot) : $boot; + } + renamef($f, $f . ($o_backup_extension || '.old')); + output_with_perm($f, 0755, join(' ', 'grub2-install', @options)); +} + +sub write_grub { + my ($bootloader, $all_hds, $o_backup_extension) = @_; + + my $fstab = [ fs::get::fstab($all_hds) ]; + my @legacy_floppies = detect_devices::floppies(); + my @sorted_hds = sort_hds_according_to_bios($bootloader, $all_hds); + write_grub_device_map(\@legacy_floppies, \@sorted_hds); + + my $file2grub; $file2grub = sub { + my ($file) = @_; + if ($file =~ m!^\(.*\)/!) { + $file; #- it's already in grub format + } else { + my ($part, $rel_file) = fs::get::file2part($fstab, $file, 'keep_simple_symlinks'); + if (my $grub = device2grub($part, \@sorted_hds)) { + $grub . $rel_file; + } elsif (!begins_with($file, '/boot/')) { + log::l("$file is on device $part->{device} which is not available at boot time. Copying it"); + $file2grub->(create_copy_in_boot($file)); + } else { + log::l("ERROR: $file is on device $part->{device} which is not available at boot time. Defaulting to a dumb value"); + "(hd0,0)$file"; + } + } }; + + if (get_append_with_key($bootloader, 'console') =~ /ttyS(\d),(\d+)/) { + $bootloader->{serial} ||= "--unit=$1 --speed=$2"; + $bootloader->{terminal} ||= "--timeout=" . ($bootloader->{timeout} || 0) . " console serial"; + } elsif ($bootloader->{method} eq 'grub-graphic') { + my $bin = '/usr/sbin/grub-gfxmenu'; + if ($bootloader->{gfxmenu} eq '' && -x "$::prefix$bin") { + my $locale = $::o->{locale} || do { require lang; lang::read() }; + run_program::rooted($::prefix, $bin, '--lang', $locale->{lang}, '--update-gfxmenu'); + $bootloader->{gfxmenu} ||= '/boot/gfxmenu'; + } + #- not handled anymore + delete $bootloader->{$_} foreach qw(splashimage viewport shade); + } else { + delete $bootloader->{gfxmenu}; + } + + my $format = sub { map { "$_ $bootloader->{$_}" } @_ }; + { - local *F; - local $\ = "\n"; - my $f = "$prefix/boot/grub/menu.lst"; - open F, ">$f" or die "cannot create grub config file: $f"; - log::l("writing grub config to $f"); + my @conf; - $lilo->{$_} and print F "$_ $lilo->{$_}" foreach qw(timeout); - - print F "color black/cyan yellow/cyan"; - print F "i18n ", $file2grub->("/boot/grub/messages"); - print F "keytable ", $file2grub->($lilo->{keytable}) if $lilo->{keytable}; - #- since we use notail in reiserfs, altconfigfile is broken :-( - unless ($bootIsReiser) { - print F "altconfigfile ", $file2grub->(my $once = "/boot/grub/menu.once"); - output "$prefix$once", " " x 100; - } - - map_index { - print F "default $::i" if $_->{label} eq $lilo->{default}; - } @{$lilo->{entries}}; - - foreach (@{$lilo->{entries}}) { - print F "\ntitle $_->{label}"; - - if ($_->{type} eq "image") { - my $vga = $_->{vga} || $lilo->{vga}; - printf F "kernel %s root=%s %s%s%s\n", - $file2grub->($_->{kernel_or_dev}), - $_->{root} =~ /loop7/ ? "707" : $_->{root}, #- special to workaround bug in kernel (see #ifdef CONFIG_BLK_DEV_LOOP) - $_->{append}, - $_->{'read-write'} && " rw", - $vga && $vga ne "normal" && " vga=$vga"; - print F "initrd ", $file2grub->($_->{initrd}) if $_->{initrd}; + if ($bootloader->{password}) { + if (!is_already_crypted($bootloader->{password})) { + my $encrypted = crypt_grub_password($bootloader->{password}); + $bootloader->{password} = "--md5 $encrypted"; + } + } + + push @conf, $format->(grep { defined $bootloader->{$_} } qw(timeout)); + push @conf, $format->(grep { $bootloader->{$_} } qw(color password serial shade terminal viewport background foreground)); + + push @conf, map { $_ . ' ' . $file2grub->($bootloader->{$_}) } grep { $bootloader->{$_} } qw(gfxmenu); + + eval { + push @conf, "default " . (find_index { $_->{label} eq $bootloader->{default} } @{$bootloader->{entries}}); + }; + + foreach my $entry (@{$bootloader->{entries}}) { + my $title = "\ntitle $entry->{label}"; + + if ($entry->{keep_verbatim}) { + push @conf, '', $entry->{verbatim}; + } elsif ($entry->{type} eq "image") { + push @conf, $title; + push @conf, grep { $entry->{$_} } 'lock'; + push @conf, join(' ', 'kernel', $file2grub->($entry->{xen}), $entry->{xen_append}) if $entry->{xen}; + + my $vga = $entry->{vga} || $bootloader->{vga}; + push @conf, join(' ', $entry->{xen} ? 'module' : 'kernel', + $file2grub->($entry->{kernel_or_dev}), + $entry->{xen} ? () : 'BOOT_IMAGE=' . simplify_label($entry->{label}), + if_($entry->{root}, $entry->{root} =~ /loop7/ ? "root=707" : "root=$entry->{root}"), #- special to workaround bug in kernel (see #ifdef CONFIG_BLK_DEV_LOOP) + $entry->{append}, + if_($entry->{'read-write'}, 'rw'), + if_($vga && $vga ne "normal", "vga=$vga")); + push @conf, "module " . $_ foreach @{$entry->{modules} || []}; + if ($entry->{initrd}) { + # split partition from initrd path and place + # it to a separate 'root' entry. + # Grub2's mkconfig takes initrd entry 'as is', + # but grub2 fails to load smth like '(hd0,1)/boot/initrd' taken from grub-legacy + my $initrd_path = $file2grub->($entry->{initrd}); + if ($initrd_path =~ /^(\([^\)]+\))/) { + push @conf, "root $1"; + $initrd_path =~ s/^(\([^\)]+\))//; + } + push @conf, join(' ', $entry->{xen} ? 'module' : 'initrd', $initrd_path); + } } else { - print F "root ", dev2grub($_->{kernel_or_dev}, \%dev2bios); - if ($_->{kernel_or_dev} !~ /fd/) { - #- boot off the second drive, so reverse the BIOS maps - $_->{mapdrive} ||= { '0x80' => '0x81', '0x81' => '0x80' } - if $_->{table} && $lilo->{boot} !~ /$_->{table}/; - - map_each { print F "map ($::b) ($::a)" } %{$_->{mapdrive} || {}}; - - print F "makeactive"; + my $dev = eval { device_string2grub($entry->{kernel_or_dev}, \@legacy_floppies, \@sorted_hds) }; + if (!$dev) { + log::l("dropping bad entry $entry->{label} for unknown device $entry->{kernel_or_dev}"); + next; + } + push @conf, $title; + push @conf, grep { $entry->{$_} } 'lock'; + if ($entry->{type} ne 'grub_configfile' || $entry->{configfile} !~ /grub\.cfg/ || !$entry->{root}) { + push @conf, join(' ', $entry->{rootnoverify} ? 'rootnoverify' : 'root', $dev); + } + + if ($entry->{table}) { + if (my $hd = fs::get::device2part($entry->{table}, \@sorted_hds)) { + if (my $bios = find_index { $hd eq $_ } @sorted_hds) { + #- boot off the nth drive, so reverse the BIOS maps + my $nb = sprintf("0x%x", 0x80 + $bios); + $entry->{mapdrive} ||= { '0x80' => $nb, $nb => '0x80' }; + } + } + } + if ($entry->{mapdrive}) { + push @conf, map_each { "map ($::b) ($::a)" } %{$entry->{mapdrive}}; + } + push @conf, "makeactive" if $entry->{makeactive}; + # grub.cfg is grub2 config, can't use it as configfile for grub-legacy + if ($entry->{type} eq 'grub_configfile' && $entry->{configfile} !~ /grub\.cfg/) { + push @conf, "configfile $entry->{configfile}"; + } elsif ($entry->{linux}) { + push @conf, "root $entry->{root}", "kernel $entry->{linux}"; + push @conf, "initrd $entry->{initrd}" if $entry->{initrd}; + } else { + push @conf, "chainloader +1"; } - print F "chainloader +1"; } } + my $f = "$::prefix/boot/grub/menu.lst"; + log::l("writing grub config to $f"); + renamef($f, $f . ($o_backup_extension || '.old')); + output_with_perm($f, 0600, map { "$_\n" } @conf); } - my $hd = fsedit::get_root($fstab, 'boot')->{rootDevice}; - - my $dev = dev2grub($lilo->{boot}, \%dev2bios); - my ($s1, $s2, $m) = map { $file2grub->("/boot/grub/$_") } qw(stage1 stage2 menu.lst); - my $f = "/boot/grub/install.sh"; - output "$prefix$f", + { + my $f = "$::prefix/boot/grub/install.sh"; + my $boot_dev = device_string2grub($bootloader->{boot}, \@legacy_floppies, \@sorted_hds); + my $files_dev = device2grub(fs::get::root_($fstab, 'boot'), \@sorted_hds); + renamef($f, $f . ($o_backup_extension || '.old')); + output_with_perm($f, 0755, "grub --device-map=/boot/grub/device.map --batch <<EOF -install $s1 d $dev $s2 p $m +root $files_dev +setup --stage2=/boot/grub/stage2 $boot_dev quit EOF -"; +"); + } - output "$prefix/boot/grub/messages", map { substr(translate($_) . "\n", 0, 78) } ( #- ensure the translated messages are not too big the hard way -#-PO: these messages will be displayed at boot time in the BIOS, use only ASCII (7bit) -#-PO: and keep them smaller than 79 chars long -__("Welcome to GRUB the operating system chooser!"), -#-PO: these messages will be displayed at boot time in the BIOS, use only ASCII (7bit) -#-PO: and keep them smaller than 79 chars long -__("Use the %c and %c keys for selecting which entry is highlighted."), -#-PO: these messages will be displayed at boot time in the BIOS, use only ASCII (7bit) -#-PO: and keep them smaller than 79 chars long -__("Press enter to boot the selected OS, \'e\' to edit the"), -#-PO: these messages will be displayed at boot time in the BIOS, use only ASCII (7bit) -#-PO: and keep them smaller than 79 chars long -__("commands before booting, or \'c\' for a command-line."), -#-PO: these messages will be displayed at boot time in the BIOS, use only ASCII (7bit) -#-PO: and keep them smaller than 79 chars long -__("The highlighted entry will be booted automatically in %d seconds."), -); - - my $e = "$prefix/boot/.enough_space"; - output $e, 1; -s $e or die _("not enough room in /boot"); - unlink $e; - $f; + check_enough_space(); +} + +sub configure_kdm_BootManager { + my ($name) = @_; + eval { common::update_gnomekderc_no_create("$::prefix/etc/kde/kdm/kdmrc", 'Shutdown' => ( + BootManager => $name + )) }; +} + +sub sync_partition_data_to_disk { + my ($part) = @_; + + common::sync(); + + if ($part->{fs_type} eq 'xfs') { + run_program::rooted($::prefix, 'xfs_freeze', '-f', $part->{mntpoint}); + run_program::rooted($::prefix, 'xfs_freeze', '-u', $part->{mntpoint}); + } +} + +sub _dev_to_MBR_backup { + my ($dev) = @_; + $dev =~ s!/dev/!!; + $dev =~ s!/!_!g; + "$::prefix/boot/boot.backup.$dev"; +} + +sub save_previous_MBR_bootloader { + my ($dev) = @_; + my $t; + open(my $F, $dev); + CORE::read($F, $t, 0x1b8); #- up to disk magic + output(_dev_to_MBR_backup($dev), $t); +} + +sub restore_previous_MBR_bootloader { + my ($dev) = @_; + log::l("restoring previous bootloader on $dev"); + output($dev, scalar cat_(_dev_to_MBR_backup($dev))); +} + +sub install_grub2 { + my ($bootloader, $all_hds) = @_; + write_grub2($bootloader, $all_hds); + write_grub2_install_sh($bootloader, '.old'); + install_raw_grub2(); +} + +sub install_raw_grub2() { + my $error; + my $f = '/boot/grub2/install.sh'; + my ($right_dir, $bad_dir) = ("$::prefix/boot/EFI/EFI/", "$::prefix/boot/EFI/efi/"); + if (-e $bad_dir && ! -e $right_dir) { + renamef($bad_dir, $right_dir); + } + if (!run_program::rooted($::prefix, "sh", "2>", \$error, $f)) { + log::explanations("grub2-install failed:\n(" . cat_($f) . ")\nError: <$error>"); + die "grub2-install failed: $error"; + } } sub install_grub { - my ($prefix, $lilo, $fstab, $hds) = @_; + my ($bootloader, $all_hds) = @_; + + write_grub($bootloader, $all_hds); - my $f = write_grub_config($prefix, $lilo, $fstab, $hds); + if (!$::testing) { + if ($bootloader->{previous_boot} && $bootloader->{previous_boot} eq $bootloader->{boot}) { + # nothing to do (already installed in {boot}) + } else { + if ($bootloader->{previous_boot}) { + restore_previous_MBR_bootloader(delete $bootloader->{previous_boot}); + } + if (fs::get::device2part($bootloader->{boot}, [ fs::get::hds($all_hds) ])) { + save_previous_MBR_bootloader($bootloader->{boot}); + $bootloader->{previous_boot} = $bootloader->{boot}; + } + } + + my @files = grep { /(stage1|stage2|_stage1_5)$/ } glob("$::prefix/lib/grub/*/*"); + cp_af(@files, "$::prefix/boot/grub"); + sync_partition_data_to_disk(fs::get::root([ fs::get::fstab($all_hds) ], 'boot')); + install_raw_grub(); + } + configure_kdm_BootManager('Grub'); +} +sub install_raw_grub() { log::l("Installing boot loader..."); - $::testing and return; - symlink "$prefix/boot", "/boot"; - run_program::run_or_die("sh", $f); - unlink "$prefix/tmp/.error.grub", "/boot"; + my $error; + run_program::rooted($::prefix, "sh", "2>", \$error, '/boot/grub/install.sh') or die "grub failed: $error"; +} + +sub when_config_changed_grub2 { + my ($_bootloader) = @_; + #- do not do anything } -sub lnx4win_file { - my $lilo = shift; - map { local $_ = $_; s,/,\\,g; "$lilo->{boot_drive}:\\lnx4win$_" } @_; +sub when_config_changed_grub { + my ($_bootloader) = @_; + #- do not do anything + + update_copy_in_boot($_) foreach glob($::prefix . boot_copies_dir() . '/*.link'); } -sub loadlin_cmd { - my ($prefix, $lilo) = @_; - my $e = get_label("linux", $lilo) || first(grep { $_->{type} eq "image" } @{$lilo->{entries}}); +sub write_refind { + my ($bootloader, $_all_hds, $o_backup_extension) = @_; - cp_af("$prefix$e->{kernel_or_dev}", "$prefix/boot/vmlinuz") unless -e "$prefix/boot/vmlinuz"; - cp_af("$prefix$e->{initrd}", "$prefix/boot/initrd.img") unless -e "$prefix/boot/initrd.img"; + my @config; - $e->{label}, sprintf"%s %s initrd=%s root=%s $e->{append}", - lnx4win_file($lilo, "/loadlin.exe", "/boot/vmlinuz", "/boot/initrd.img"), - $e->{root} =~ /loop7/ ? "0707" : $e->{root}; #- special to workaround bug in kernel (see #ifdef CONFIG_BLK_DEV_LOOP) + foreach my $entry (@{$bootloader->{entries}}) { + if ($entry->{type} eq 'image' && $entry->{kernel_or_dev} eq '/boot/vmlinuz') { + my $vga = $entry->{vga} || $bootloader->{vga}; + my $boot_params = join(' ', + "root=$entry->{root}", + $entry->{append}, + if_($entry->{'read-write'}, 'rw'), + if_($vga && $vga ne "normal", "vga=$vga") + ); + push @config, '"' . $entry->{label} . '" "' . $boot_params . '"'; + } + } + if (@config) { + my $f = "$::prefix/boot/refind_linux.conf"; + log::l("writing rEFInd config to $f"); + renamef($f, $f . ($o_backup_extension || '.old')); + output_with_perm($f, 0600, map { "$_\n" } @config); + check_enough_space(); + } else { + log::l("config has no entries - rEFInd config file not written"); + } + + my $default_kernel = readlink("$::prefix/boot/vmlinuz"); + if ($default_kernel) { + if ($bootloader->{use_nvram}) { + write_refind_previous_boot_var($default_kernel); + } else { + write_refind_previous_boot_file($default_kernel, "$::prefix/boot/EFI/EFI/refind"); + write_refind_previous_boot_file($default_kernel, "$::prefix/boot/EFI/EFI/BOOT"); + } + } } -sub install_loadlin { - my ($prefix, $lilo, $fstab) = @_; +sub write_refind_previous_boot_var { + my ($kernel) = @_; + my ($efivars, $already_mounted) = mount_efivars(); + my $previous_boot = "$efivars/PreviousBoot-36d08fa7-cf0b-42f5-8f14-68df73ed3740"; + run_program::run('chattr', '-i', $previous_boot) if -e $previous_boot; + if (open(my $efivar, '>:raw', $previous_boot)) { + require Encode; + log::l("writing rEFInd PreviousBoot variable"); + print $efivar "\x07\x00\x00\x00"; + print $efivar Encode::encode('UTF16-LE', $kernel); + print $efivar "\x00\x00"; + close($efivar); + } else { + log::l("failed to write rEFInd PreviousBoot variable: $@ ($!)"); + } + run_program::run('umount', $efivars) if !$already_mounted; +} - my $boot; - ($boot) = grep { $lilo->{boot} eq "/dev/$_->{device}" } @$fstab; - ($boot) = grep { loopback::carryRootLoopback($_) } @$fstab unless $boot && $boot->{device_windobe}; - ($boot) = grep { isFat($_) } @$fstab unless $boot && $boot->{device_windobe}; - log::l("loadlin device is $boot->{device} (windobe $boot->{device_windobe})"); - $lilo->{boot_drive} = $boot->{device_windobe}; +sub write_refind_previous_boot_file { + my ($kernel, $base_path) = @_; + return if ! -e "$base_path/refind.conf" || ! mkdir_p("$base_path/vars"); + if (open(my $f, '>:raw', "$base_path/vars/PreviousBoot")) { + require Encode; + log::l("writing rEFInd $base_path/vars/PreviousBoot file"); + print $f Encode::encode('UTF16-LE', "Boot boot\\$kernel"); + print $f "\x00\x00"; + close($f); + } else { + log::l("failed to write rEFInd $base_path/vars/PreviousBoot file"); + } +} - my ($winpart) = grep { $_->{device_windobe} eq 'C' } @$fstab; - log::l("winpart is $winpart->{device}"); - my $winhandle = any::inspect($winpart, $prefix, 'rw'); - my $windrive = $winhandle->{dir}; - log::l("windrive is $windrive"); +sub install_refind { + my ($bootloader, $all_hds) = @_; + + if ($bootloader->{install_mode} ne 'no_install') { + my (@options, $error); + if ($bootloader->{install_mode} eq 'as_default') { + $bootloader->{esp_device} or die "ESP device is unknown"; + push @options, '--usedefault'; + push @options, $bootloader->{esp_device}; + #- refind_install uses lower case file names. If a default bootloader was previously + #- created with an upper case file name, Linux won't overwrite it. + my $default_fn = "$::prefix/boot/EFI/EFI/BOOT/BOOT" . uc(uefi_type()) . ".EFI"; + unlink($default_fn) if -e $default_fn; + } elsif ($bootloader->{install_mode} eq 'nvram_only') { + push @options, '--nvramonly'; + } + run_program::rooted($::prefix, '/sbin/refind-install', '2>', \$error, @options) + or die "refind-install failed: $error"; + } - my ($label, $cmd) = loadlin_cmd($prefix, $lilo); + #- This file is not used by rEFInd itself. It just defines the paths to the image + #- files used for the standard banner choices. + my %h = getVarsFromSh("$::prefix/etc/sysconfig/refind"); - #install_loadlin_config_sys($lilo, $windrive, $label, $cmd); - #install_loadlin_desktop($lilo, $windrive); + my $banner_source; + if ($bootloader->{banner_path} eq 'refind_banner.png') { + $banner_source = "$::prefix" . $h{REFIND_BANNER}; + } elsif ($bootloader->{banner_path} eq 'mageia_theme.png') { + $banner_source = "$::prefix" . $h{MAGEIA_THEME}; + } + if (defined $banner_source && ! (-f $banner_source && -r $banner_source)) { + log::l("$banner_source does not exist or is not readable"); + $bootloader->{banner_path} = ''; + $bootloader->{banner_scale} = 'noscale'; + undef $banner_source; + } - output "/initrd/loopfs/lnx4win/linux.bat", unix2dos( -'@echo off -echo Mandrake Linux -smartdrv /C -' . "$cmd\n"); + #- Try both possible locations for the main config file. + modify_refind_config($bootloader, $banner_source, "$::prefix/boot/EFI/EFI/refind"); + modify_refind_config($bootloader, $banner_source, "$::prefix/boot/EFI/EFI/BOOT"); + write_refind($bootloader, $all_hds); } -sub install_loadlin_config_sys { - my ($lilo, $windrive, $label, $cmd) = @_; +sub modify_refind_config { + my ($bootloader, $banner_source, $esp_dir) = @_; + + my $config_file = "$esp_dir/refind.conf"; + return if ! -f $config_file || ! -w $config_file; + + my $use_nvram = $bootloader->{use_nvram} ? 'true' : 'false'; + + my $banner_path = $bootloader->{banner_path}; + cp_f($banner_source, "$esp_dir/$banner_path") if (defined $banner_source); + + my $banner_scale = $bootloader->{banner_scale}; + + my @config; + + my %done; + foreach (cat_utf8($config_file)) { + if ($_ =~ /^#?use_nvram\s/) { + if (! $done{use_nvram}) { + push @config, "use_nvram $use_nvram\n" ; + $done{use_nvram} = 1; + } + } elsif ($_ =~ /^#?banner\s/) { + if (! $done{banner_path}) { + if ($banner_path eq '') { + push @config, "#banner my_banner.png\n"; + } else { + push @config, "banner $banner_path\n"; + } + $done{banner_path} = 1; + } + } elsif ($_ =~ /^#?banner_scale\s/) { + if (! $done{banner_scale}) { + push @config, "banner_scale $banner_scale\n"; + $done{banner_scale} = 1; + } + } else { + push @config, $_; + } + } - my $config_sys = "$windrive/config.sys"; - local $_ = cat_($config_sys); - output "$windrive/config.mdk", $_ if $_; - - my $timeout = $lilo->{timeout} || 1; + if (@config) { + log::l("writing rEFInd config to $config_file"); + renamef($config_file, $config_file . '.old'); + output_with_perm($config_file, 0600, @config); + } else { + log::l("config has no entries - rEFInd config file not written"); + } +} + +sub when_config_changed_refind { + my ($_bootloader) = @_; + #- do not do anything +} + +=item action($bootloader, $action, @para) + +Calls the C<$action> function with @para parameters: + + $actions->($bootloader, @para) + +If needed, the function name will be resolved to call a boot loader specific function (eg: for LILO/GRUB/...) + +This Swiss army knife function is heavily used by eg /sbin/bootloader-config + +=cut + +sub action { + my ($bootloader, $action, @para) = @_; - $_ = " -[Menu] -menuitem=Windows -menudefault=Windows,$timeout + my $main_method = main_method($bootloader->{method}); + my $f = $bootloader::{$action . '_' . $main_method} or die "unknown bootloader method $bootloader->{method} ($action)"; + $f->($bootloader, @para); +} + +=item install($bootloader, $all_hds) + +Writes back the boot loader config. Calls the proper write_XYZ() function. + +=cut + +sub install { + my ($bootloader, $all_hds) = @_; -[Windows] -" . $_ if !/^\Q[Menu]/m; + my ($efivars, $already_mounted) = mount_efivars() if is_uefi(); - #- remove existing entry - s/^menuitem=$label\s*//mi; - s/\n\[$label\].*?(\n\[|$)/$1/si; + $bootloader->{keytable} = keytable($bootloader->{keytable}) if $bootloader->{method} eq 'lilo'; + action($bootloader, 'install', $all_hds); - #- add entry - s/(.*\nmenuitem=[^\n]*)/$1\nmenuitem=$label/s; + run_program::run('umount', $efivars) if is_uefi() && !$already_mounted; +} - $_ .= " -[$label] -shell=$cmd -"; - output $config_sys, unix2dos($_); +sub get_grub2_pkg() { + my ($prefix, $pkg); + if (is_uefi()) { + my %convert = (ia32 => 'i386', aa64 => 'arm64', x64 => 'x86_64'); + my %pkgs = (ia32 => 'x86-32', aa64 => 'aarch-64', x64 => 'x86-64'); + $prefix = $convert{uefi_type()} . "-efi"; + $pkg = "grub2-efi(" . $pkgs{uefi_type()} . ")"; + } else { + $prefix = 'i386-pc'; + $pkg = 'grub2'; + } + ($prefix, $pkg); } -sub install_loadlin_desktop { - my ($lilo, $windrive) = @_; - my $windir = lc(cat_("$windrive/msdos.sys") =~ /^WinDir=.:\\(\S+)/m ? $1 : "windows"); +sub ensure_pkg_is_installed { + my ($do_pkgs, $bootloader) = @_; -#-PO: "Desktop" and "Start Menu" are the name of the directories found in c:\windows -#-PO: so you may need to put them in English or in a different language if MS-windows doesn't exist in your language - foreach (__("Desktop"), -#-PO: "Desktop" and "Start Menu" are the name of the directories found in c:\windows - __("Start Menu")) { - my $d = "$windrive/$windir/" . translate($_); - -d $d or $d = "$windrive/$windir/$_"; - -d $d or log::l("can't find windows $d directory"), next; - output "$d/Linux4Win.url", unix2dos(sprintf -q([InternetShortcut] -URL=file:\lnx4win\lnx4win.exe -WorkingDirectory=%s -IconFile=%s -IconIndex=0 -), lnx4win_file($lilo, "/", "/lnx4win.ico")); + my %suppl = ( + # method => [ 'pkg_name', 'file_to_test' ], + 'grub-graphic' => [ qw(mageia-gfxboot-theme /usr/share/gfxboot/themes/Mageia/boot/message) ], + 'grub2-graphic' => [ qw(grub2-mageia-theme /boot/grub2/themes/maggy/theme.txt) ], + ); + my $main_method = main_method($bootloader->{method}); + if ($main_method eq 'grub2') { + my ($prefix, $pkg) = get_grub2_pkg(); + $do_pkgs->ensure_is_installed($pkg, "/usr/lib/grub/$prefix/ext2.mod", 1) or return 0; + } elsif ($main_method eq 'refind') { + $do_pkgs->ensure_is_installed('refind', '/sbin/refind-install', 1) or return 0; + } elsif (member($main_method, qw(grub grub2 lilo))) { + $do_pkgs->ensure_binary_is_installed($main_method, $main_method, 1) or return 0; + } + # Install gfx theme if needed: + if (my $pkg = $suppl{$bootloader->{method}}) { + $do_pkgs->ensure_is_installed(@$pkg, 1) or return 0; } + 1; } +sub parse_grub2_config { + my ($l, $grubcfg, $part) = @_; -sub install { - my ($prefix, $lilo, $fstab, $hds) = @_; + my ($linux, $menuentry, $root, $root_dev, $initrd); - if (my ($p) = grep { $lilo->{boot} =~ /\Q$_->{device}/ } @$fstab) { - die _("You can't install the bootloader on a %s partition\n", partition_table::type2fs($p)) - if isThisFs('xfs', $p); + foreach (cat_($grubcfg)) { + chomp; + if (/^menuentry\s+['"]([^']+)["']/) { + if ($menuentry && $root) { + my $parttype = partition_table::raw::typeOfMBR($root_dev); + if ((!$parttype || $parttype eq "empty") && $linux) { + push @$l, { menuentry => $menuentry, bootpart => $part, root => $root, linux => $linux, initrd => $initrd, grub_conf => $grubcfg }; + } + } + $menuentry = $1; + $root = $linux = undef; + } elsif (/set root='(\([^\)]+\))'/) { + $root = $1; + + if ($root =~ /\(([^,]+),msdos(\d+)\)/) { + my $dev_title = "/" . $1; + my $part_num = $2; + my $dec_part_num = $part_num-1; + $dev_title =~ s!hd!dev/sd!; + $dev_title =~ tr/0123456789/abcdefghi/; + + $root_dev = $part_num ? $dev_title . $part_num : $dev_title; + $root =~ s/msdos$part_num/$dec_part_num/; + } + } elsif (/^\s+linux\s+(.+)/) { + $linux = $1; + } elsif (/^\s+initrd\s+(.+)/) { + $initrd = $1; + } } - { - my $f = "$prefix/etc/sysconfig/system"; - setVarsInSh($f, add2hash_({ CLEAN_TMP => $lilo->{CLEAN_TMP} }, { getVarsFromSh($f) })); +} + +=item find_other_distros_grub_conf($fstab) + +Returns a list of other distros' grub.conf + +=cut + +sub find_other_distros_grub_conf { + my ($fstab) = @_; + + my @unknown_true_fs = + grep { isTrueLocalFS($_) && + (!$_->{mntpoint} || !member($_->{mntpoint}, '/home', fs::type::directories_needed_to_boot())); + } @$fstab; + + log::l("looking for configured grub on partitions " . join(' ', map { $_->{device} } @unknown_true_fs)); + + my @l; + foreach my $part (@unknown_true_fs) { + my $handle = any::inspect($part, $::prefix) or next; + + foreach my $bootdir ('', '/boot') { + my $f = find { -e "$handle->{dir}$bootdir/$_" } 'grub.conf', 'grub/menu.lst' or next; + push @l, { bootpart => $part, bootdir => $bootdir, grub_conf => "$bootdir/$f" }; + } + foreach my $bootdir ('', '/boot', '/boot/grub', '/boot/grub2') { + my $f = find { -e "$handle->{dir}$bootdir/$_" } 'grub.cfg' or next; + my $parttype = partition_table::raw::typeOfMBR($part->{device}); + if (!$parttype || $parttype eq "empty") { + parse_grub2_config(\@l, "$handle->{dir}/$bootdir/$f", $part); + } else { + push @l, { bootpart => $part, bootdir => $bootdir, grub_conf => "$bootdir/$f" }; + } + } + if (my $f = common::release_file($handle->{dir})) { + my $h = common::parse_release_file($handle->{dir}, $f, $part); + $h->{name} = $h->{release}; + push @l, $h; + } elsif ($handle && -e "$handle->{dir}/etc/issue") { + my ($s, $dropped) = cat_("$handle->{dir}/etc/issue") =~ /^([^\\\n]*)(.*)/; + log::l("found /etc/issue: $s (removed: $dropped)"); + push @l, { name => $s, part => $part }; + } } - $lilo->{keytable} = keytable($prefix, $lilo->{keytable}); + my $root; + my $set_root = sub { + my ($v) = @_; + $root and log::l("don't know what to do with $root->{name} ($root->{part}{device})"); + $root = $v; + }; + my @found; + while (my $e = shift @l) { + if ($e->{name}) { + $set_root->($e); + } else { + if (@l && $l[0]{name}) { + $set_root->(shift @l); + } - if (exists $lilo->{methods}{grub}) { - #- when lilo is selected, we don't try to install grub. - #- just create the config file in case it may be useful - eval { write_grub_config($prefix, $lilo, $fstab, $hds) }; + my $ok; + if ($root && $root->{part} == $e->{bootpart} && $e->{bootdir}) { + # easy case: /boot is not a separate partition + $ok = 1; + } elsif ($root && $root->{part} != $e->{bootpart} && !$e->{bootdir}) { + log::l("associating '/' $root->{part}{device} with '/boot' $e->{bootpart}{device}"); + $ok = 1; + } + if ($ok) { + add2hash($e, $root); + undef $root; + } elsif ($root) { + log::l("weird case for grub conf in $e->{bootpart}{device}, keeping '/' from $root->{part}{device}"); + } else { + log::l("could not recognise the distribution for $e->{grub_conf} in $e->{bootpart}{device}"); + } + $e->{name} ||= "Linux $e->{bootpart}{device}"; + push @found, $e; + } } + $set_root->(undef); - my %l = grep_each { $::b } %{$lilo->{methods}}; - my @rcs = map { - c::is_secure_file('/tmp/.error') or die "can't ensure a safe /tmp/.error"; - my $f = $bootloader::{"install_$_"} or die "unknown bootloader method $_"; - eval { $f->(@_) }; - $@; - } reverse sort keys %l; #- reverse sort for having grub installed after lilo if both are there. - - return if grep { !$_ } @rcs; #- at least one worked? - die first(map { $_ } @rcs); + @found; } -#-###################################################################################### -#- Wonderful perl :( -#-###################################################################################### -1; # +sub update_for_renumbered_partitions { + my ($in, $renumbering, $all_hds) = @_; + + my @configs = grep { $_->{content} } config_files(); + $_->{new} = $_->{orig} = $_->{content} foreach @configs; + + my @sorted_hds; { + my $grub2dev = read_grub_device_map(); + map_each { + $sorted_hds[$1] = fs::get::device2part($::b, $all_hds->{hds}) if $::a =~ /hd(\d+)/; + } %$grub2dev; + } + + #- NB: we make the changes with an added string inside so that hda5 is only renamed once to hda6 + + foreach (@$renumbering) { + my ($old, $new) = @$_; + log::l("renaming $old -> $new"); + (my $lnew = $new) =~ s/(\d+)$/__DRAKX_DONE__$1/; + $_->{new} =~ s/\b$old/$lnew/g foreach @configs; + + any { $_->{name} eq 'grub' } @configs or next; + + my ($old_grub, $new_grub) = map { device_string2grub($_, [], \@sorted_hds) } $old, $new; + log::l("renaming $old_grub -> $new_grub"); + (my $lnew_grub = $new_grub) =~ s/\)$/__DRAKX_DONE__)/; + $_->{new} =~ s/\Q$old_grub/$lnew_grub/g foreach @configs; + } + + $_->{new} =~ s/__DRAKX_DONE__//g foreach @configs; + + my @changed_configs = grep { $_->{orig} ne $_->{new} } @configs or return 1; # no need to update + + $in->ask_okcancel('', N("Your bootloader configuration must be updated because partition has been renumbered")) or return; + + foreach (@changed_configs) { + renamef("$::prefix/$_->{file}", "$::prefix/$_->{file}.old"); + output("$::prefix/$_->{file}", $_->{new}); + } + + my $main_method = detect_main_method($all_hds); + my @needed = map { + $_ eq 'grub' ? 'grub_install' : $_; + } $main_method ? $main_method : ('lilo', 'grub'); + + if (intersection(\@needed, [ map { $_->{name} } @changed_configs ])) { + $in->ask_warn('', N("The bootloader cannot be installed correctly. You have to boot rescue and choose \"%s\"", + N("Re-install Boot Loader"))); + } + 1; +} + +=back + +=cut + +1; |