361 lines
10 KiB
Perl
Executable File
361 lines
10 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
our $VERSION = '0.8.1';
|
|
|
|
use Getopt::Long qw(:config no_ignore_case auto_version);
|
|
use Pod::Usage qw(pod2usage);
|
|
use File::Basename;
|
|
use File::Temp qw(tempfile tempdir);
|
|
use File::Copy;
|
|
use File::Path qw(make_path remove_tree);
|
|
use Sort::Versions;
|
|
|
|
use Data::Dumper;
|
|
$Data::Dumper::Indent = 1;
|
|
$Data::Dumper::Sortkeys = 1;
|
|
$Data::Dumper::Purity = 1;
|
|
|
|
use Config::IniFiles;
|
|
use Sort::Versions;
|
|
|
|
sub latestKernel;
|
|
sub createInitramfs;
|
|
sub unifiedEFI;
|
|
sub execute;
|
|
sub safeCopy;
|
|
sub cleanupMount;
|
|
|
|
BEGIN {
|
|
$SIG{INT} = \&cleanupMount;
|
|
$SIG{TERM} = \&cleanupMount;
|
|
}
|
|
|
|
my ( %runConf, %config, %components );
|
|
|
|
my $configfile = "/etc/zfsbootmenu/config.ini";
|
|
|
|
$runConf{bootdir} = "/boot";
|
|
$runConf{confd} = "/usr/share/zfsbootmenu/dracut.conf.d";
|
|
$runConf{version} = $VERSION;
|
|
|
|
GetOptions(
|
|
"version|v=s" => \$runConf{version},
|
|
"pkgname|p=s" => \$runConf{pkgname},
|
|
"action|a=s" => \$runConf{action},
|
|
"update|u=s" => \$runConf{update},
|
|
"kernel|k=s" => \$runConf{kernel},
|
|
"bootdir|b=s" => \$runConf{bootdir},
|
|
"confd|C=s" => \$runConf{confd},
|
|
"config|c=s" => \$configfile,
|
|
);
|
|
|
|
# Sanity check, ensure we have a configuration file
|
|
unless ( -f $configfile ) {
|
|
print "$configfile missing, exiting\n";
|
|
exit;
|
|
}
|
|
|
|
# Versions ending in .0 will be stripped by petitboots' syslinux parser
|
|
$runConf{version} = $runConf{version} . "_1";
|
|
|
|
# Read our config into a hash
|
|
tie %config, 'Config::IniFiles', ( -file => $configfile );
|
|
|
|
unless ( ( defined $config{Global}{ManageImages} ) and ( $config{Global}{ManageImages} ) ) {
|
|
print "ManageImages not enabled, no action taken\n";
|
|
exit;
|
|
}
|
|
|
|
# Override the location of our specific dracut.conf.d directory
|
|
if ( defined $config{Global}{DracutConfDir} ) {
|
|
$runConf{confd} = $config{Global}{DracutConfDir};
|
|
}
|
|
|
|
# Ensure our bootloader partition is mounted
|
|
$runConf{umount_on_exit} = 0;
|
|
if ( ( defined $config{Global}{BootMountPoint} ) and ( length $config{Global}{BootMountPoint} ) ) {
|
|
my $mounted = 0;
|
|
|
|
my $cmd = "mount";
|
|
my @output = execute($cmd);
|
|
my $status = pop(@output);
|
|
foreach my $line (@output) {
|
|
chomp($line);
|
|
if ( $line =~ m/$config{Global}{BootMountPoint}/ ) {
|
|
$mounted = 1;
|
|
last;
|
|
}
|
|
}
|
|
|
|
unless ($mounted) {
|
|
print "Mounting $config{Global}{BootMountPoint}\n";
|
|
$cmd = "mount $config{Global}{BootMountPoint}";
|
|
execute($cmd);
|
|
$runConf{umount_on_exit} = 1;
|
|
}
|
|
}
|
|
|
|
# Create a temp directory
|
|
# It is automatically purged on program exit
|
|
my $dir = File::Temp->newdir();
|
|
my $tempdir = $dir->dirname;
|
|
|
|
# Set our kernel from the CLI, or pick the latest available in /boot
|
|
if ( ( defined $runConf{kernel} ) and ( length $runConf{kernel} ) ) {
|
|
unless ( -f $runConf{kernel} ) {
|
|
printf "The provided kernel %s was not found, unable to continue", $runConf{kernel};
|
|
exit;
|
|
}
|
|
} else {
|
|
$runConf{kernel} = latestKernel;
|
|
}
|
|
|
|
$runConf{bootdir} = dirname( $runConf{kernel} );
|
|
( $runConf{kernel_prefix}, $runConf{kernel_version} ) = split( '-', basename( $runConf{kernel} ) );
|
|
|
|
# We always need to create an initramfs
|
|
printf "Creating ZFS Boot Menu %s, with %s %s\n", $runConf{version}, $runConf{kernel_prefix}, $runConf{kernel_version};
|
|
|
|
$runConf{initramfs} = createInitramfs( $tempdir, $runConf{kernel_version} );
|
|
|
|
# Create a unified kernel/initramfs/command line EFI file
|
|
if ( defined( $config{EFI}{Copies} ) and ( $config{EFI}{Copies} gt 0 ) ) {
|
|
$runConf{unified_efi} = unifiedEFI( $tempdir, $runConf{kernel}, $runConf{initramfs} );
|
|
|
|
if ( defined( $config{EFI}{Versioned} ) and ( $config{EFI}{Versioned} ) ) {
|
|
$runConf{efi_target} =
|
|
sprintf( "%s/%s-%s.EFI", $config{EFI}{ImageDir}, $runConf{kernel_prefix}, $runConf{version} );
|
|
} else {
|
|
$runConf{efi_target} = sprintf( "%s/%s.EFI", $config{EFI}{ImageDir}, $runConf{kernel_prefix} );
|
|
}
|
|
|
|
my $glob = sprintf( "%s/%s-*.EFI", $config{Components}{ImageDir}, $runConf{kernel_prefix} );
|
|
my @efi = sort glob($glob);
|
|
|
|
my $index = 0;
|
|
foreach my $entry (@efi) {
|
|
if ( $entry eq $runConf{efi_target} ) {
|
|
splice @efi, $index, 1;
|
|
}
|
|
$index++;
|
|
}
|
|
|
|
printf "Found %s existing EFI images, allowed to have a total of %s\n", scalar @efi, $config{EFI}{Copies};
|
|
while ( scalar @efi > $config{EFI}{Copies} ) {
|
|
my $image = shift(@efi);
|
|
printf "Removing %s\n", $image;
|
|
unlink $image;
|
|
}
|
|
|
|
make_path $config{EFI}{ImageDir};
|
|
if ( safeCopy( $runConf{unified_efi}, $runConf{efi_target} ) ) {
|
|
printf "Created a unified EFI at %s\n", $runConf{efi_target};
|
|
}
|
|
}
|
|
|
|
# Create a separate kernel / initramfs. Used by syslinux/extlinux/grub.
|
|
if ( defined( $config{Components}{Copies} ) and ( $config{Components}{Copies} gt 0 ) ) {
|
|
if ( defined( $config{Components}{Versioned} ) and ( $config{Components}{Versioned} ) ) {
|
|
$runConf{kernel_target} =
|
|
sprintf( "%s/%s-%s", $config{Components}{ImageDir}, $runConf{kernel_prefix}, $runConf{version} );
|
|
$runConf{initramfs_target} = sprintf( "%s/initramfs-%s.img", $config{Components}{ImageDir}, $runConf{version} );
|
|
|
|
my $glob = sprintf( "%s/%s-*", $config{Components}{ImageDir}, $runConf{kernel_prefix} );
|
|
my @listing = sort glob($glob);
|
|
|
|
# Filter EFI files and our target image
|
|
my @components;
|
|
foreach my $entry (@listing) {
|
|
if ( $entry eq $runConf{kernel_target} ) {
|
|
next;
|
|
} elsif ( $entry =~ /EFI$/i ) {
|
|
next;
|
|
}
|
|
push( @components, $entry );
|
|
}
|
|
|
|
printf "Found %s existing images, allowed to have a total of %s\n", scalar @components, $config{Components}{Copies};
|
|
while ( scalar @components > $config{Components}{Copies} ) {
|
|
my $kernel = shift(@components);
|
|
my $initramfs = sprintf( "%s.img", $kernel );
|
|
$initramfs =~ s/\Q$runConf{kernel_prefix}/initramfs/;
|
|
printf "Removing %s, %s\n", $kernel, $initramfs;
|
|
unlink $kernel;
|
|
unlink $initramfs;
|
|
}
|
|
} else {
|
|
$runConf{kernel_target} = sprintf( "%s/%s-bootmenu", $config{Components}{ImageDir}, $runConf{kernel_prefix} );
|
|
$runConf{kernel_backup} =
|
|
sprintf( "%s/%s-bootmenu-backup", $config{Components}{ImageDir}, $runConf{kernel_prefix} );
|
|
$runConf{initramfs_target} = sprintf( "%s/initramfs-bootmenu.img", $config{Components}{ImageDir} );
|
|
$runConf{initramfs_backup} = sprintf( "%s/initramfs-bootmenu-backup.img", $config{Components}{ImageDir} );
|
|
|
|
if ( -f $runConf{kernel_target} ) {
|
|
if ( safeCopy( $runConf{kernel_target}, $runConf{kernel_backup} )
|
|
and safeCopy( $runConf{initramfs_target}, $runConf{initramfs_backup} ) )
|
|
{
|
|
printf "Created %s, %s\n", $runConf{kernel_backup}, $runConf{initramfs_backup};
|
|
}
|
|
}
|
|
}
|
|
|
|
make_path $config{Components}{ImageDir};
|
|
if ( safeCopy( $runConf{kernel}, $runConf{kernel_target} )
|
|
and safeCopy( $runConf{initramfs}, $runConf{initramfs_target} ) )
|
|
{
|
|
printf "Created %s, %s\n", $runConf{kernel_target}, $runConf{initramfs_target};
|
|
}
|
|
}
|
|
|
|
# Generate syslinux.cfg, requires components to be built
|
|
if ( defined( $config{syslinux}{CreateConfig} ) and ( $config{syslinux}{CreateConfig} eq 1 ) ) {
|
|
my $glob = sprintf( "%s/%s-*", $config{Components}{ImageDir}, $runConf{kernel_prefix} );
|
|
my @listing = sort glob($glob);
|
|
|
|
# Filter EFI files, in case they're in the same directory
|
|
my @components;
|
|
foreach my $entry (@listing) {
|
|
if ( $entry =~ /EFI$/i ) {
|
|
next;
|
|
}
|
|
push( @components, $entry );
|
|
}
|
|
|
|
$runConf{syslinux_temp} = join( '/', $tempdir, 'syslinux.conf' );
|
|
open CFG, '>', $runConf{syslinux_temp};
|
|
|
|
my $header = <<'EOF';
|
|
UI menu.c32
|
|
PROMPT 0
|
|
|
|
MENU TITLE Boot Menu
|
|
TIMEOUT 50
|
|
EOF
|
|
|
|
print CFG $header;
|
|
|
|
my $add_default = 1;
|
|
while (@components) {
|
|
my $entry = pop(@components);
|
|
|
|
my $directory = dirname($entry);
|
|
|
|
# Strip the mountpoint prefix out to generate a correct path based on /
|
|
$directory =~ s/\Q$config{Global}{BootMountPoint}//;
|
|
|
|
my $kernel = basename($entry);
|
|
my ( undef, $version ) = split( '-', $kernel );
|
|
my $label = "ZFSBootMenu-$version";
|
|
my $menu_label = "ZFS Boot Menu v$version";
|
|
|
|
if ($add_default) {
|
|
print CFG "DEFAULT $label\n\n";
|
|
$add_default--;
|
|
}
|
|
|
|
print CFG "LABEL $label\n";
|
|
print CFG "MENU LABEL $menu_label\n";
|
|
print CFG "KERNEL $directory/$kernel\n";
|
|
print CFG "INITRD $directory/initramfs-$version.img\n";
|
|
print CFG "APPEND $config{Kernel}{CommandLine}\n";
|
|
print CFG "\n";
|
|
|
|
}
|
|
close CFG;
|
|
|
|
make_path dirname( $config{syslinux}{Config} );
|
|
safeCopy( $runConf{syslinux_temp}, $config{syslinux}{Config} );
|
|
}
|
|
|
|
END {
|
|
cleanupMount;
|
|
}
|
|
|
|
# Finds the latest kernel in /boot
|
|
sub latestKernel {
|
|
my @prefixes = ( "vmlinux*", "vmlinuz*", "linux*", "kernel*" );
|
|
for my $prefix (@prefixes) {
|
|
my $glob = join( '/', ( $runConf{bootdir}, $prefix ) );
|
|
my @kernels = glob($glob);
|
|
next if !@kernels;
|
|
for ( sort { versioncmp( $b, $a ) } @kernels ) {
|
|
return $_;
|
|
}
|
|
}
|
|
}
|
|
|
|
# Returns the path to an initramfs, or dies with an error
|
|
sub createInitramfs {
|
|
my ( $temp, $kver ) = @_;
|
|
|
|
my $output_file = join( '/', $temp, "zfsbootmenu" );
|
|
my @cmd = ( qw(dracut -q -f --confdir), $runConf{confd}, $output_file, qw(--kver), $kver, );
|
|
my @output = execute(@cmd);
|
|
my $status = pop(@output);
|
|
if ( $status eq 0 ) {
|
|
return $output_file;
|
|
} else {
|
|
print Dumper(@output);
|
|
die "Failed to create $output_file";
|
|
}
|
|
}
|
|
|
|
sub unifiedEFI {
|
|
my ( $temp, $kernel, $initramfs ) = @_;
|
|
|
|
my $output_file = join( '/', $temp, "zfsbootmenu.efi" );
|
|
my $cmdline_file = join( '/', $temp, "cmdline.txt" );
|
|
my $cmdline = $config{Kernel}{CommandLine};
|
|
|
|
open( my $fh, '>', $cmdline_file );
|
|
print $fh $cmdline;
|
|
close($fh);
|
|
|
|
my @cmd = (
|
|
qw(objcopy),
|
|
qw(--add-section .osrel=/etc/os-release --change-section-vma .osrel=0x20000),
|
|
qq(--add-section .cmdline=$cmdline_file),
|
|
qw(--change-section-vma .cmdline=0x30000),
|
|
qq(--add-section .linux=$kernel),
|
|
qw(--change-section-vma .linux=0x40000),
|
|
qq(--add-section .initrd=$initramfs),
|
|
qw(--change-section-vma .initrd=0x3000000),
|
|
qw(/usr/lib/gummiboot/linuxx64.efi.stub),
|
|
$output_file
|
|
);
|
|
|
|
my @output = execute(@cmd);
|
|
my $status = pop(@output);
|
|
if ( $status eq 0 ) {
|
|
return $output_file;
|
|
} else {
|
|
print Dumper(@output);
|
|
die "Failed to create $output_file";
|
|
}
|
|
}
|
|
|
|
sub execute {
|
|
( @_ = qx{@_ 2>&1}, $? >> 8 );
|
|
}
|
|
|
|
sub safeCopy {
|
|
my ( $source, $dest ) = @_;
|
|
|
|
unless ( copy( $source, $dest ) ) {
|
|
printf "Unable to copy %s to %s: %s\n", $source, $dest, $!;
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub cleanupMount {
|
|
if ( $runConf{umount_on_exit} ) {
|
|
print "Unmounting $config{Global}{BootMountPoint}\n";
|
|
my $cmd = "umount $config{Global}{BootMountPoint}";
|
|
execute($cmd);
|
|
}
|
|
}
|