zfsbootmenu/bin/generate-zbm

1133 lines
30 KiB
Perl
Executable File

#!/usr/bin/env perl
# vim: softtabstop=2 shiftwidth=2 expandtab
use strict;
use warnings;
our $VERSION = '2.3.0+dev';
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::stat;
use File::Path qw(make_path remove_tree);
use File::Glob qw(:globally :nocase);
use Sort::Versions;
use bigint qw(hex);
use Pod::Usage qw(pod2usage);
use Data::Dumper;
$Data::Dumper::Indent = 1;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Purity = 1;
use Sort::Versions;
use YAML::PP;
use boolean;
use Storable qw( dclone );
use constant REFARRAY => ref [];
sub versionedKernel;
sub latestKernel;
sub createInitramfs;
sub createUEFIBundle;
sub execute;
sub safeCopy;
sub nonempty;
sub cleanupMount;
sub enabled;
sub maxRevision;
sub groupKernels;
sub pruneVersions;
sub purgeFiles;
sub verboseUnlink;
BEGIN {
$SIG{INT} = \&cleanupMount;
$SIG{TERM} = \&cleanupMount;
}
my ( %runConf, %config );
$runConf{config} = "/etc/zfsbootmenu/config.yaml";
$runConf{bootdir} = "/boot";
GetOptions(
"version|v=s" => \$runConf{version},
"kernel|k=s" => \$runConf{kernel},
"kver|K=s" => \$runConf{kernel_version},
"prefix|p=s" => \$runConf{kernel_prefix},
"bootdir|b=s" => \$runConf{bootdir},
"confd|C=s" => \$runConf{confd},
"cmdline|l=s" => \$runConf{cmdline},
"config|c=s" => \$runConf{config},
"enable" => \$runConf{enable},
"disable" => \$runConf{disable},
"initcpio|i!" => \$runConf{usecpio},
"hookd|H=s@" => \$runConf{cpio_hookd},
"debug|d" => \$runConf{debug},
"showver|V" => \$runConf{showver},
"help|h" => sub {
pod2usage( -verbose => 2 );
exit;
},
) or exit 1;
if ( defined $runConf{showver} and $runConf{showver} ) {
printf "%s\n", $VERSION;
exit 0;
}
if ( -r $runConf{config} ) {
eval {
local $SIG{'__DIE__'};
my $yaml = YAML::PP->new( boolean => 'boolean' )->load_file( $runConf{config} );
%config = %$yaml;
} or do {
my $error = <<"EOF";
Unable to parse configuration $runConf{config} as YAML.
EOF
print $error;
warn $@ if $@;
exit 1;
};
} else {
printf "Configuration %s does not exist or is unreadable\n", $runConf{config};
exit 1;
}
if ( $runConf{disable} ) {
$runConf{enable} = false;
}
if ( defined $runConf{enable} ) {
$config{Global}{ManageImages} = boolean( $runConf{enable} );
my $yaml = YAML::PP->new(
boolean => 'boolean',
header => 0,
);
$yaml->dump_file( $runConf{config}, \%config );
my $state = $runConf{enable} ? "true" : "false";
printf "ManageImages set to '%s' in %s\n", $state, $runConf{config};
exit;
}
unless ( $config{Global}{ManageImages} ) {
print "ManageImages not enabled, no action taken\n";
exit;
}
unless ( defined $runConf{usecpio} ) {
# If usecpio wasn't set by cmdline, try loading from the config
if ( defined $config{Global}{InitCPIO} ) {
$runConf{usecpio} = $config{Global}{InitCPIO};
} else {
my @output = execute(qw(sh -c "command -v dracut"));
my $status = pop(@output);
if ( $status eq 0 ) {
print "No initramfs generator specified; using dracut\n";
$runConf{usecpio} = false;
} else {
print "No initramfs generator specified; using mkinitcpio\n";
$runConf{usecpio} = true;
}
}
}
unless ( defined $runConf{confd} ) {
# Set initramfs configuration from defaults or config file
# Defaults and config key depend on initramfs generator
my $ckey;
if ( $runConf{usecpio} ) {
$runConf{confd} = "/etc/zfsbootmenu/mkinitcpio.conf";
$ckey = "InitCPIOConfig";
} else {
$runConf{confd} = "/etc/zfsbootmenu/dracut.conf.d";
$ckey = "DracutConfDir";
}
# Replace the default if a configuration option exists
if ( defined $config{Global}{$ckey} ) {
$runConf{confd} = $config{Global}{$ckey};
}
}
if ( $runConf{usecpio} and not defined $runConf{cpio_hookd} ) {
# With initcpio mode, load hookdirs from config when not specified on cmdline
my @hooks;
if ( defined $config{Global}{InitCPIOHookDirs} ) {
if ( ref $config{Global}{InitCPIOHookDirs} eq REFARRAY ) {
foreach my $hookd ( @{ $config{Global}{InitCPIOHookDirs} } ) {
push( @hooks, $hookd );
}
} else {
push( @hooks, $config{Global}{InitCPIOHookDirs} );
}
}
$runConf{cpio_hookd} = \@hooks;
}
# Ensure our bootloader partition is mounted
$runConf{umount_on_exit} = 0;
if ( nonempty $config{Global}{BootMountPoint} ) {
my $mounted = 0;
my @output = execute(qq(mountpoint $config{Global}{BootMountPoint}));
my $status = pop(@output);
unless ( $status eq 0 ) {
print "Mounting $config{Global}{BootMountPoint}\n";
my @output = execute(qq(mount $config{Global}{BootMountPoint}));
my $status = pop(@output);
if ( $status eq 0 ) {
$runConf{umount_on_exit} = 1;
} else {
foreach my $line (@output) {
print $line;
}
printf "Unable to mount %s\n", $config{Global}{BootMountPoint};
exit $status;
}
}
}
if ( nonempty $config{Global}{PreHooksDir} and -d $config{Global}{PreHooksDir} ) {
while ( my $hook = <$config{Global}{PreHooksDir}/*> ) {
next unless -x $hook;
Log("Processing hook: $hook");
my @output = execute(qq($hook));
Log( \@output );
}
}
# Create a temp directory
# It is automatically purged on program exit
my $dir = File::Temp->newdir();
my $tempdir = $dir->dirname;
# Config file may provide some default values for command-line args
if ( nonempty $config{Kernel}{Path} and !nonempty $runConf{kernel} ) {
$runConf{kernel} = $config{Kernel}{Path};
}
if ( nonempty $config{Kernel}{Prefix} and !nonempty $runConf{kernel_prefix} ) {
$runConf{kernel_prefix} = $config{Kernel}{Prefix};
}
if ( nonempty $config{Kernel}{Version} and !nonempty $runConf{kernel_version} ) {
$runConf{kernel_version} = $config{Kernel}{Version};
$runConf{kernel_version} =~ s/%current\b/%{current}/i;
}
if ( nonempty $config{Global}{Version} and !nonempty $runConf{version} ) {
$runConf{version} = $config{Global}{Version};
}
if ( nonempty $config{Kernel}{CommandLine} and !nonempty $runConf{cmdline} ) {
$runConf{cmdline} = $config{Kernel}{CommandLine};
}
if ( nonempty $runConf{version} ) {
$runConf{version} =~ s/%current\b/%{current}/i;
$runConf{version} =~ s/%\{current\}/$VERSION/i;
} else {
$runConf{version} = $VERSION;
}
# Map "%current" kernel version to output of `uname r`
if ( nonempty $runConf{kernel_version} and $runConf{kernel_version} =~ /%\{current\}/i ) {
my @uname = execute(qw(uname -r));
my $status = pop(@uname);
unless ( $status eq 0 and scalar @uname ) {
print "Cannot determine current kernel version\n";
exit $status;
}
chomp @uname;
$runConf{kernel_version} =~ s/%\{current\}/$uname[0]/i;
}
if ( nonempty $runConf{kernel} ) {
# Make sure the provided kernel file exists
unless ( -f $runConf{kernel} ) {
printf "The provided kernel %s was not found, unable to continue\n", $runConf{kernel};
exit 1;
}
} else {
# Try to determine a kernel file when one was not provided
if ( nonempty $runConf{kernel_version} ) {
my $exactVersion;
( $runConf{kernel}, $exactVersion ) = versionedKernel $runConf{kernel_version};
# Make sure a kernel was found
unless ( nonempty $runConf{kernel} ) {
print "Unable to find file for kernel version $runConf{kernel_version}\n";
exit 1;
}
# If the kernel version was not exact, allow it to be determined later
unless ($exactVersion) {
undef $runConf{kernel_version};
}
} else {
$runConf{kernel} = latestKernel;
unless ( nonempty $runConf{kernel} ) {
print "Unable to find latest kernel; specify version or path manually\n";
exit 1;
}
}
}
# Try to determine kernel_prefix or kernel_version if necessary
unless ( nonempty $runConf{kernel_version} ) {
# Kernel version comes from either file name or internal strings
$runConf{kernel_version} = kernelVersion( $runConf{kernel} );
unless ( nonempty $runConf{kernel_version} ) {
printf "Unable to determine kernel version from %s\n", $runConf{kernel};
exit 1;
}
}
unless ( nonempty $runConf{kernel_prefix} ) {
# Prefix is basename of file, less any "-<version>" suffix
$runConf{kernel_prefix} = basename( $runConf{kernel} );
$runConf{kernel_prefix} =~ s/-\Q$runConf{kernel_version}\E$//;
unless ( nonempty $runConf{kernel_prefix} ) {
printf "Unable to determine kernel prefix from %s\n", $runConf{kernel};
exit 1;
}
}
printf "Creating ZFSBootMenu %s from kernel %s\n", $VERSION, $runConf{kernel};
my $spl_hostid = "/sys/module/spl/parameters/spl_hostid";
if ( -f $spl_hostid ) {
open PROC, $spl_hostid;
$runConf{hostid}{module} = sprintf( "%08x", <PROC> );
close PROC;
} else {
$runConf{hostid}{module} = "00000000";
}
my $etc_hostid = "/etc/hostid";
if ( $runConf{hostid}{module} ne "00000000" and -f $etc_hostid ) {
open SPL, '<:raw', $etc_hostid;
read SPL, my $hostid, 4;
close SPL;
$runConf{hostid}{etc} = sprintf( "%08x", unpack( 'L<4', $hostid ) );
if ( $runConf{hostid}{module} ne $runConf{hostid}{etc} ) {
print "SPL ($runConf{hostid}{module}) and system ($runConf{hostid}{etc}) hostids do not match!\n";
}
}
# Create the initramfs as long as some output will consume it
my $initramfs;
if ( enabled $config{EFI} or enabled $config{Components} ) {
$initramfs = createInitramfs( $tempdir, $runConf{kernel_version} );
}
# Create a unified kernel/initramfs/command line EFI file
if ( enabled $config{EFI} ) {
my $unified_efi = createUEFIBundle( $tempdir, $runConf{kernel}, $initramfs );
my $efi_target;
my $efi_prefix = sprintf( "%s/%s", $config{EFI}{ImageDir}, $runConf{kernel_prefix} );
Log("Setting \$efi_prefix: $efi_prefix");
my $efi_versions = int $config{EFI}{Versions};
make_path $config{EFI}{ImageDir};
if ( $efi_versions > 0 ) {
Log("EFI.Versions is $efi_versions");
# Find UEFI bundles and group by apparent version
my @efi = glob sprintf( "%s-*.EFI", $efi_prefix );
my $efi_groups = groupKernels( \@efi, $efi_prefix, ".EFI" );
Log($efi_groups);
# Determine the revision to use for this image
my $revision = maxRevision( $efi_groups->{ $runConf{version} }, ".EFI" ) + 1;
$efi_target = sprintf( "%s-%s_%s.EFI", $efi_prefix, $runConf{version}, $revision );
Log("Setting \$efi_target: $efi_target");
# Attempt to copy the file, clean up if it does not
unless ( safeCopy( $unified_efi, $efi_target, 0 ) ) {
verboseUnlink( $efi_target, "Failed to create $efi_target" );
exit 1;
}
# Prune the old versions
pruneVersions( $efi_groups, $runConf{version}, $efi_versions );
} else {
$efi_target = sprintf( "%s.EFI", $efi_prefix );
# Copy to a placeholder location to ensure success
my ( $efi_fh, $efi_tempfile ) = tempfile( "zbm.XXXXXX", DIR => $config{EFI}{ImageDir}, UNLINK => 0 );
close $efi_fh;
unless ( safeCopy( $unified_efi, $efi_tempfile, 0 ) ) {
verboseUnlink( $efi_tempfile, "Failed to create $efi_target" );
exit 1;
}
# Roll backups
my $efi_backup = sprintf( "%s-backup.EFI", $efi_prefix );
if ( -f $efi_target and rename( $efi_target, $efi_backup ) ) {
printf "Created backup %s -> %s\n", $efi_target, $efi_backup;
}
unless ( rename( $efi_tempfile, $efi_target ) ) {
verboseUnlink( $efi_tempfile, "Failed to create $efi_target" );
exit 1;
}
}
printf "Created new UEFI image %s\n", $efi_target;
}
# Create a separate kernel / initramfs. Used by syslinux/extlinux/grub.
if ( enabled $config{Components} ) {
my ( $kernel_target, $initramfs_target );
my $component_prefix = sprintf( "%s/%s", $config{Components}{ImageDir}, $runConf{kernel_prefix} );
my $component_versions = int $config{Components}{Versions};
make_path $config{Components}{ImageDir};
if ( $component_versions > 0 ) {
# Find ZBM kernels and group by apparent version
my @kernels = glob( sprintf( "%s-*", $component_prefix ) );
my $kern_groups = groupKernels( \@kernels, $component_prefix );
my $revision = maxRevision( $kern_groups->{ $runConf{version} } ) + 1;
$kernel_target = sprintf( "%s-%s_%s", $component_prefix, $runConf{version}, $revision );
$initramfs_target =
sprintf( "%s/initramfs-%s_%s.img", $config{Components}{ImageDir}, $runConf{version}, $revision );
unless ( safeCopy( $initramfs, $initramfs_target, 0 ) ) {
verboseUnlink( $initramfs_target, "Failed to create $initramfs_target" );
exit 1;
}
unless ( safeCopy( $runConf{kernel}, $kernel_target, 0 ) ) {
verboseUnlink( $kernel_target, "Failed to create $kernel_target" );
verboseUnlink( $initramfs_target, "" );
exit 1;
}
# Prune old versions of the kernel
pruneVersions( $kern_groups, $runConf{version}, $component_versions );
# Map each kernel to initramfs and prune those too
keys %$kern_groups;
while ( my ( $kver, $image ) = each %$kern_groups ) {
foreach (@$image) {
s/\Q$component_prefix\E/$config{Components}{ImageDir}\/initramfs/;
s/$/.img/;
}
}
pruneVersions( $kern_groups, $runConf{version}, $component_versions );
} else {
$kernel_target = sprintf( "%s-bootmenu", $component_prefix );
$initramfs_target = sprintf( "%s/initramfs-bootmenu.img", $config{Components}{ImageDir} );
# Copy to a placeholder location to ensure success
my ( $init_fh, $init_tempfile ) = tempfile( "init.XXXXXX", DIR => $config{Components}{ImageDir}, UNLINK => 0 );
close $init_fh;
unless ( safeCopy( $initramfs, $init_tempfile, 0 ) ) {
verboseUnlink( $init_tempfile, "Failed to create $initramfs_target" );
exit 1;
}
my ( $kern_fh, $kern_tempfile ) = tempfile( "kern.XXXXXX", DIR => $config{Components}{ImageDir}, UNLINK => 0 );
close $kern_fh;
unless ( safeCopy( $runConf{kernel}, $kern_tempfile, 0 ) ) {
verboseUnlink( $kern_tempfile, "Failed to create $kernel_target" );
verboseUnlink( $init_tempfile, "" );
exit 1;
}
# Roll backups
my $kernel_backup = sprintf( "%s-backup", $kernel_target );
if ( -f $kernel_target and rename( $kernel_target, $kernel_backup ) ) {
printf "Created backup %s -> %s\n", $kernel_target, $kernel_backup;
}
my $initramfs_backup = sprintf( "%s/initramfs-bootmenu-backup.img", $config{Components}{ImageDir} );
if ( -f $initramfs_target and rename( $initramfs_target, $initramfs_backup ) ) {
printf "Created backup %s -> %s\n", $initramfs_target, $initramfs_backup;
}
unless ( rename( $init_tempfile, $initramfs_target ) ) {
verboseUnlink( $init_tempfile, "Failed to create $initramfs_target" );
verboseUnlink( $kern_tempfile, "" );
exit 1;
}
unless ( rename( $kern_tempfile, $kernel_target ) ) {
verboseUnlink( $kern_tempfile, "Failed to create $kernel_target" );
exit 1;
}
}
printf "Created initramfs image %s\n", $initramfs_target;
printf "Created kernel image %s\n", $kernel_target;
}
if ( nonempty $config{Global}{PostHooksDir} and -d $config{Global}{PostHooksDir} ) {
while ( my $hook = <$config{Global}{PostHooksDir}/*> ) {
next unless -x $hook;
Log("Processing hook: $hook");
my @output = execute(qq($hook));
Log( \@output );
}
}
END {
cleanupMount;
}
# Finds specifically versioned kernel in /boot
sub versionedKernel {
my ( $kver, ) = @_;
foreach my $prefix (qw(vmlinuz linux vmlinux kernel)) {
my $pattern = join( '/', ( $runConf{bootdir}, join( '-', ( $prefix, $kver ) ) ) );
# Try an exact match first
if ( -f $pattern ) {
return $pattern, true;
}
# Otherwise, try to glob
my @kernels = sort versioncmp glob($pattern);
next unless @kernels;
return pop @kernels, false;
}
return;
}
# Finds the latest kernel in /boot, if possible
sub latestKernel {
my @prefixes = ( "vmlinuz*", "vmlinux*", "linux*", "kernel*" );
for my $prefix (@prefixes) {
my $glob = join( '/', ( $runConf{bootdir}, $prefix ) );
my %kernels;
for my $kernel ( glob($glob) ) {
my $version = kernelVersion($kernel);
next unless defined($version);
Log("Identified version $version for kernel $kernel");
$kernels{$version} = $kernel;
}
next unless ( keys %kernels );
for ( sort { versioncmp( $b, $a ) } keys %kernels ) {
Log("Latest kernel: $_");
return $kernels{$_};
}
}
return;
}
# Attempts to determine a version for the given kernel, by
#
# a. Identifying the first version-looking string in the file, or
# b. Identifying a version-like part in the name of the file
#
# If one of these exists and not the other, that value is used; if both exist,
# the name-derived value is used if that version string can be matched
# somewhere in the file contents, otherwise the version is undefined.
sub kernelVersion {
my $kernel = shift;
my ( $filever, $namever );
# Consider an unreadable file to have no version
unless ( -r $kernel ) {
Log("Unable to read path $kernel, assuming no version");
return;
}
# Read version from the file name, if possible
basename($kernel) =~ m/-([0-9]+\.[0-9]+\.[0-9]+.*)/;
if ( defined $1 ) {
$namever = $1;
}
# Read strings in the kernel to recover a version, if possible
my @output = execute(qq(strings $kernel));
my $status = pop(@output);
if ( $status eq 0 ) {
for (@output) {
# Versions are any three dot-separated numbers followed by non space
next unless (/([0-9]+\.[0-9]+\.[0-9]+\S+)/);
my $ver = $1;
# First version match is always the file version
$filever = $ver unless ( nonempty $filever );
# When there is no version from the file name, we have a match
last unless ( nonempty $namever );
# A version that equals the file version supersedes the first match
if ( $namever eq $ver ) {
$filever = $ver;
last;
}
}
}
# If only one is defined, that's the version
unless ( nonempty $filever ) {
Log("No version found in kernel strings, using $namever from path $kernel");
return $namever;
}
unless ( nonempty $namever ) {
Log("No version found in path $kernel, using $filever from kernel strings");
return $filever;
}
# Warn if the two alternatives do not agree
if ( $namever ne $filever ) {
my $warning = <<"EOF";
WARNING: ignoring inconsistent versions in kernel $kernel:
Path suggests version $namever.
Kernel strings suggest version $filever.
To use this kernel, explicitly specify the path and version.
EOF
print $warning;
return;
}
return $namever;
}
# Given a sections size, calculate where the next section should be placed,
# while respecting the stub alignment value
sub increaseBundleOffset {
my ( $step, $offset, $alignment ) = @_;
$offset += int( ( $step + $alignment - 1 ) / $alignment * $alignment );
Log( "New offset is: " . hex($offset) );
return $offset;
}
# Adds the commands necessary to put another section into the EFI bundle,
# and then calculates where the bundle offset has been moved to
sub addBundleSection {
my ( $cmds, $secname, $filename, $offset, $alignment ) = @_;
my $hex_offset = sprintf( "0x%X", $offset );
push( @$cmds, ( "--add-section", "$secname=\"$filename\"" ), qw(--change-section-vma), ("$secname=\"$hex_offset\""),
);
my $sb = stat($filename);
return increaseBundleOffset( $sb->size, $offset, $alignment );
}
# Creates a UEFI bundle from an initramfs and kernel
# Returns the path to the bundle or dies with an error
sub createUEFIBundle {
my ( $imagedir, $kernel, $initramfs ) = @_;
my $output_file = join( '/', $imagedir, "zfsbootmenu.EFI" );
unless ( -f $kernel and -f $initramfs ) {
print "Cannot find kernel or initramfs to create UEFI bundle\n";
exit 1;
}
my $uefi_stub;
if ( nonempty $config{EFI}{Stub} ) {
$uefi_stub = $config{EFI}{Stub};
unless ( -f $uefi_stub ) {
print "UEFI stub loader '$uefi_stub' does not exist\n";
exit 1;
}
} else {
# For now, default stub locations are x86_64 only
my @uefi_stub_defaults = qw(
/usr/lib/systemd/boot/efi/linuxx64.efi.stub
/usr/lib/gummiboot/linuxx64.efi.stub
);
foreach my $stubloc (@uefi_stub_defaults) {
if ( -f $stubloc ) {
$uefi_stub = $stubloc;
last;
}
}
unless ( defined $uefi_stub and -f $uefi_stub ) {
print "Unable to find UEFI stub loader at default locations:\n";
foreach my $stubloc (@uefi_stub_defaults) {
print " $stubloc\n";
}
exit 1;
}
}
my ( $uki_alignment, $uki_offset );
# Determine stub alignment, most likely 4096
my @cmd = qw(objdump -p);
push( @cmd, $uefi_stub );
my @output = execute(@cmd);
my $status = pop(@output);
if ( $status eq 0 ) {
foreach my $line (@output) {
if ( $line =~ m/SectionAlignment\s+(\d+)/ ) {
Log( "Alignment is: " . hex($1) );
$uki_alignment = hex($1);
}
}
} else {
print "Unable to determine stub alignment!\n";
exit 1;
}
# Determine initial UKI offset value by grabbing the size and VMA of
# the last section of the EFI stub.
@cmd = qw(objdump -w -h);
push( @cmd, $uefi_stub );
@output = execute(@cmd);
$status = pop(@output);
if ( $status eq 0 ) {
my @sizes = split( /\s+/, @output[ scalar @output - 1 ] );
my $size = "0x" . $sizes[3];
my $vma = "0x" . $sizes[4];
my $sum = hex($size) + hex($vma);
$uki_offset = increaseBundleOffset( $sum, 0, $uki_alignment );
Log( "Initial offset is: " . hex($uki_offset) );
} else {
print "Unable to determine initial stub offset!\n";
exit 1;
}
@cmd = qw(objcopy);
my ( $hex_offset, $sb );
if ( -f "/etc/os-release" ) {
$uki_offset = addBundleSection( \@cmd, ".osrel", "/etc/os-release", $uki_offset, $uki_alignment );
}
if ( nonempty $runConf{cmdline} ) {
my $cmdline = join( '/', $imagedir, "cmdline.txt" );
open( my $fh, '>', $cmdline );
print $fh $runConf{cmdline};
close($fh);
$uki_offset = addBundleSection( \@cmd, ".cmdline", $cmdline, $uki_offset, $uki_alignment );
}
if ( nonempty $config{EFI}{SplashImage} and -f $config{EFI}{SplashImage} ) {
# only supported with systemd-boot's efistub,
# but gummiboot doesn't care if the section exists
$uki_offset = addBundleSection( \@cmd, ".splash", $config{EFI}{SplashImage}, $uki_offset, $uki_alignment );
}
$uki_offset = addBundleSection( \@cmd, ".initrd", $initramfs, $uki_offset, $uki_alignment );
# Add the kernel last, so that it can decompress without overflowing other sections
$uki_offset = addBundleSection( \@cmd, ".linux", $kernel, $uki_offset, $uki_alignment );
push( @cmd, ( $uefi_stub, $output_file ) );
my $command = join( ' ', @cmd );
Log("Executing: $command");
@output = execute(@cmd);
$status = pop(@output);
if ( $status eq 0 ) {
foreach my $line (@output) {
Log($line);
}
return $output_file;
} else {
foreach my $line (@output) {
print $line;
}
print "Failed to create $output_file\n";
exit $status;
}
}
# Creates an initramfs and returns its path, or dies with an error
sub createInitramfs {
my ( $imagedir, $kver ) = @_;
my $output_file = join( '/', $imagedir, "zfsbootmenu.img" );
my @cmd;
my $flagsKey;
if ( $runConf{usecpio} ) {
push( @cmd, ( qw(mkinitcpio --config), $runConf{confd} ) );
push( @cmd, qw(-v) ) if $runConf{debug};
# Add hook directories as appropriate
if ( defined $runConf{cpio_hookd} ) {
foreach my $hookd ( @{ $runConf{cpio_hookd} } ) {
push( @cmd, ( "--hookdir", $hookd ) );
}
}
$flagsKey = "InitCPIOFlags";
} else {
push( @cmd, ( qw(dracut -f --confdir), $runConf{confd} ) );
push( @cmd, qw(-q) ) unless $runConf{debug};
$flagsKey = "DracutFlags";
}
# Load custom flag additions from configuration file
if ( defined $config{Global}{$flagsKey} ) {
if ( ref $config{Global}{$flagsKey} eq REFARRAY ) {
foreach my $flag ( @{ $config{Global}{$flagsKey} } ) {
push( @cmd, $flag );
}
} else {
push( @cmd, $config{Global}{$flagsKey} );
}
}
# Specify kernel version and ouptut location
if ( $runConf{usecpio} ) {
push( @cmd, ( qw(-A zfsbootmenu), "--generate", $output_file, "--kernel", $kver ) );
} else {
push( @cmd, ( $output_file, $kver ) );
}
my $command = join( ' ', @cmd );
Log("Executing: $command");
my @output = execute(@cmd);
my $status = pop(@output);
if ( $status eq 0 ) {
foreach my $line (@output) {
Log($line);
}
return $output_file;
} else {
foreach my $line (@output) {
print $line;
}
print "Failed to create $output_file\n";
exit $status;
}
}
sub execute {
( @_ = qx{@_ 2>&1}, $? >> 8 );
}
sub safeCopy {
my ( $source, $dest, $savetime ) = @_;
my $preserve = ( defined $savetime ) ? boolean($savetime) : true;
Log("safeCopy called with: $source, $dest, $preserve");
unless ( copy( $source, $dest ) ) {
printf "Unable to copy %s to %s: %s\n", $source, $dest, $!;
return 0;
}
if ($preserve) {
# Copy the access and mod times if possible
my $sb = stat $source;
utime( $sb->atime, $sb->mtime, $dest );
}
return 1;
}
sub nonempty {
my $item = shift;
return ( defined $item and length $item );
}
sub enabled {
my $section = shift;
return ( defined $section->{Enabled} and $section->{Enabled} );
}
sub cleanupMount {
my $signal = shift;
if ( $runConf{umount_on_exit} ) {
print "Unmounting $config{Global}{BootMountPoint}\n";
execute(qq(umount $config{Global}{BootMountPoint}));
}
if ( defined $signal ) {
print "$0 terminating on signal $signal\n";
exit 1;
}
}
sub maxRevision {
my ( $files, $suffix ) = @_;
my $revision = 0;
$suffix = "" unless ( defined $suffix );
foreach my $file (@$files) {
if ( $file =~ /_(\d+)\Q$suffix\E$/ ) {
$revision = $1 if ( $1 > $revision );
}
}
Log("maxRevision discovered: $revision");
return $revision;
}
sub groupKernels {
my ( $kernels, $prefix, $suffix ) = @_;
my %groups;
$suffix = "" unless ( defined $suffix );
foreach my $kernel (@$kernels) {
next unless ( $kernel =~ /^\Q$prefix\E-(.+)_\d+\Q$suffix\E$/ );
push( @{ $groups{$1} }, $kernel );
}
return \%groups;
}
sub pruneVersions {
my ( $versions, $current, $keep ) = @_;
my $old_version;
Log("pruneVersions called with: $current, $keep");
Log($versions);
$keep = 0 unless ( defined $keep and $keep gt 0 );
# Keep revisions current version
purgeFiles( $versions->{$current}, $keep );
# Sort the versions and remove the current
my @old_versions = sort versioncmp keys %$versions;
my $index = 0;
foreach my $key (@old_versions) {
if ( $key eq $current ) {
splice( @old_versions, $index, 1 );
} else {
$index++;
}
}
# Purge all of the too-old revisions
while ( scalar @old_versions > $keep ) {
$old_version = shift @old_versions;
purgeFiles( $versions->{$old_version} );
}
# Purge all but the remaining revision of the leftover versions
foreach $old_version (@old_versions) {
purgeFiles( $versions->{$old_version}, 1 );
}
}
sub purgeFiles {
my ( $files, $keep ) = @_;
return unless ( defined $files );
$keep = 0 unless ( defined $keep and $keep gt 0 );
if ( $keep gt 0 ) {
my @sorted_files = sort versioncmp @$files;
while ( scalar @sorted_files > $keep ) {
my $file = shift @sorted_files;
verboseUnlink $file;
}
} else {
foreach my $file (@$files) {
verboseUnlink $file;
}
}
}
sub verboseUnlink {
my ( $file, $message ) = @_;
return unless ( -f $file );
# If a message is defined, display regardless of unlink success
if ( defined $message and ( $message ne "" ) ) {
print "$message\n";
}
if ( unlink $file ) {
# Print a default success message if none was defined
print "Removed file $file\n" unless ( defined $message );
} else {
print "ERROR: unable to remove $file: $!\n";
}
}
sub Log {
my $entry = shift;
return unless $runConf{debug};
chomp($entry);
unless ( ref($entry) ) {
print STDERR "## $entry\n";
} elsif ( ref $entry eq REFARRAY ) {
foreach my $line ( @{$entry} ) {
chomp $line;
print STDERR "## $line\n";
}
} else {
print STDERR Dumper($entry);
}
}
__END__
=for comment
KEEP IN SYNC WITH docs/man/generate-zbm.8.rst
=head1 NAME
B<generate-zbm> - ZFSBootMenu initramfs generator
=head1 SYNOPSIS
B<generate-zbm> [options]
=head1 OPTIONS
Where noted, command-line options supersede options in the B<generate-zbm>(5) configuration file.
=over 4
=item B<--version|-v> I<zbm-version>
Override the ZFSBootMenu version in output files; supersedes I<Global.Version>
=item B<--kernel|-k> I<kernel-path>
Manually specify a specific kernel; supersedes I<Kernel.Path>
=item B<--kver|-K> I<kernel-version>
Manually specify a specific kernel version; supersedes I<Kernel.Version>
=item B<--prefix|-p> I<image-prefix>
Manually specify the output image prefix; supersedes I<Kernel.Prefix>
=item B<--initcpio|-i>
Force the use of mkinitcpio instead of dracut.
=item B<--no-initcpio|-i>
Force the use of dracut instead of mkinitcpio.
=item B<--confd|-C> I<config-path>
Specify initramfs configuration path
=over 4
=item For dracut: supersedes I<Global.DracutConfDir>
=item For mkinitcpio: supersedes I<Global.InitCPIOConfig>
=back
=item B<--hookd|-H> I<hookd-path>
Specify mkinitcpio hook directory; supersedes I<Global.InitCPIOHookDirs>
May be specified more than once. Ignored when using dracut.
=item B<--cmdline|-l> I<options>
Override the kernel command line; supersedes I<Kernel.CommandLine>
=item B<--bootdir|-b> I<boot-path>
Specify the path to search for kernel files; default: I</boot>
=item B<--config|-c> I<conf-file>
Specify the path to a configuration file; default: I</etc/zfsbootmenu/config.yaml>
=item B<--enable>
Set the I<Global.ManageImages> option to true, enabling image generation.
=item B<--disable>
Set the I<Global.ManageImages> option to false, disabling image generation.
=item B<--debug|d>
Enable debug output
=item B<--showver|V>
Print ZFSBootMenu version and quit.
=back
=head1 SEE ALSO
B<generate-zbm>(5) B<zfsbootmenu>(7)
=head1 AUTHOR
ZFSBootMenu Team L<https://github.com/zbm-dev/zfsbootmenu>
=cut