#! /usr/bin/perl

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# package Tmp version 1.0
#
# Create temporary files/directories and ensures they are removed at
# program end.
#
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{
  package Tmp;

  use File::Temp;
  use strict 'vars';
  use Cwd 'abs_path';

  sub new
  {
    my $self = {};
    my $save_tmp = shift;

    bless $self;

    my $x = $0;
    $x =~ s#.*/##;
    $x =~ s/(\s+|"|\\|')/_/;
    $x = 'tmp' if$x eq "";

    my $t = File::Temp::tempdir(abs_path("/tmp") . "/$x.XXXXXXXX", CLEANUP => $save_tmp ? 0 : 1);

    $self->{base} = $t;

    if(!$save_tmp) {
      my $s_t = $SIG{TERM};
      $SIG{TERM} = sub { File::Temp::cleanup; &$s_t if $s_t };

      my $s_i = $SIG{INT};
      $SIG{INT} = sub { File::Temp::cleanup; &$s_i if $s_i };
    }

    return $self
  }

  sub dir
  {
    my $self = shift;
    my $dir = shift;
    my $t;

    if($dir ne "" && !-e("$self->{base}/$dir")) {
      $t = "$self->{base}/$dir";
      die "error: mktemp failed\n" unless mkdir $t, 0755;
    }
    else {
      chomp ($t = `mktemp -d $self->{base}/XXXX`);
      die "error: mktemp failed\n" if $?;
    }

    return $t;
  }

  sub file
  {
    my $self = shift;
    my $file = shift;
    my $t;

    if($file ne "" && !-e("$self->{base}/$file")) {
      $t = "$self->{base}/$file";
      open my $f, ">$t";
      close $f;
    }
    else {
      chomp ($t = `mktemp $self->{base}/XXXX`);
      die "error: mktemp failed\n" if $?;
    }

    return $t;
  }

  # helper function
  sub umount
  {
    my $mp = shift;

    if(open(my $f, "/proc/mounts")) {
      while(<$f>) {
        if((split)[1] eq $mp) {
          # print STDERR "umount $mp\n";
          ::susystem("umount $mp");
          return;
        }
      }
      close $f;
    }
  }

  sub mnt
  {
    my $self = shift;
    my $dir = shift;

    my $t = $self->dir($dir);

    if($t ne '') {
      eval 'END { local $?; umount $t }';

      my $s_t = $SIG{TERM};
      $SIG{TERM} = sub { umount $t; &$s_t if $s_t };

      my $s_i = $SIG{INT};
      $SIG{INT} = sub { umount $t; &$s_i if $s_i };
    }

    return $t;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
use strict;

use Getopt::Long;
use Digest::MD5;
use Digest::SHA;
use File::Find;
use File::Path;
use Cwd 'abs_path';
use JSON;

use Data::Dumper;
$Data::Dumper::Sortkeys = 1;
$Data::Dumper::Terse = 1;
$Data::Dumper::Indent = 1;

our $VERSION = "0.0";
our $LIBEXECDIR = "/usr/lib";

my @boot_archs = qw ( x86_64 i386 s390x s390 ia64 aarch64 ppc ppc64 ppc64le );
my $magic_id = "6803f54d-f1f0-4d84-8917-96f9c3c669ab";
my $magic_sig_id = "7984fc91-a43f-4e45-bf27-6d3aa08b24cf";

# valid kernel module extensions
my $kext_regexp = '\.ko(?:\.xz|\.gz|\.zst)?';
my $kext_glob = '.ko{,.xz,.gz,.zst}';
my @kext_list = qw ( .ko .ko.xz .ko.gz .ko.zst );

sub usage;
sub check_root;
sub show_progress;
sub susystem;
sub fname;
sub analyze_boot;
sub build_todo;
sub new_file;
sub copy_or_new_file;
sub copy_file;
sub prepare_mkisofs;
sub build_filelist;
sub update_filelist;
sub run_mkisofs;
sub read_sector;
sub write_sector;
sub fix_catalog;
sub relocate_catalog;
sub rerun_mkisofs;
sub run_isohybrid;
sub run_isozipl;
sub run_syslinux;
sub run_createrepo;
sub isols;
sub find_magic;
sub meta_iso;
sub meta_fat;
sub fat_data_start;
sub create_initrd;
sub add_instsys_rh;
sub add_instsys_suse;
sub add_instsys_classic;
sub add_instsys_live;
sub prepare_new_instsys_files;
sub new_ki_name;
sub get_kernel_initrd;
sub update_kernel_initrd;
sub get_initrd_format;
sub unpack_orig_initrd;
sub extract_installkeys;
sub create_cd_ikr;
sub merge_options;
sub isolinux_add_option;
sub grub2_add_option;
sub yaboot_add_option;
sub update_boot_options;
sub exclude_files;
sub prepare_normal;
sub prepare_micro;
sub prepare_nano;
sub prepare_pico;
sub set_mkisofs_metadata;
sub trim_volume_id;
sub add_to_content_file;
sub update_content_or_checksums;
sub update_content;
sub update_checksums;
sub update_treeinfo;
sub create_sign_key;
sub add_sign_key;
sub sign_content_or_checksums;
sub file_magic;
sub fs_type;
sub get_archive_type;
sub unpack_cpiox;
sub unpack_archive;
sub format_array;
sub get_initrd_modules;
sub build_module_list;
sub add_modules_to_initrd;
sub add_modules_to_instsys;
sub update_no_compression_settings;
sub replace_kernel_mods;
sub new_products_xml;
sub prepare_addon;
sub check_mksquashfs_comp;
sub check_tagmedia_signature_tag;
sub eval_size;
sub add_initrd_option;
sub wipe_iso;
sub get_media_style;
sub get_media_variant;
sub analyze_products;
sub check_product;
sub crypto_cleanup;
sub run_crypto_disk;
sub read_ini;
sub write_ini;
sub create_efi_image;
sub rebuild_efi_image;
sub read_config;
sub get_file_arch;
sub apply_duds_1;
sub apply_duds_2;
sub apply_single_dud;
sub kernel_module_exists;
sub run_depmod;

my %config;
my $sudo;
my $sudo_checked;
my $check_root_ok;
my $opt_create;
my $opt_save_temp;
my $opt_dst;
my $opt_joliet = 1;
my $opt_verbose = 0;
my $opt_efi = 1;
my $opt_hybrid;
my $opt_hybrid_fs;
my $opt_hybrid_gpt;
my $opt_hybrid_mbr;
my $opt_no_prot_mbr;
my $opt_no_mbr_code;
my $opt_no_mbr_chs;
my $opt_zipl;
my $opt_check;
my $opt_digest;
my @opt_initrds;
my @opt_duds;
my @opt_instsys;
my @opt_rescue;
my $opt_boot_options;
my $opt_type;
my $opt_vendor;
my $opt_preparer;
my $opt_application;
my $opt_volume;
my $opt_volume1;
my $opt_no_docs = 1;
my $opt_loader;
my $opt_sign = 1;
my $opt_sign_key;
my $opt_sign_key_id;
my $opt_sign_pass_file;
my $opt_sign_image;
my @opt_kernel_rpms;
my @opt_kernel_modules;
my $opt_arch;
my $opt_new_boot_entry;
my @opt_addon_packages;
my $opt_addon_name;
my $opt_addon_alias;
my $opt_addon_prio = 60;
my $opt_rebuild_initrd;
my $opt_size;
my $opt_net;
my $opt_instsys_url;
my $opt_defaultrepo;
my $opt_no_iso;
my $opt_merge_repos = 1;
my $opt_list_repos;
my $opt_include_repos;
my $opt_enable_repos;
my $opt_crypto;
my $opt_crypto_fs = 'ext4';
my $opt_crypto_password;
my $opt_crypto_title;
my $opt_crypto_top_dir;
my $opt_instsys_in_repo = 1;
my $opt_create_repo;
my $opt_signature_file;
my $opt_no_compression;
my $opt_instsys_size;
my $opt_mount_iso;
my $opt_hide_efi_image;
my $opt_grub_dir;
my $opt_grub_efi_dir;
my $opt_luks;
my $opt_initrd_options = [];

Getopt::Long::Configure("gnu_compat");

GetOptions(
  'create|c=s'       => sub { $opt_create = 1; $opt_dst = $_[1] },
  'create-repo'      => sub { $opt_create_repo = 1;},
  'joliet'           => \$opt_joliet,
  'no-joliet'        => sub { $opt_joliet = 0 },
  'efi'              => \$opt_efi,
  'no-efi'           => sub { $opt_efi = 0 },
  'uefi'             => \$opt_efi,
  'no-uefi'          => sub { $opt_efi = 0 },
  'efi-image'        => sub { $opt_hide_efi_image = 0 },
  'no-efi-image'     => \$opt_hide_efi_image,
  'uefi-image'       => sub { $opt_hide_efi_image = 0 },
  'no-uefi-image'    => \$opt_hide_efi_image,
  'check'            => \$opt_check,
  'no-check'         => sub { $opt_check = 0 },
  'digest=s'         => \$opt_digest,
  'no-digest'        => sub { $opt_digest = "" },
  'sign'             => \$opt_sign,
  'no-sign'          => sub { $opt_sign = 0 },
  'sign-image'       => \$opt_sign_image,
  'no-sign-image'    => sub { $opt_sign_image = 0 },
  'sign-key=s'       => \$opt_sign_key,
  'sign-key-id=s'    => \$opt_sign_key_id,
  'sign-pass-file=s' => \$opt_sign_pass_file,
  'signature-file=s' => \$opt_signature_file,
  'gpt'              => sub { $opt_hybrid = 1; $opt_hybrid_gpt = 1 },
  'mbr'              => sub { $opt_hybrid = 1; $opt_hybrid_mbr = 1 },
  'hybrid'           => \$opt_hybrid,
  'no-hybrid'        => sub { $opt_hybrid = 0 },
  'hybrid-fs=s'      => sub { $opt_hybrid = 1; $opt_hybrid_fs = $_[1] },
  'fat'              => sub { $opt_hybrid = 1; $opt_hybrid_fs = 'fat'; $opt_efi = 0; $opt_no_iso = 1 },
  'crypto'           => sub { $opt_crypto = 1; $opt_hybrid = 0; },
  'password=s'       => \$opt_crypto_password,
  'luks=s'           => \$opt_luks,
  'title=s'          => \$opt_crypto_title,
  'top-dir=s'        => \$opt_crypto_top_dir,
  'filesystem=s'     => \$opt_crypto_fs,
  'no-iso'           => \$opt_no_iso,
  'size=s'           => \$opt_size,
  'instsys-size|live-root-size=s' => \$opt_instsys_size,
  'protective-mbr'   => sub { $opt_no_prot_mbr = 0 },
  'no-protective-mbr' => \$opt_no_prot_mbr,
  'mbr-code'         => sub { $opt_no_mbr_code = 0 },
  'no-mbr-code'      => \$opt_no_mbr_code,
  'mbr-chs'          => sub { $opt_no_mbr_chs = 0 },
  'no-mbr-chs'       => \$opt_no_mbr_chs,
  'initrd=s'         => \@opt_initrds,
  'apply-dud=s'      => \@opt_duds,
  'instsys|live-root=s' => \@opt_instsys,
  'rescue=s'         => \@opt_rescue,
  'rebuild-initrd'   => sub { $opt_rebuild_initrd = 2 },
  'no-rebuild-initrd' => sub { $opt_rebuild_initrd = 0 },
  'boot=s'           => \$opt_boot_options,
  'grub2'            => sub { $opt_loader = "grub" },
  'isolinux'         => sub { $opt_loader = "isolinux" },
  'zipl'             => \$opt_zipl,
  'grub-dir=s'       => \$opt_grub_dir,
  'grub-efi-dir=s'   => \$opt_grub_efi_dir,
  'no-zipl'          => sub { $opt_zipl = 0 },
  'micro'            => sub { $opt_type = 'micro' },
  'nano'             => sub { $opt_type = 'nano' },
  'pico'             => sub { $opt_type = 'pico' },
  'net=s'            => \$opt_net,
  'instsys-url=s'    => \$opt_instsys_url,
  'defaultrepo=s'    => \$opt_defaultrepo,
  'instsys-in-repo!' => \$opt_instsys_in_repo,
  'volume=s'         => \$opt_volume,
  'volume1=s'        => \$opt_volume1,
  'vendor=s'         => \$opt_vendor,
  'preparer=s'       => \$opt_preparer,
  'application=s'    => \$opt_application,
  'no-docs'          => \$opt_no_docs,
  'keep-docs'        => sub { $opt_no_docs = 0 },
  'kernel=s{1,}'     => \@opt_kernel_rpms,
  'modules=s{1,}'    => \@opt_kernel_modules,
  'arch=s'           => \$opt_arch,
  'add-entry=s'      => \$opt_new_boot_entry,
  'addon=s{1,}'      => \@opt_addon_packages,
  'addon-name=s'     => \$opt_addon_name,
  'addon-alias=s'    => \$opt_addon_alias,
  'addon-prio=i'     => \$opt_addon_prio,
  'no-merge-repos'   => sub { $opt_merge_repos = 0 },
  'merge-repos'      => \$opt_merge_repos,
  'list-repos'       => \$opt_list_repos,
  'include-repos=s'  => \$opt_include_repos,
  'enable-repos=s'   => \$opt_enable_repos,
  'no-compression=s' => sub { @$opt_no_compression{split /,/, $_[1]} = ( 1 .. 8 ) },
  'mount-iso'        => \$opt_mount_iso,
  'no-mount-iso'     => sub { $opt_mount_iso = 0 },
  'initrd-config=s'  => $opt_initrd_options,
  'save-temp'        => \$opt_save_temp,
  'verbose|v'        => sub { $opt_verbose++ },
  'version'          => sub { print "$VERSION\n"; exit 0 },
  'help'             => sub { usage 0 },
) || usage 1;

usage 1 unless $opt_create || $opt_list_repos;
usage 1 if $opt_hybrid_fs !~ '^(|iso|fat)$';
usage 1 if defined($opt_digest) && $opt_digest !~ '^(|md5|sha1|sha224|sha256|sha384|sha512)$';
usage 1 if defined($opt_enable_repos) && $opt_enable_repos !~ /^(0|1|no|yes|auto|ask)$/i;

if(@opt_duds && @opt_kernel_rpms) {
  die "You cannot use options --apply-dud and --kernel simultaneously.\n";
}

for (keys %$opt_no_compression) {
  die "Option --no-compression only accepts firmware, modules, or squashfs\n" if !/^(firmware|modules|squashfs)$/;
}

if(@opt_kernel_rpms && ! defined $opt_rebuild_initrd) {
  $opt_rebuild_initrd = 1;
}

if(@opt_duds && !$opt_rebuild_initrd) {
  $opt_rebuild_initrd = 3;
}

if(@opt_kernel_modules && !@opt_kernel_rpms) {
  die "No kernel packages specified. Use --kernel together with --modules.\n";
}

die "no password\n" if $opt_crypto && $opt_crypto_password eq "";

$ENV{PATH} = "$LIBEXECDIR/mkmedia:/usr/bin:/bin:/usr/sbin:/sbin";

if($>) {
  if($opt_rebuild_initrd == 2) {
    die "mkmedia must be run with root permissions when --rebuild-initrd is used\n"
  }
  if($opt_rebuild_initrd == 1) {
    die "mkmedia must be run with root permissions to update kernel config.\nOr use --no-rebuild-initrd.\n"
  }
  if($opt_rebuild_initrd == 3) {
    # for live-root changes and full initrd rebuilds
    die "mkmedia must be run with root permissions to appy driver updates.\n"
  }
}

my $basic_config = "$ENV{HOME}/.mkmediarc";
$basic_config = "$ENV{HOME}/.mksuserc" unless -f $basic_config;

if(open my $f, $basic_config) {
  while(<$f>) {
    next if /^\s*#/;
    if(/^\s*(\S+?)\s*=\s*(.*?)\s*$/) {
      my $key = $1;
      my $val = $2;
      $val =~ s/^\"|\"$//g;
      $config{$key} = $val;
    }
  }
  close $f;
}

if($config{sudo}) {
  $sudo = $config{sudo};
  $sudo =~ s/\s*$/ /;
}

$opt_sign_key ||= $config{'sign-key'};
$opt_sign_key_id ||= $config{'sign-key-id'};

my $tmp = Tmp::new($opt_save_temp);

# my $tmp_mnt = $tmp->mnt('mnt');
my $tmp_new = $tmp->dir('new');
my $tmp_err = $tmp->file('err');
my $tmp_sort = $tmp->file('sort');
my $tmp_exclude = $tmp->file('exclude');
my $tmp_filelist = $tmp->file('filelist');
my $tmp_fat = $tmp->file('fat');

my @sources;
my $files;
my $files_to_keep;
my $boot;
my $todo;
my $iso_cnt = 0;
my $mkisofs = { command => '/usr/bin/mkisofs' };
my $iso_file;
my $iso_fh;
my $two_runs;
my $add_kernel;
my $add_initrd;
my $orig_initrd;
my $orig_initrd_filename;
my $initrd_has_parts;
my $has_efi = 0;
my $has_el_torito = 0;
my $sign_key_pub;
my $sign_key_dir;
my $sign_key_id;
my $initrd_installkeys;
my $initrd_format;
my $rebuild_initrd;
my $hybrid_part_type;
my $hybrid_mbr_code;	# mbr code to use for iso hybrid mode
my $hybrid_grub;	# grub instead of isolinux is used for iso hybrid boot
my $kernel;
my $warned;
my $read_write;
my $mksquashfs_has_comp;
my $tagmedia_has_signature_tag;
my $image_size;
my $syslinux_config;
my $initrd_options;
my $has_content;
my $product_db;
my $repomd_instsys_location;
my $sign_passwd_option;
my $media_style = 'suse';
my $media_variant = 'install';
my $detected_signature_file;
my $orig_initrd_00_lib;
my $instsys_size;
my $signature_file_used;
my $applied_duds;
my $depmod;

my $progress_start = 0;
my $progress_end = 100;
my $progress_txt = 'building:';

$mkisofs->{command} = "/usr/bin/genisoimage" if ! -x $mkisofs->{command};
die "mkisofs: command not found\n" if ! -x $mkisofs->{command};

$mksquashfs_has_comp = check_mksquashfs_comp;
$tagmedia_has_signature_tag = check_tagmedia_signature_tag;

if(defined $opt_size) {
  $image_size = eval_size $opt_size;
  die "$opt_size: invalid size\n" unless $image_size;
  printf "target image size: %.2f GiB ($image_size blocks)\n", $image_size / (1 << 21);
}

if(defined $opt_instsys_size) {
  $instsys_size = eval_size $opt_instsys_size;
  die "$opt_instsys_size: invalid size\n" unless defined $instsys_size;

  # add at least an empty directory if we are going to resize the live root file system
  if(!@opt_instsys) {
    push @opt_instsys, $tmp->dir();
  }
}

if($opt_sign_pass_file) {
  if ($opt_sign_key || $opt_sign_key_id) {
    if (-e $opt_sign_pass_file) {
      $sign_passwd_option = "--pinentry-mode loopback --passphrase-file $opt_sign_pass_file"
    } else {
      die "Passphrasefile $opt_sign_pass_file does not exist\n";
    }
  } else {
    print "--sign-pass-file ignored because of missing --sign-key or --sign-key-id\n"
  }
}

if($opt_create || $opt_list_repos) {
  $iso_file = $opt_dst;

  die "$iso_file: block device not allowed\n" if -b $iso_file;

  for (@ARGV) {
    s#/*$##;
    next if $_ eq "";
    if(-d) {
      if(`find $_ -xdev \\! -readable`) {
        die "Some files in $_ are not user-readable; you need root privileges.\n";
      }
      my $d_skel = (<$_/usr/lib/skelcd/*>)[0];
      my $d_tftp = (<$_/usr/share/tftpboot-installation/*>)[0];
      if(-d $d_skel ) {
        push @sources, { dir => $d_skel, real_name => $_, type => 'dir' };
      }
      elsif(-d $d_tftp ) {
        # copy only necessary files
        my $tmp_dir = $tmp->dir();
        system "cp -r $d_tftp/{EFI,boot} $tmp_dir";
        push @sources, { dir => $d_tftp, real_name => $_, type => 'dir' };
      }
      else {
        push @sources, { dir => $_, real_name => $_, type => 'dir' };
      }
    }
    elsif(-f _) {
      my $t = `file -b -k -L $_ 2>/dev/null`;
      if($t =~ /ISO 9660 CD-ROM/) {
        my $d;
        $opt_mount_iso = check_root if ! defined $opt_mount_iso;
        if($opt_mount_iso) {
          check_root "Sorry, can't access ISO images; you need root privileges.";
          print "mounting $_\n" if $opt_verbose >= 2;
          $d = $tmp->mnt(sprintf("mnt_%04d", $iso_cnt));
          susystem "mount -oro,loop $_ $d";
        }
        else {
          print "unpacking $_\n" if $opt_verbose >= 2;
          $d = $tmp->dir();
          system "cd $d && isoinfo -i '" . abs_path($_) . "' -RJX 2>/dev/null && chmod -R a=r,a=rX,u+w ."
            and die "$_: ISO unpacking failed\n";
        }
        $iso_cnt++;
        push @sources, { dir => $d, real_name => $_, type => 'iso' };
        if(`find $d -xdev \\! -readable`) {
          die "Some files in $_ are not user-readable; you need root privileges.\n";
        }
        # fixme: does not reliably work: with gpt+mbr, 'file' does not report a gpt
        if($iso_cnt == 1 && $t =~ /GPT partition table/) {
          if(!defined $opt_hybrid_gpt && !defined $opt_hybrid_mbr) {
            $opt_hybrid = 1;
            $opt_hybrid_gpt = 1;
          }
        }
      }
      elsif($t =~ /RPM /) {
        $iso_cnt++;
        my $d_rpm = $tmp->mnt(sprintf("mnt_%04d", $iso_cnt));
        system "rpm2cpio $_ | ( cd $d_rpm ; cpio --quiet -dmiu --no-absolute-filenames 2>/dev/null)";
        my $d_skel = (<$d_rpm/usr/lib/skelcd/*>)[0];
        my $d_tftp = (<$d_rpm/usr/share/tftpboot-installation/*>)[0];
        if(-d $d_skel ) {
          push @sources, { dir => $d_skel, real_name => $_, type => 'dir' };
        }
        elsif(-d $d_tftp ) {
          # remove unnecessary files
          unlink "$d_tftp/README";
          system "rm -r $d_tftp/net";
          push @sources, { dir => $d_tftp, real_name => $_, type => 'dir' };
        }
        else {
          push @sources, { dir => $d_rpm, real_name => $_, type => 'dir' };
        }
      }
      else {
        die "$_: unsupported source type\n";
      }
    }
    elsif(-e _) {
      die "$_: unsupported source type\n";
    }
    else {
      die "$_: no such file or directory\n";
    }
  }

  if(!@sources) {
    my $msg = "no sources - nothing to do\n";
    if(@opt_kernel_rpms || @opt_kernel_modules || @opt_addon_packages) {
      $msg .= "Maybe you forgot '--' after --kernel, --modules, or --addon?\n";
    }
    die $msg;
  }

  $media_style = get_media_style \@sources;
  $media_variant = get_media_variant \@sources;

  print "media style $media_style, variant $media_variant\n";

  # default hybrid settings
  if($media_style eq 'suse') {
    $opt_hybrid = 1 if !defined($opt_hybrid);
    $opt_hybrid_mbr = 1 if !defined($opt_hybrid_mbr) && !defined($opt_hybrid_gpt);
    $opt_hybrid_fs = 'iso' if !defined($opt_hybrid_fs);
  }
  else {
    # rh
    $opt_hybrid = 1 if !defined($opt_hybrid);
    $opt_hybrid_mbr = $opt_hybrid_gpt = 1 if !defined($opt_hybrid_mbr) && !defined($opt_hybrid_gpt);
    $opt_hybrid_fs = '' if !defined($opt_hybrid_fs);
  }

  # we might need two mkisofs runs...
  $two_runs = ($opt_hybrid && $opt_hybrid_fs) || $opt_crypto;

  analyze_products \@sources;
  build_filelist \@sources;
  $boot = analyze_boot;
  get_initrd_format;

  $applied_duds = apply_duds_1;

  if($opt_type eq "micro") {
    if($opt_defaultrepo) {
      $opt_instsys_in_repo = 0;
    }
    elsif($media_style eq 'suse') {
      $opt_create_repo = 1;
    }
  }

  # good indicator for openSUSE vs. SLE (one product per medium vs. many)
  my $num_products = @{$product_db->{list}};

  if(
    $media_style eq 'suse' &&
    $media_variant eq 'install' &&
    (($opt_type eq "micro" && $num_products <= 1) || $opt_type eq "nano") &&
    !$opt_defaultrepo
  ) {
    print "Warning: use --defaultrepo option to set a repository.\n";
  }

  # note: analyze_products may set $detected_signature_file
  if(!defined $opt_signature_file) {
    $opt_signature_file = $detected_signature_file;
    $opt_signature_file = ".signature" if !defined $opt_signature_file && $media_style eq 'suse';
  }
  $signature_file_used = defined $opt_signature_file;
  $opt_signature_file = "glumps" if $opt_signature_file eq "";
  my $x = copy_or_new_file $opt_signature_file;

  # delete competing signature file, if any
  if(defined $detected_signature_file && $detected_signature_file ne $opt_signature_file) {
    my $sf = fname $detected_signature_file;
    push @{$mkisofs->{exclude}}, $sf if $sf;
  }

  # assume repomd layout if 'content' file is missing
  $has_content = 1 if fname "content";
  if(!$has_content) {
    print "assuming repo-md sources\n";
    if(!$opt_instsys_url && !$opt_instsys_in_repo) {
      my $x = get_kernel_initrd;
      die "oops: no initrd?\n" unless $x;
      if($x->{initrd} =~ m#(boot/[^/]+)/#) {
        $repomd_instsys_location = "$1/root";
        # Note
        #   When encryption is in use we must not set the instsys location
        #   here. This would cause linuxrc to miss the instsys as the URL
        #   below does never point inside an encrypted volume.
        #   Instead, run_crypto_disk() handles this when writing 90_crypto.
        $opt_instsys_url = "disk:/$repomd_instsys_location" unless $opt_crypto;
      }
    }

    exclude_files [ "README", "net" ];
  }

  if($opt_instsys_url) {
    add_initrd_option "instsys", $opt_instsys_url;
  }

  if($opt_net && !$opt_defaultrepo) {
    $opt_defaultrepo = "cd:/,hd:/,$opt_net";
  }

  if($opt_defaultrepo) {
    add_initrd_option "defaultrepo", $opt_defaultrepo;
  }

  for my $i (@$opt_initrd_options) {
    if($i =~ /^([^=]+)=(.*)$/) {
      my $key = $1;
      my $value = $2;
      $value =~ s/^("|')(.*)\1$/$2/;
      add_initrd_option $key, $value;
    }
    else {
      add_initrd_option $i, undef;
    }
  }

  if($opt_sign && (
      # we are going to change '/content' resp. '/CHECKSUMS' in one way or another
      @opt_initrds || @opt_duds || @opt_instsys || @opt_rescue || @opt_kernel_rpms || $opt_boot_options ||
      $opt_new_boot_entry || $opt_include_repos || update_content_or_checksums || $opt_sign_image
    )
  ) {
    extract_installkeys;
    create_sign_key;
    add_sign_key;
  }
  if(@opt_kernel_rpms) {
    replace_kernel_mods;
  }

  apply_duds_2 $applied_duds;

  $add_initrd = create_initrd;
  update_kernel_initrd;
  update_boot_options;

  if($media_style eq 'rh') {
    add_instsys_rh;
  }
  else {
    add_instsys_suse;
  }

  prepare_normal;
  prepare_micro if $opt_type eq 'micro';
  prepare_nano if $opt_type eq 'nano';
  prepare_pico if $opt_type eq 'pico';

  if($opt_create_repo) {
    run_createrepo $sources[0]{dir};
  }

  prepare_addon;

  # FIXME: suse also has it...
  if($media_style eq 'rh' && $opt_type !~ /^(micro|nano|pico)$/) {
    update_treeinfo;
  }

  sign_content_or_checksums if update_content_or_checksums;
  $todo = build_todo;
  set_mkisofs_metadata;

  prepare_mkisofs;

  # print "sources = ", Dumper(\@sources);
  # print "mkisofs = ", Dumper($mkisofs);

  if($opt_verbose >= 2) {
    print "analyzed boot config:\n";
    print Dumper($boot);
    print "creating boot config:\n";
    print Dumper($todo);
  }

  if($opt_verbose >= 3) {
    print "mkisofs files:\n";
    print Dumper($mkisofs->{filelist});
    print "mkisofs sort list:\n";
    print Dumper($mkisofs->{sort});
    print "mkisofs excluded files:\n";
    print Dumper($mkisofs->{exclude});
    print "mkisofs graft points:\n";
    print Dumper($mkisofs->{grafts});
  }

  if($two_runs) {
    if($opt_hybrid_fs eq 'iso') {
      $progress_end = 50;
    }
    if($opt_hybrid_fs eq 'fat') {
      $progress_end = 33;
    }
  }

  run_mkisofs;

  if($two_runs) {
    if($opt_crypto) {
      $progress_start = 50;
      $progress_end = 100;
      run_crypto_disk;
      exit
    }

    rerun_mkisofs;
  }

  fix_catalog;
  relocate_catalog;

  if($opt_hybrid) {
    run_isohybrid;
    run_syslinux if $opt_hybrid_fs eq 'fat';
  }
  run_isozipl if $opt_zipl;

  wipe_iso if $opt_no_iso;

  if(!defined $opt_digest) {
    $opt_digest = $media_style eq 'rh' ? 'md5' : 'sha256';
  }

  if($opt_digest ne "") {
    my $chk = $opt_check ? "--check" : "";
    my $digest = $opt_digest;
    my $pad = "";
    my $style = $media_style;
    if($media_style eq 'suse') {
      $pad = "--pad 150";
    }
    else {
      if($opt_digest ne 'md5') {
        $style = 'suse';
        print "Warning: embedding $opt_digest digest in SUSE format.\n";
      }
    }
    print "calculating $digest...";
    my $tag_sig_opt = "";
    if($tagmedia_has_signature_tag) {
      $tag_sig_opt = "--signature-tag" if $opt_sign_image;
    }
    system "tagmedia --style $style $chk $pad $tag_sig_opt --digest '$digest' '$iso_file' >/dev/null";
    print "\n";
    if($opt_sign && $sign_key_dir && $opt_sign_image) {
      my $tmp_dir = $tmp->dir();
      # For rh media with signature outside iso data area, --digest will never add a SIGNATURE tag.
      # But --import-signature below will.
      # Simply repeat the process in this case, so the correct meta data block is signed.
      my $cnt = 1;
      $cnt++ if `tagmedia '$iso_file'` !~ /^signature\s*=\s*\d+/mi;
      while($cnt--) {
        system "tagmedia --export-tags $tmp_dir/tags $iso_file >/dev/null 2>&1";
        if(-s "$tmp_dir/tags") {
          print "signing $iso_file\n" if $opt_verbose >= 1 && $cnt == 0;
          system "gpg --homedir=$sign_key_dir --local-user '$sign_key_id' --batch --yes --armor --detach-sign $sign_passwd_option $tmp_dir/tags";
          system "tagmedia --import-signature $tmp_dir/tags.asc $iso_file";
        }
      }
    }
  }
}

# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# usage(exit_code)
#
# Print help text and exit with exit_code.
#
sub usage
{
  print <<"= = = = = = = =";
Usage: mkmedia [OPTIONS] [SOURCES]
Create and modify bootable media.

General options:

      --verbose                   Show more detailed messages. Can be repeated to log even more.
      --version                   Show mkmedia version.
      --save-temp                 Keep temporary files.
      --help                      Write this help text.

Show available repositories:

      --list-repos                List all available repositories in SOURCES.

Create new image:

  -c, --create FILE               Create ISO or disk image FILE from SOURCES.

Media type related options:

      --micro                     Create image with just enough files to run the installer.
      --nano                      Create image with just enough files for a network based installation.
      --pico                      Even less than --nano, keep just the boot loader (for testing).

Media integrity related options:

      --check                     Tag ISO to be verified before starting the installation.
      --no-check                  Don't tag ISO (default).
      --digest DIGEST             Embed DIGEST to verify ISO integrity (default: sha256).
      --no-digest                 Don't embed any digest to verify ISO integrity.
      --sign-image                Embed signature for entire image.
      --no-sign-image             Don't embed signature for entire image. (default)
      --signature-file FILE       Store embedded signature in FILE (default: /.signature for SUSE-style media,
                                  no signature file for RH-style media).
      --sign                      Re-sign '/CHECKSUMS' if it has changed (default).
      --no-sign                   Don't re-sign '/CHECKSUMS'.
      --sign-key KEY_FILE         Use this key file instead of generating a transient key.
      --sign-key-id KEY_ID        Use this key id instead of generating a transient key.
      --sign-pass-file            Use the password stored in this file to open the key.

Initrd/instsys update related options:

      --apply-dud DUD             Apply driver update DUD (can be repeated).
      --initrd DIR|RPM|DUD        Add content of DIR, RPM, or DUD to initrd (can be repeated).
      --rebuild-initrd            Rebuild the entire initrd instead of appending changes.
      --no-rebuild-initrd         Append changes to the initrd instead of rebuilding.
      --initrd-config KEY=VALUE   Add config option to initrd intended for linuxrc/YaST/dracut/Agama
                                  (can be repeated).
      --instsys DIR|RPM           Add content of DIR or RPM to installation system or root file
                                  system for Live media (can be repeated).
      --live-root DIR|RPM         Alias for --instsys.
      --rescue DIR|RPM            Add content of DIR or RPM to rescue system (can be repeated).
      --instsys-size SIZE_SPEC    Resize Live root file system.
      --live-root-size SIZE_SPEC  Alias for --instsys-size.
      --no-docs                   Don't include package doc files (default).
      --keep-docs                 Include package doc files.

Kernel/module update related options:

      --kernel RPM_LIST           Replace kernel, kernel modules, and kernel firmware used for booting.
                                  RPM_LIST is a list of kernel or firmware packages.
                                  Note: this option expects a variable number of arguments.
                                  Note also: implies --rebuild-initrd since mkmedia 3.0.
      --modules MODULE_LIST       A list of modules to be added to the initrd.
                                  To be used together with --kernel.
                                  Note: this option expects a variable number of arguments.
      --no-compression LIST       A comma-separated list of: firmware, modules, squashfs.

Add-on related options:

      --addon RPM_LIST            A list of RPMs that should be made available as add-on.
      --addon-name NAME           Use NAME as the add-on name.
      --addon-alias ALIAS         Set repo alias to ALIAS.
      --addon-prio NUM            Set add-on repository priority to NUM (default: 60).

ISO file system related options:

      --joliet                    Use Joliet extensions (default).
      --no-joliet                 Don't use Joliet extensions.
      --volume VOLUME_ID          Set ISO volume id to VOLUME_ID.
      --vendor VENDOR_ID          Set ISO publisher id to VENDOR_ID.
      --preparer PREPARER_ID      Set ISO data preparer id to PREPARER_ID.
      --application APP_ID        Set ISO application id to APP_ID.
      --volume1 VOLUME_ID         Specify ISO volume id of the entire image - in case it should differ
                                  from the ISO volume id used for the partition.

General image layout related options:

      --uefi                      Make ISO UEFI bootable (default).
      --no-uefi                   Don't make ISO UEFI bootable.
      --uefi-image                Make UEFI boot image visible in ISO9660 file system (default if it exists).
      --no-uefi-image             Hide UEFI boot image in ISO9660 file system (default if it does not exist).
      --zipl                      Make image zIPL bootable (default on s390x).
      --no-zipl                   Don't make image zIPL bootable (default if not on s390x).
      --gpt                       Add GPT when in isohybrid mode.
      --mbr                       Add MBR when in isohybrid mode (default).
      --prot-mbr                  When writing a GPT, write a protective MBR (default).
      --no-prot-mbr               When writing a GPT, don't write a protective MBR.
      --mbr-code                  Include x86 MBR boot code (default).
      --no-mbr-code               Don't include x86 MBR boot code.
      --mbr-chs                   Fill in sensible CHS values in MBR partition table (default).
      --no-mbr-chs                Use 0xffffff instead of CHS values in MBR partition table.
      --no-iso                    Don't make image accessible as ISO9660 file system.
      --hybrid                    Create an image which is both an ISO and a disk (default).
      --no-hybrid                 Create a regular ISO image without extra gimmicks.
      --hybrid-fs FS              Use file system FS for the disk partition created in hybrid mode.
      --fat                       Create an image that's suitable to be put on a USB disk.
      --size SIZE_SPEC            The intended size of the disk image when using a FAT file.

Media repository related options:

      --merge-repos               Create a common media.1/products file (default).
      --no-merge-repos            Skip the special treatment of repositories and just merge all SOURCES.
      --include-repos LIST        Comma-separated list of repository names to include in the final image.
      --enable-repos WHEN         Whether to enable repos. WHEN can be 'auto', 'yes', 'ask', or 'no' (default: no).
      --create-repo               Re-create and sign the repository (default: don't).

Repository location related options:

      --net URL                   Use URL as default network repository.
      --instsys-url URL           Load the installation system from the specified URL.
      --instsys-in-repo           Load installation system from repository (default).
      --no-instsys-in-repo        Do not load installation system from repository but from local disks.
      --defaultrepo URL_LIST      URL_LIST is a list of comma (',') separated URLs the installer will try in turn.

Boot menu related options:

      --boot OPTIONS              Add OPTIONS to default boot options.
      --add-entry BOOT_ENTRY      Create a new boot entry with name BOOT_ENTRY.

Image encryption related options:

      --crypto                    If set, an encrypted disk image is created.
      --password PASSWORD         Use PASSWORD for encrypting the disk image.
      --luks OPTIONS              Pass OPTIONS to 'cryptsetup luksFormat'.
      --title TITLE               The password query screen uses TITLE as title (default: openSUSE).
      --top-dir DIR               The installation files are placed into subdir DIR.
      --filesystem FS             Use file system FS for the encrypted image (default: ext4).

Debug options:
      --mount-iso                 Mount ISO images to access them (default if run as root).
      --no-mount-iso              Unpack ISO images to access them (default if run as normal user).

More information is available in the mkmedia(1) manual page.
= = = = = = = =

  exit shift;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# check_root(msg)
#
# Checks if we can get root privileges if required.
#
# - msg: message to show to user if things fail
#        if msg is not set, return status whether running as root is possible
#
sub check_root
{
  my $p;
  my $msg = shift;

  if(!$sudo_checked) {
    $sudo_checked = 1;

    $check_root_ok = 0;
    if(!$>) {
      undef $sudo;
      $check_root_ok = 1;
    }
    else {
      my $p;
      chomp($p = `bash -c 'type -p $sudo'`) if $sudo;
      $check_root_ok = 1 if $p ne "";
    }
  }

  die "$msg\n" if !$check_root_ok && $msg;

  return $check_root_ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# susystem(cmd)
#
# Run command with root privileges.
#
# - cmd: command to run
#
sub susystem
{
  system $sudo . $_[0];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# show_progress(percent)
#
# Helper function to update progress indicator.
#
# - percent: percentage to show
#
sub show_progress
{
  my $p = shift;

  return if $progress_end - $progress_start < 1;

  $p = 0 if $p < 0;
  $p = 100 if $p > 100;

  $p = ($progress_end - $progress_start) / 100.0 * $p + $progress_start;

  printf "\r$progress_txt %3d%%", $p;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fname(name)
#
# Get full file name.
#
# - name: file name
#
# Returns full file name including path.
#
# We keep track of files and their locations as they can come from different
# sources (directories). For mkisofs it's necessary to ensure file names are
# unique.
#
# The function returns the current instance of the file.
#
sub fname
{
  return undef if !defined $_[0];

  if(exists $files->{$_[0]}) {
    return "$files->{$_[0]}/$_[0]";
  }
  else {
    return undef;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# build_filelist(sources)
#
# sources is an array_ref containing a list of directories to be scanned and
# added to our internal file list.
#
# The global vars $files and $mkisofs->{exclude} are updated.
#
# The point here is that mkisofs refuses to resolve name conflicts (when
# merging several sources). So we have to do this ourselves and track
# obsolete (that is, when duplicates show up) files in $mkisofs->{exclude}.
#
# That's only needed for regular files; directories don't matter.
#
sub build_filelist
{
  my $src = $_[0];

  # Internally generated files are put into the $tmp_new base directory.
  # $files holds a database of all files and their locations (their base
  # directories).
  #
  # The aim is that files that come later in the $src list replace earlier
  # versions. With $tmp_new taking even more precedence.
  #
  # At the start of this function $files has already been set up with the
  # files in $tmp_new.
  #
  # So, go through $src in reverse, put new files into $files and exclude
  # duplicates.
  #
  # This does only apply to files, not directories.
  #
  for my $s (reverse @$src) {
    File::Find::find({
      wanted => sub {
        if(m#^$s->{dir}/(.+)#) {
          my $file_name = $1;
          if($files->{$file_name}) {
            if(-f "$s->{dir}/$file_name") {
              push @{$mkisofs->{exclude}}, "$s->{dir}/$file_name";
            }
          }
          else {
            $files->{$file_name} = $s->{dir};
          }
        }
      },
      no_chdir => 1
    }, $s->{dir});
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_filelist(sources)
#
# Like build_filelist() but new files will replace existing ones.
#
sub update_filelist
{
  my $src = $_[0];

  for my $s (@$src) {
    File::Find::find({
      wanted => sub {
        if(m#^$s->{dir}/(.+)#) {
          my $file_name = $1;
          if($files->{$file_name}) {
            if(-f "$files->{$file_name}/$file_name") {
              push @{$mkisofs->{exclude}}, "$files->{$file_name}/$file_name";
            }
          }
          $files->{$file_name} = $s->{dir};
        }
      },
      no_chdir => 1
    }, $s->{dir});
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# analyze_boot()
#
# Scan sources and determne boot configuration. The result is put into the
# global $boot var.
#
sub analyze_boot
{
  my $boot;

  for (@boot_archs) {
    if(-d fname("boot/$_")) {
      $boot->{$_} = { base => "boot/$_" };

      $boot->{$_}{initrd} = "boot/$_/loader/initrd" if -f fname("boot/$_/loader/initrd");
      $boot->{$_}{initrd} = "boot/$_/isolinux/initrd" if -f fname("boot/$_/isolinux/initrd");
      $boot->{$_}{initrd} = "boot/$_/initrd" if -f fname("boot/$_/initrd");

      $boot->{$_}{kernel} = "boot/$_/loader/linux" if -f fname("boot/$_/loader/linux");
      $boot->{$_}{kernel} = "boot/$_/isolinux/linux" if -f fname("boot/$_/isolinux/linux");
      $boot->{$_}{kernel} = "boot/$_/vmrdr.ikr" if -f fname("boot/$_/vmrdr.ikr");
      $boot->{$_}{kernel} = "boot/$_/linux" if -f fname("boot/$_/linux");

      if(-f fname("boot/$_/loader/isolinux.bin") && -f fname("boot/$_/loader/isolinux.cfg")) {
        $boot->{$_}{bl}{isolinux} = { base => "boot/$_/loader", file => "isolinux.bin", arch => $_ };
      }
      if(-f fname("boot/$_/isolinux/isolinux.bin") && -f fname("boot/$_/isolinux/isolinux.cfg")) {
        $boot->{$_}{bl}{isolinux} = { base => "boot/$_/isolinux", file => "isolinux.bin", arch => $_ };
      }
      if(-f fname("boot/$_/cd.ikr")) {
        $boot->{$_}{bl}{ikr} = { base => "boot/$_/cd.ikr", arch => $_ };
        if(-f fname("boot/$_/suse.ins")) {
          $boot->{$_}{bl}{ikr}{ins} = "boot/$_/suse.ins";
        }
      }
      if(-f fname("boot/$_/grub2-efi/cd.img")) {
        $boot->{$_}{bl}{grub2} = { base => "boot/$_/grub2-efi", file => "cd.img", arch => $_ };
      }
      if(-f fname("boot/$_/grub2/cd.img")) {
        $boot->{$_}{bl}{grub2} = { base => "boot/$_/grub2", file => "cd.img", arch => $_ };
      }
      if(-f fname("boot/$_/loader/eltorito.img")) {
        $boot->{$_}{bl}{grub2} = { base => "boot/$_/loader", file => "eltorito.img", config => "boot/grub2/grub.cfg", arch => $_ };
        $boot->{$_}{bl}{efi} = { base => "boot/$_/loader/efiboot.img", arch => $_ };
        if(-f fname("boot/$_/loader/boot_hybrid.img")) {
          $hybrid_mbr_code = fname("boot/$_/loader/boot_hybrid.img");
          $hybrid_grub = 1;
        }
      }
      if(-f fname("boot/$_/grub2-ieee1275/core.elf")) {
        $boot->{$_}{bl}{grub2} = { base => "boot/$_/grub2-ieee1275", file => "core.elf", arch => $_ };
      }
      if(-f fname("boot/$_/efi")) {
        $boot->{$_}{bl}{efi} = { base => "boot/$_/efi", arch => $_ };
      }
      if(-f fname("ppc/bootinfo.txt")) {
        $boot->{$_}{bl}{chrp} = { base => "ppc", arch => $_ };
      }
    }

    if(-f fname("suseboot/linux64")) {
      $boot->{ppc64} = { base => "suseboot", arch => "ppc64", kernel => "suseboot/linux64"};
      $boot->{ppc64}{initrd} = "suseboot/initrd64" if -f fname("suseboot/initrd64");
      if(-f fname("ppc/bootinfo.txt")) {
        $boot->{ppc64}{bl}{yaboot} = { base => "suseboot", file => "suseboot/yaboot.ibm", arch => "ppc64" };
        $boot->{ppc64}{bl}{chrp} = { base => "ppc", arch => "ppc64" };
      }
    }
  }

  if (-d fname("isolinux") && -f fname("isolinux/isolinux.bin") &&  -f fname("isolinux/isolinux.cfg")) {
    $_ = "x86_64";
    $boot->{$_} = { base => "isolinux" };
    $boot->{$_}{initrd} = "isolinux/initrd.img" if -f fname("isolinux/initrd.img");
    $boot->{$_}{kernel} = "isolinux/vmlinuz" if -f fname("isolinux/vmlinuz");

    # copy of original initrd; keep in sync on final image
    $boot->{$_}{initrd_alt} = "images/pxeboot/initrd.img" if -f fname("images/pxeboot/initrd.img");
    $boot->{$_}{kernel_alt} = "images/pxeboot/vmlinuz" if -f fname("images/pxeboot/vmlinuz");

    $boot->{$_}{bl}{isolinux} = { base => "isolinux", file => "isolinux.bin", arch => $_ };
    if(-f fname("images/efiboot.img")) {
      $boot->{$_}{bl}{efi} = { base => "images/efiboot.img", arch => $_ };
    }
  }

  if(-f fname("images/pxeboot/initrd.img") && -d fname("boot/grub2/i386-pc") && -f fname("images/eltorito.img")) {
    $_ = "x86_64";
    $boot->{$_} = { base => "images" };
    $boot->{$_}{bl}{grub2} = { base => "images", file => "eltorito.img", config => "boot/grub2/grub.cfg", arch => $_ };
    if(-f fname("boot/grub2/i386-pc/boot_hybrid.img")) {
      $hybrid_mbr_code = fname("boot/grub2/i386-pc/boot_hybrid.img");
      $hybrid_grub = 1;
    }
    $boot->{$_}{bl}{efi} = { base => "images/efiboot.img", arch => $_ };
    $boot->{$_}{initrd} = "images/pxeboot/initrd.img" if -f fname("images/pxeboot/initrd.img");
    $boot->{$_}{kernel} = "images/pxeboot/vmlinuz" if -f fname("images/pxeboot/vmlinuz");
  }

  # sanitize; kiwi creates stray directories
  for (keys %$boot) {
    delete $boot->{$_} unless $boot->{$_}{kernel} && $boot->{$_}{initrd};
  }

  if(-d fname("EFI/BOOT")) {
    $boot->{efi} = { base => "EFI/BOOT" };

    # Ensure there will be a UEFI boot image if it is currently missing,
    # guessing its name if necessary.
    #
    # This is for media where the UEFI image is 'hidden' in the file system and
    # cannot be accessed.
    #
    # The image will be auto-generated based on the content of the 'EFI' directory.

    for my $arch (@boot_archs) {
      next unless $boot->{$arch};
      next if $boot->{$arch}{bl} && $boot->{$arch}{bl}{efi};
      next unless $arch eq 'x86_64' || $arch eq 'i386' || $arch eq 'aarch64';
      next unless -d fname("boot/$arch");
      if($media_variant ne 'install') {
        next unless -d fname("boot/$arch/loader");
        $boot->{$arch}{bl}{efi} = { base => "boot/$arch/loader/efiboot.img", arch => $arch };
        if(-f fname("boot/$arch/loader/boot_hybrid.img")) {
          $hybrid_mbr_code = fname("boot/$arch/loader/boot_hybrid.img");
          $hybrid_grub = 1;
        }
      }
      else {
        $boot->{$arch}{bl}{efi} = { base => "boot/$arch/efi", arch => $arch };
      }
    }
  }

  return $boot;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# todo = build_todo()
#
# Build list of boot configurations the new image should have and return it.
#
# This list is later used contructing the mkisofs/isohybrid calls.
#
sub build_todo
{
  my $todo;
  my @legacy_eltorito;

  # legacy El-Torito x86 boot
  # In theory more than one entry could be created, but BIOSes don't really
  # expect that...
  for (sort keys %$boot) {
    if($boot->{$_}{bl} && $boot->{$_}{bl}{isolinux} && (!$opt_loader || $opt_loader eq "isolinux")) {
      push @legacy_eltorito, { eltorito => $boot->{$_}{bl}{isolinux} };
    }
    if(
      $boot->{$_}{bl} &&
      $boot->{$_}{bl}{grub2} &&
      !$boot->{$_}{bl}{chrp} &&
      (!$opt_loader || $opt_loader eq "grub")
    ) {
      push @legacy_eltorito, { eltorito => $boot->{$_}{bl}{grub2} };
    }
  }

  # ... so we just pick one.
  if(@legacy_eltorito) {
    my $x = $legacy_eltorito[0]{eltorito}{base};
    push @$todo, $legacy_eltorito[0];
    if(@legacy_eltorito > 1) {
      print "more than one El Torito legacy boot entry detected, choosing /$x\n";
    }
  }

  # standard UEFI boot
  for (sort keys %$boot) {
    if($boot->{$_}{bl} && $boot->{$_}{bl}{efi}) {
      push @$todo, { efi => $boot->{$_}{bl}{efi} };
    }
  }

  # s390 also uses el-torito
  for (sort keys %$boot) {
    if($_ eq 's390x') {
      $opt_no_mbr_code = 1 if !defined $opt_no_mbr_code;
      $opt_zipl = 1 if !defined $opt_zipl;
      if($opt_boot_options) {
        my $f = copy_or_new_file "boot/s390x/parmfile";
        my $boot_opts;
        if(open my $fd, $f) {
          local $/;
          $boot_opts = <$fd>;
          close $fd;
        }
        $boot_opts =~ s/\s*$//;
        $opt_boot_options = merge_options $boot_opts, $opt_boot_options;
        if(open my $fd, ">", $f) {
          print $fd "$opt_boot_options\n";
          close $fd;
        }
      }
      if($opt_zipl) {
        if(!fname("boot/s390x/zipl.map")) {
          # add zipl map file, if necessary
          mkdir "$tmp_new/boot", 0755;
          mkdir "$tmp_new/boot/s390x", 0755;
          if(open my $f, ">$tmp_new/boot/s390x/zipl.map") {
            syswrite $f, ("\x00" x 0x4000);	# 16k should be enough
            close $f;
          }
        }
        print "zIPL bootable (s390x)\n";
      }
    }
    if($boot->{$_}{bl} && $boot->{$_}{bl}{ikr}) {
      push @$todo, { ikr => $boot->{$_}{bl}{ikr} };
    }
  }

  # chrp: just ensure we get a proper partition table
  for (sort keys %$boot) {
    if($boot->{$_}{bl} && $boot->{$_}{bl}{chrp}) {
      print "CHRP bootable ($_)\n";
      $hybrid_part_type = 0x96;
      $opt_hybrid = 1;
      $opt_hybrid_fs = "";
      $opt_no_mbr_chs = 1 if !defined $opt_no_mbr_chs;
      $opt_no_mbr_code = 1 if !defined $opt_no_mbr_code;
      $two_runs = 0;
      $mkisofs->{options} .= " -U";	# untranslated filenames for ppc firmware
    }
  }

  return $todo;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# new_file(fname)
#
# Create a new empty file with name fname.
#
# Return full path to fname.
#
sub new_file
{
  my $fname = $_[0];
  my $new_path = "$tmp_new/$fname";

  if($fname =~ m#(.+)/([^/]+)#) {
    system "mkdir -p '$tmp_new/$1'";
  }

  if(open my $x, ">$new_path") { close $x }

  # update file location database
  $files->{$fname} = $tmp_new;

  return $new_path;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# copy_or_new_file(fname)
#
# Create a writable copy of fname or a new empty file if fname does not exist.
#
# Return full path to fname.
#
sub copy_or_new_file
{
  return copy_file($_[0]) || new_file($_[0]);
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# copy_file(fname)
#
# Create a writable copy of fname.
#
# Return full path to fname or undef if it does not exist.
#
sub copy_file
{
  my $f = fname($_[0]);
  my $n;

  return undef unless defined $f;

  # we may already have a copy...
  if($f eq "$tmp_new/$_[0]") {
    return $f;
  }

  if(-d $f) {
    $n = "$tmp_new/$_[0]";
    system "mkdir -p '$n'";
  }
  elsif(-f $f) {
    if($_[0] =~ m#(.+)/([^/]+)#) {
      $n = "$tmp_new/$1/$2";
      system "mkdir -p '$tmp_new/$1'; cp '$f' '$tmp_new/$1'";
    }
    elsif($_[0] !~ m#/#) {
      $n = "$tmp_new/$_[0]";
      system "cp '$f' '$tmp_new'";
    }

    push @{$mkisofs->{exclude}}, $f;
    system "chmod u+w '$tmp_new/$_[0]'";
  }

  # update file location database
  if(defined $n) {
    my $x = $n;
    if($x =~ s#/$_[0]$##) {
      $files->{$_[0]} = $x;
      # print "$_[0] -> $x\n";
    }
  }

  return $n;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_mkisofs()
#
# Gather information needed to build the mkisofs command line.
#
# The result is stored in the global $mkisofs var.
#
# This uses the todo list from build_todo() to setup the boot config.
#
sub prepare_mkisofs
{
  my $iso_catalog;

  # general options
  $mkisofs->{options} .= " -duplicates-once -l -r -f -pad -input-charset utf8 -o '$iso_file'";
  $mkisofs->{options} .= " -V '" . substr($opt_volume, 0, 32) . "'";
  $mkisofs->{options} .= " -A '" . substr($opt_application, 0, 128) . "'";
  $mkisofs->{options} .= " -p '" . substr($opt_preparer, 0, 128) . "'";
  $mkisofs->{options} .= " -publisher '" . substr($opt_vendor, 0, 128) . "'";
  $mkisofs->{options} .= " -J -joliet-long" if $opt_joliet;

  # special loader options
  for (@$todo) {
    my $t = (keys %$_)[0];

    if($t eq 'eltorito') {
      $has_el_torito = 1;
      copy_file "$_->{$t}{base}/$_->{$t}{file}";
      push @{$mkisofs->{sort}}, "$tmp_new/$_->{$t}{base}/boot.catalog 4";
      # push @{$mkisofs->{sort}}, fname("$_->{$t}{base}/$_->{$t}{file}") . " 3";
      push @{$mkisofs->{sort}}, "$tmp_new/$_->{$t}{base}/$_->{$t}{file} 3";
      # push @{$mkisofs->{sort}}, "$_->{$t}{base}/$_->{$t}{file} 3";
      push @{$mkisofs->{sort}}, "$tmp_new/$_->{$t}{base} 1";
      $mkisofs->{options} .=
        " -no-emul-boot -boot-load-size 4 -boot-info-table" .
        " -b $_->{$t}{base}/$_->{$t}{file} -c $_->{$t}{base}/boot.catalog" .
        " -hide $_->{$t}{base}/boot.catalog";
      $mkisofs->{options} .=
        " -hide-joliet $_->{$t}{base}/boot.catalog" if $opt_joliet;
      print "El-Torito legacy bootable ($_->{$t}{arch})\n";
      push @$iso_catalog, "Legacy ($_->{$t}{arch})";
    }
    elsif($opt_efi && $t eq 'efi') {
      $has_efi = 1;
      my $efi_file = $_->{$t}{base};
      my $f = fname($efi_file);

      my $hide = $opt_hide_efi_image;
      $hide = !defined($f) if !defined($hide);

      printf "UEFI image: %s%s\n", $efi_file, $hide ? " (hidden)" : "";

      if(!$f || ! -s $f || rebuild_efi_image($efi_file)) {
        create_efi_image $_->{$t}{base};
        $f = fname($_->{$t}{base});
      }
      my $s = -s $f;
      $s = (($s + 2047) >> 11) << 2;
      $s = 1 if $s == 0 || $s > 0xffff;
      push @{$mkisofs->{sort}}, "$f 1000001";
      $mkisofs->{options} .=
        " -eltorito-alt-boot -no-emul-boot -boot-load-size $s -b $_->{$t}{base}";
      if($hide) {
        # First line is not sufficient - why?
        $mkisofs->{options} .= " -hide $efi_file -hide-joliet $efi_file";
        $mkisofs->{options} .= " -hide $f -hide-joliet $f";
      }
      print "El-Torito UEFI bootable ($_->{$t}{arch})\n";
      push @$iso_catalog, "UEFI ($_->{$t}{arch})";
      $mkisofs->{fix_catalog} = $iso_catalog;
    }
    elsif($t eq 'ikr') {
      if($_->{$t}{ins}) {
        # need to create base
        create_cd_ikr($_->{$t}{base}, $_->{$t}{ins});
      }
      $mkisofs->{options} .=
        " -eltorito-alt-boot -no-emul-boot -boot-load-size 1 -b $_->{$t}{base}";
      print "El-Torito legacy bootable ($_->{$t}{arch})\n";
      push @$iso_catalog, "Legacy ($_->{$t}{arch})";
      $mkisofs->{fix_catalog} = $iso_catalog;
    }
  }

  my $sf = copy_or_new_file "glump";

  if(open my $fh, ">", $sf) {
    print $fh $magic_id;
    close $fh;
  }

  push @{$mkisofs->{sort}}, "$sf 1000000";

  my $sf = fname $opt_signature_file;

  if($signature_file_used) {
    print "signature file used\n" if $opt_verbose >= 1;
    if(open my $fh, ">", $sf) {
      print $fh $magic_sig_id, "\x00" x (0x800 - length $magic_sig_id);
      close $fh;
    }
  }
  else {
    print "signature file not used\n" if $opt_verbose >= 1;
    push @{$mkisofs->{exclude}}, $sf if $sf;
  }

  push @{$mkisofs->{sort}}, "$sf 999999";

  # hide name if it is "glump" or 'glumps'
  $mkisofs->{options} .= " -hide glump -hide glumps";
  $mkisofs->{options} .= " -hide-joliet glump -hide-joliet glumps" if $opt_joliet;

  if($mkisofs->{sort}) {
    $mkisofs->{options} .= " -sort '$tmp_sort'";
  }

  if($mkisofs->{exclude}) {
    $mkisofs->{options} .= " -exclude-list '$tmp_exclude'";
  }

  # pass source locations via separate file to mkmedia, not as command line options
  $mkisofs->{filelist} = [ (map { $_->{dir} } grep { !$_->{skip} } @sources), $tmp_new ];

  # add relocated directory trees (graft points in mkisofs speak)
  push @{$mkisofs->{filelist}}, @{$mkisofs->{grafts}} if $mkisofs->{grafts};

  $mkisofs->{options} .= " -graft-points -path-list '$tmp_filelist'";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_mkisofs()
#
# Build actual mkisofs command line and run it.
#
sub run_mkisofs
{
  my $log;
  my $ok;
  my $cmd;

  # create sort file
  if($mkisofs->{sort}) {
    if(open my $fh, ">$tmp_sort") {
      print $fh "$_\n" for @{$mkisofs->{sort}};
      close $fh;
    }
  }

  # create exclude file
  if($mkisofs->{exclude}) {
    if(open my $fh, ">$tmp_exclude") {
      print $fh "$_\n" for @{$mkisofs->{exclude}};
      close $fh;
    }
  }

  # create file with file list
  if($mkisofs->{filelist}) {
    if(open my $fh, ">$tmp_filelist") {
      print $fh "$_\n" for @{$mkisofs->{filelist}};
      close $fh;
    }
  }

  $cmd = "$mkisofs->{command}$mkisofs->{options}";

  print "running:\n$cmd\n" if $opt_verbose >= 2;

  # seems to be necessary, else some changes are lost...
  system "sync";

  if(open my $fh, "$cmd 2>&1 |") {
    $| = 1;
    $ok = 1;	# sometimes mkisofs doesn't show any progress, so set ok here...
    while(<$fh>) {
      if(/^\s*(\d*\.\d)\d%/) {
        $ok = 1;
        show_progress $1;
      }
      else {
        $log .= $_;
      }
    }
    show_progress 100 if $ok;
    print "\n" if $progress_end == 100;
    close $fh;
    # printf STDERR "ret = $?\n";
    $ok = 0 if $?;
  }

  $ok = 0 if $log =~ /mkisofs: Permission denied/;

  my $joliet_msg;
  if($log =~ /mkisofs: Cannot use Joliet/) {
    $joliet_msg =
      "\n" .
      "Joliet file names are limited to 103 chars.\n" .
      "Some file names were longer.\n\n" .
      "Run mkmedia with the --no-joliet option to avoid this error.\n";
  }

  print $log if $opt_verbose >= 3 || (!$ok && (!$joliet_msg || $opt_verbose >= 1));

  die "Error: $mkisofs->{command} failed\n" . $joliet_msg if !$ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# buf = read_sector(nr)
#
# Read 2k sector from iso image.
#
# - nr: sector number
#
# Uses global file handle $iso_fh.
#
sub read_sector
{
  my $buf;

  die "$iso_file: seek error\n" unless seek($iso_fh, $_[0] * 0x800, 0);
  die "$iso_file: read error\n" if sysread($iso_fh, $buf, 0x800) != 0x800;

  return $buf;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# write_sector(nr, buf)
#
# Write 2k sector to iso image.
#
# - nr: sector number
# - buf: data to write
#
# Uses global file handle $iso_fh.
#
sub write_sector
{
  die "$iso_file: seek error\n" unless seek($iso_fh, $_[0] * 0x800, 0);
  die "$iso_file: write error\n" if syswrite($iso_fh, $_[1], 0x800) != 0x800;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fix_catalog()
#
# Fixes el torito boot catalog.
#
# mkisofs writes a booot catalog that's not exactly standard conform. This
# function fixes it.
#
sub fix_catalog
{
  return unless $mkisofs->{fix_catalog};

  die "$iso_file: $!\n" unless open $iso_fh, "+<", $iso_file;

  my $vol_descr = read_sector 0x10;
  my $vol_id = substr($vol_descr, 0, 7);
  die "$iso_file: not an iso9660 fs\n" if $vol_id ne "\x01CD001\x01";

  my $eltorito_descr = read_sector 0x11;
  my $eltorito_id = substr($eltorito_descr, 0, 0x1e);
  die "$iso_file: not bootable\n" if $eltorito_id ne "\x00CD001\x01EL TORITO SPECIFICATION";

  my $boot_catalog_idx = unpack "V", substr($eltorito_descr, 0x47, 4);
  die "$iso_file: strange boot catalog location: $boot_catalog_idx\n" if $boot_catalog_idx < 0x12;

  my $boot_catalog = read_sector $boot_catalog_idx;

  my $entries = @{$mkisofs->{fix_catalog}};

  my @entry;

  # collect boot catalog entries
  # depending on the mkisofs variant, the catalog may or may not be correct
  # that is, have section headers (type 0x90, 0x91) or not

  for (my $i = my $j = 0; $i < $entries; $j++) {
    my $ent = substr $boot_catalog, 32 * ($j + 1), 32;
    my $t = (unpack "C", $ent)[0];

    next if $t == 0x90 || $t == 0x91;

    if($t != 0x88) {
      die "$iso_file: boot entry $i: strange content\n";
    }

    push @entry, $ent;
    substr($entry[-1], 12, 20) = pack "Ca19", 1, $mkisofs->{fix_catalog}[$i];

    $i++;
  }

  # rewrite the boot catalog completely

  substr($boot_catalog, 32) = "\x00" x (length($boot_catalog) - 32);

  substr($boot_catalog, 32 * 1, 32) = $entry[0];

  if(!$has_el_torito && $has_efi) {
    # change platform id (1 byte at offset 1) from 0x0 to 0xef (EFI)...
    substr($boot_catalog, 1, 1) = "\xef";
    # ... and adjust header checksum (16 bits at offset 0x1c)
    substr($boot_catalog, 0x1c, 2) = pack("v", unpack("v", substr($boot_catalog, 0x1c, 2)) - 0xef00);
  }

  for (my $i = 1; $i < $entries; $i++) {
    my $section_head = pack "CCva28", $i == $entries - 1 ? 0x91 : 0x90, 0xef, 1, "";
    substr($boot_catalog, 32 * (2 * $i), 32) = $section_head;
    substr($boot_catalog, 32 * (2 * $i + 1), 32) = $entry[$i];
  }

  write_sector $boot_catalog_idx, $boot_catalog;

  close $iso_fh;
  undef $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# relocate_catalog()
#
# Relocate boot catalog.
#
# Some hardware has problems booting from dvd when the boot catalog is not
# near the beginning of the iso image.
#
# The catalog can actually be nearly in any place in the iso image but
# mkisofs doesn't let you influence it (much).
#
# But mkisofs puts a 'comment' block near the start of the iso image. So, we
# take the somewhat drastic step to relocate the catalog into this 'comment'
# block and have the catalog as much at the top of the image as possible.
#
sub relocate_catalog
{
  return unless $mkisofs->{fix_catalog};

  die "$iso_file: $!\n" unless open $iso_fh, "+<", $iso_file;

  my $vol_descr = read_sector 0x10;
  my $vol_id = substr($vol_descr, 0, 7);
  die "$iso_file: not an iso9660 fs\n" if $vol_id ne "\x01CD001\x01";

  my $path_table = unpack "V", substr($vol_descr, 0x08c, 4);
  die "$iso_file: strange path table location: $path_table\n" if $path_table < 0x11;

  my $new_location = $path_table - 1;

  my $eltorito_descr = read_sector 0x11;
  my $eltorito_id = substr($eltorito_descr, 0, 0x1e);
  die "$iso_file: not bootable\n" if $eltorito_id ne "\x00CD001\x01EL TORITO SPECIFICATION";

  my $boot_catalog_idx = unpack "V", substr($eltorito_descr, 0x47, 4);
  die "$iso_file: strange boot catalog location: $boot_catalog_idx\n" if $boot_catalog_idx < 0x12;

  my $boot_catalog = read_sector $boot_catalog_idx;

  my $vol_descr2 = read_sector $new_location - 1;
  my $vol_id2 = substr($vol_descr2, 0, 7);
  if($vol_id2 ne "\xffCD001\x01") {
    undef $new_location;
    for(my $i = 0x12; $i < 0x40; $i++) {
      $vol_descr2 = read_sector $i;
      $vol_id2 = substr($vol_descr2, 0, 7);
      if($vol_id2 eq "\x00TEA01\x01" || $boot_catalog_idx == $i + 1) {
        $new_location = $i + 1;
        last;
      }
    }
  }

  die "$iso_file: unexpected layout\n" unless defined $new_location;

  # oops, already relocated?
  return if $boot_catalog_idx == $new_location;

  my $version_descr = read_sector $new_location;
  die "$iso_file: unexpected layout\n" if $version_descr ne ("\x00" x 0x800) && substr($version_descr, 0, 4) ne "MKI ";

  # now reloacte to $new_location
  substr($eltorito_descr, 0x47, 4) = pack "V", $new_location;
  write_sector $new_location, $boot_catalog;
  write_sector 0x11, $eltorito_descr;

  printf "boot catalog moved: %d -> %d\n", $boot_catalog_idx, $new_location if $opt_verbose >= 1;

  close $iso_fh;
  undef $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# rerun_mkisofs()
#
# Prepare hybrid image and run mkisofs again.
#
sub rerun_mkisofs
{
  my $iso_file_list = isols;
  my $iso_magic = find_magic($iso_file_list);

  die "$iso_file: oops, magic not found\n" unless $iso_magic;

  if($opt_hybrid_fs eq 'iso') {
    meta_iso($iso_magic);
    $progress_start = 50;
  }
  elsif($opt_hybrid_fs eq 'fat') {
    $progress_start = 33;
    $progress_end = 67;
    meta_fat($iso_magic, $iso_file_list);
    $progress_start = 67;
  }

  $progress_end = 100;

  if($opt_volume1) {
    $mkisofs->{options} .= " -V '" . substr($opt_volume1, 0, 32) . "'";
  }

  run_mkisofs;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_isohybrid()
#
# Add a partition table to the iso image and, on x86, add boot code to the
# partition table.
#
sub run_isohybrid
{
  my $opt;
  my $ok;
  my $part_type = $hybrid_part_type;

  if($opt_hybrid_fs eq 'fat') {
    $part_type = 0x0c if !$part_type;
  }

  $opt .= " --uefi" if $has_efi;
  $opt .= " --no-legacy" if !$has_el_torito;
  $opt .= " --gpt" if $opt_hybrid_gpt;
  $opt .= " --mbr" if $opt_hybrid_mbr;
  $opt .= " --no-mbr" if $opt_no_prot_mbr;
  $opt .= " --no-code" if $opt_no_mbr_code;
  $opt .= " --no-chs" if $opt_no_mbr_chs;
  $opt .= sprintf(" --type 0x%x", $part_type) if $part_type;
  $opt .= " --offset $mkisofs->{partition_start}" if $mkisofs->{partition_start};
  $opt .= " --size $image_size" if $image_size;
  $opt .= " --mbr-file $hybrid_mbr_code" if $hybrid_mbr_code;
  $opt .= " --grub" if $hybrid_grub;

  my $cmd = "isohybrid $opt '$iso_file'";

  print "running:\n$cmd\n" if $opt_verbose >= 1;

  $ok = !system("$cmd 2>$tmp_err >&2");

  if(open my $fh, "<", $tmp_err) {
    local $/;
    $_ = <$fh>;
    close $fh;
  }

  print $_ if $opt_verbose >= 2 || !$ok;

  die "Error: isohybrid failed\n" if !$ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_syslinux()
#
# Make fat partition bootable using syslinux. This requires the 'real'
# syslinux package and does not work on non-x86 achitectures.
#
sub run_syslinux
{
  return unless $syslinux_config && $mkisofs->{partition_start};

  my $mbr;
  if(open my $f, "/usr/share/syslinux/mbr.bin") {
    local $/;
    $mbr = <$f>;
    close $f;
  }

  if(!-x "/usr/bin/syslinux" || length($mbr) != 440) {
    die "syslinux is needed to build a bootable FAT image, please install package 'syslinux'\n"
  }

  # syslinux must be run as root now
  susystem "syslinux -t " . ($mkisofs->{partition_start} << 9) . " -d '$syslinux_config' -i '$iso_file'";

  die "$iso_file: $!\n" unless open $iso_fh, "+<", $iso_file;
  syswrite $iso_fh, $mbr;
  close $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_isozipl()
#
# Make iso image zipl bootable.
#
sub run_isozipl
{
  my $opt;
  my $ok;

  $opt = " --options '$opt_boot_options'" if $opt_boot_options;

  my $cmd = "isozipl$opt '$iso_file'";

  print "running:\n$cmd\n" if $opt_verbose >= 1;

  $ok = !system("$cmd 2>$tmp_err >&2");

  if(open my $fh, "<", $tmp_err) {
    local $/;
    $_ = <$fh>;
    close $fh;
  }

  print $_ if $opt_verbose >= 2 || !$ok;

  die "Error: isozipl failed\n" if !$ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_createrepo(repo_dir)
#
# Run 'createrepo' on repo_dir to create a repo-md repo.
#
sub run_createrepo
{
  my $dir = $_[0];
  my $ok;

  my $tmp_dir = $tmp->dir();
  my $new_source = { dir => $tmp_dir, real_name => $tmp_dir, type => 'dir' };

  $dir = $tmp_dir if $opt_type =~ /^(micro|nano|pico)$/;

  my $cmd = "createrepo --simple-md-filenames --general-compress-type=gz -o '$tmp_dir' '$dir'";

  print "running:\n$cmd\n" if $opt_verbose >= 1;

  $ok = !system("$cmd 2>$tmp_err >&2");

  if(open my $fh, "<", $tmp_err) {
    local $/;
    $_ = <$fh>;
    close $fh;
  }

  print $_ if $opt_verbose >= 2 || !$ok;

  die "error: createrepo failed\n" if !$ok;

  # sign repomd.xml

  my $name = "$dir/repodata/repomd.xml";

  return if !$sign_key_dir || !-f $name;

  system "cp $sign_key_pub $name.key";

  print "signing '$name'\n" if $opt_verbose >= 1;

  system "gpg --homedir=$sign_key_dir --local-user '$sign_key_id' --batch --yes --armor --detach-sign $sign_passwd_option $name";

  exclude_files [ "repodata" ];

  push @sources, $new_source;
  update_filelist [ $new_source ];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# ISO file list sorted by start address.
#
# Return ref to array with files.
#
sub isols
{
  my $files;

  open my $fd, "isoinfo -R -l -i $iso_file 2>/dev/null |";

  my $dir = "/";

  while(<$fd>) {
    if(/^Directory listing of\s*(\/.*\/)/) {
      $dir = $1;
      next;
    }

    # isoinfo format change
    # cf. https://sourceforge.net/p/cdrtools/mailman/message/35173024
    s/^\s*\d+\s+//;

    if(/^(.)(.*)\s\[\s*(\d+)(\s+\d+)?\]\s+(.*?)\s*$/) {
      my $type = $1;
      my @x = split ' ', $2;
      $type = ' ' if $type eq '-';
      if($5 ne '.' && $5 ne '..') {
        push @$files, { name => "$dir$5", type => $type, start => $3 + 0, size => $x[4] };
      }
    }
  }

  close $fd;

  $files = [ sort { $a->{start} <=> $b->{start} } @$files ] if $files;

  # we need some more date for fat fs
  if($opt_hybrid_fs eq 'fat') {
    for (my $i = 0; $i < @$files - 1; $i++) {
      next unless $files->[$i]{type} eq ' ';
      my $p = $files->[$i + 1]{start} - $files->[$i]{start} - (($files->[$i]{size} + 0x7ff) >> 11);
      $files->[$i]{pad} = $p if $p > 0;
      my $is_link = $files->[$i + 1]{start} == $files->[$i]{start};
      $files->[$i + 1]{link} = 1 if $is_link;
      if($p < 0) {
        if($is_link) {
          print STDERR "link found: $files->[$i]{name} = $files->[$i+1]{name}\n";
        }
        else {
          die "$files->[$i]{name}: oops, negative padding: $p\n";
        }
      }
    }
  }

  # printf "%6d\t%s %8d %s\n", $_->{start}, $_->{type}, $_->{size}, $_->{name} for @$files;

  return $files;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# magic = find_magic(file_list)
#
# Find magic block.
# - file_list: array ref with file names as produced by isols()
# - magic: hash ref with offset of magic block ('block') and
#   offset of first (with lowest start offset) file ('extra')
#
# Offsets are in 2k units (due to iso fs heritage).
#
sub find_magic
{
  my $cnt;
  my $start;
  my $first;

  my $files = shift;

  die "$iso_file: $!\n" unless open $iso_fh, "<", $iso_file;

  found: for (@$files) {
    next unless $_->{type} eq ' ';
    last if $cnt++ >= 8;			# check just first 8 files
    my $buf;
    for (my $i = 0; $i >= -16; $i--) {		# go back up to 16 blocks
      seek $iso_fh, ($_->{start} + $i) << 11, 0;
      sysread $iso_fh, $buf, length $magic_id;
      $start = $_->{start} + $i, last found if $buf eq $magic_id;
    }
  }

  close $iso_fh;

  for (@$files) {
    next unless $_->{type} eq ' ';
    $first = $_->{start};
    last;
  }

  $first = 0 if $first >= $start;

  print "meta data found: first = $first, start = $start\n" if $opt_verbose >= 1;

  return { extra => $first, block => $start };
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# metca_iso(magic)
#
# Prepare hybrid image using iso fs for partition.
# - magic: hash ref as returned by find_magic()
#
sub meta_iso
{
  my $magic = shift;

  # copy meta data

  $mkisofs->{partition_start} = $magic->{block} * 4;

  my $blocks = $magic->{block} + 1;
  my $buf;

  die "$iso_file: $!\n" unless open $iso_fh, "<", $iso_file;

  my $sf = fname "glump";

  open my $fh, ">", "$sf" or die "$sf: $?\n";

  for (my $i = 0; $i < $blocks; $i++) {
    die "$iso_file: read error\n" unless sysread($iso_fh, $buf, 2048) == 2048;
    die "$sf: write error\n" unless syswrite($fh, $buf, 2048) == 2048;
  }

  close $fh;
  close $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fat_mkfs(name, size, hidden)
#
# Create a fat file system image.
# - name: image name
# - size: size in blocks
# - hidden: hidden blocks (aka planned partition offset)
#
sub fat_mkfs
{
  my ($name, $size, $hidden) = @_;

  open my $fh, ">", $name;
  close $fh;
  truncate $name, $size << 9;
  # try fat16 first
  system "mformat -i '$name' -T $size -H $hidden -s 32 -h 64 -c 4 -d 1 -v 'SUSEDISK' :: 2>/dev/null" and
  system "mformat -i '$name' -T $size -H $hidden -s 32 -h 64 -c 4 -F -d 1 -v 'SUSEDISK' ::";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# meta_fat(magic, file_list)
#
# Prepare hybrid image using fat fs for partition.
# - magic: hash ref as returned by find_magic()
# - file_list: array ref with file names as produced by isols()
#
sub meta_fat
{
  my $magic = shift;
  my $iso_files = shift;

  my $fat_size;

  my $tmp = $tmp->file('somefile');

  for (reverse @$iso_files) {
    next unless $_->{type} eq ' ';
    $fat_size = $_->{start} + (($_->{size} + 0x7ff) >> 11);
    last;
  }

  for (@$iso_files) {
    next unless $_->{type} eq 'd';
    $fat_size++;
  }

  $fat_size += ($fat_size >> 8) + 4;

  # we want $fat_size to count 512 byte blocks, not 2k blocks as in iso fs
  $fat_size *= 4;

  # add a bit free space (4 MB)
  $fat_size += 4 << 11;

  # and round up to full MB
  my $fat_size = (($fat_size + 2047) >> 11) << 11;

  printf "fat_size (auto) = $fat_size\n" if $opt_verbose >= 2;

  # disk size - partition offset - max alignment
  my $user_fat_size = $image_size - ($magic->{block} << 2) - 3;

  # use user-specified value, if possible
  $fat_size = $user_fat_size if $user_fat_size > $fat_size;

  printf "fat_size (final) = $fat_size\n" if $opt_verbose >= 2;

  fat_mkfs $tmp_fat, $fat_size, 0;

  my $fat_data_start = fat_data_start $tmp_fat;

  my $align = ($fat_data_start & 0x7ff) >> 9;
  $align = (4 - $align) & 3;

  print "fat fs alignment: $align blocks\n" if $opt_verbose >= 2;

  $mkisofs->{partition_start} = ($magic->{block} << 2) + $align;

  # remake, but with correct start offset stored in bpb
  fat_mkfs $tmp_fat, $fat_size, $mkisofs->{partition_start};

  # 1.: directories
  for (@$iso_files) {
    next unless $_->{type} eq 'd';
    system "mmd -i '$tmp_fat' -D o ::$_->{name}";
  }

  # 2.: directory entries
  for (@$iso_files) {
    next unless $_->{type} eq ' ';
    system "mcopy -i '$tmp_fat' -D o $tmp ::$_->{name}";
  }

  # 3.: add files
  my $pad = 0;
  my $pad_cnt = 0;
  my $pr_size = (@$iso_files);
  $pr_size = 1 if !$pr_size;
  my $pr_cnt = 0;
  for (@$iso_files) {
    $pr_cnt++;
    next unless $_->{type} eq ' ';
    truncate $tmp, $_->{size};
    system "mcopy -i '$tmp_fat' -D o $tmp ::$_->{name}";
    if($_->{pad}) {
      $pad += $_->{pad};
      truncate $tmp, $pad << 11;
      truncate $tmp, $_->{pad} << 11;
      $pad_cnt++;
      system "mcopy -i '$tmp_fat' -D o $tmp ::padding$pad_cnt";
    }
    show_progress 100 * $pr_cnt / $pr_size;
  }

  system "mdel -i '$tmp_fat' '::padding*'" if $pad;

  # 4.: read file offsets
  for (@$iso_files) {
    $_->{fat} = 0;
    $_->{fat} = $1 if `mshowfat -i '$tmp_fat' ::$_->{name}` =~ /<(\d+)/;
  }

  # 5.: verify file offsets
  my $dif;
  my $first;
  for (@$iso_files) {
    next unless $_->{type} eq ' ' && $_->{fat};
    $first = $_->{fat};
    $dif = $_->{start} - $_->{fat};
    last;
  }

  # for (@$iso_files) {
  #   printf "%6d %6d  [%4d]  (%d)\t%s %8d %s\n", $_->{start}, $_->{fat}, $_->{start} - $_->{fat}, $_->{pad} ? $_->{pad} : 0, $_->{type}, $_->{size}, $_->{name};
  # }

  for (@$iso_files) {
    next unless $_->{type} eq ' ' && $_->{fat};
    if($_->{start} - $_->{fat} != $dif) {
      printf STDERR "%6d %6d\t%s %8d %s\n", $_->{start}, $_->{fat}, $_->{type}, $_->{size}, $_->{name};
      die "$_->{name}: wrong fat offset: $dif\n";
    }
  }

  my $last_block;

  for (reverse @$iso_files) {
    next unless $_->{type} eq ' ' && $_->{fat};
    print "last file: $_->{name} $_->{fat}\n" if $opt_verbose >= 2;
    $last_block =  $_->{fat} + (($_->{size} + 0x7ff) >> 11);
    last;
  }

  print "last block: $last_block\n" if $opt_verbose >= 2;

  # we're going to use syslinux instead of isolinux, so rename the config file
  if($opt_hybrid_fs eq 'fat') {
    for (@$iso_files) {
      if($_->{name} =~ m#/isolinux.cfg$#) {
        system "mren -i '$tmp_fat' '::$_->{name}' syslinux.cfg";
        $syslinux_config = $_->{name};
        $syslinux_config =~ s#^/##;
        $syslinux_config =~ s#/[^/]+$##;
        last;
      }
    }
  }

  my $data_start = $fat_data_start + (($first - 2) << 11);
  $last_block = ($fat_data_start >> 9) + (($last_block - 2) << 2);

  printf "last_block = $last_block\n" if $opt_verbose >= 2;

  die "$tmp_fat: oops, data start not found\n" unless $data_start;

  print "data start = $data_start\n" if $opt_verbose >= 2;

  truncate $tmp_fat, $data_start;

  # now copy the fat

  my $sf = fname "glump";

  open my $fh, ">", $sf;

  seek $fh, $align << 9, 0;

  open my $fat_fh, $tmp_fat;

  for (my $i = 0; $i < $data_start >> 20; $i++) {
    my $buf;
    sysread $fat_fh, $buf, 1 << 20;
    syswrite $fh, $buf, 1 << 20;
  }

  if(my $i = $data_start & ((1 << 20) - 1)) {
    my $buf;
    sysread $fat_fh, $buf, $i;
    syswrite $fh, $buf, $i;
  }

  close $fat_fh;

  if($magic->{extra}) {
    my $buf;
    open $iso_fh, $iso_file;
    seek $iso_fh, $magic->{extra} << 11, 0;
    for (my $i = $magic->{extra}; $i < $magic->{block} + 1; $i++) {
      sysread $iso_fh, $buf, 0x800;
      syswrite $fh, $buf, 0x800;
    }
    close $iso_fh;
  }

  close $fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fat_data_start(fs_image_file)
#
# Returns the offset (in bytes) of the data area of the fat fs in
# fs_image_file or not at all if there are problems detecting it.
#
sub fat_data_start
{
  my $data_start;

  for (`dosfsck -v '$_[0]' 2>/dev/null`) {
    if(/Data area starts at byte (\d+)/) {
      $data_start = $1;
      last;
    }
  }

  die "error: dosfsck failed\n" unless $data_start;

  return $data_start;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# create_initrd()
#
# Combine the various initrd parts into the new one.
#
# This will only _append_ the new parts to the original unless
# $opt_rebuild_initrd is set.
#
sub create_initrd
{
  return undef if !@opt_initrds;

  my $tmp_initrd = $tmp->file();
  my $tmp_dir = $tmp->dir();

  if($opt_rebuild_initrd) {
    print "initrd: complete rebuild\n" if $opt_verbose >= 1;
    unpack_orig_initrd if !$orig_initrd;
    die "initrd unpacking failed\n" if !$orig_initrd;
    $tmp_dir = $orig_initrd;

    if(
      $kernel->{initrd_layout} ne 'install' &&
      $kernel->{modules_replaced}
    ) {
      # remove old modules and firmware trees
      print "removing old kernel tree (initrd): $kernel->{target_lib_dir}/modules/$kernel->{orig_version}\n" if $opt_verbose >= 2;
      system "rm -rf $orig_initrd/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}";
      system "rm -rf $orig_initrd/$kernel->{target_lib_dir}/firmware";
    }
  }

  for my $i (@opt_initrds) {
    my $type = get_archive_type $i;

    if($type) {
      unpack_archive $type, $i, $tmp_dir;
    }
    else {
      print STDERR "Warning: ignoring initrd part $i\n";
    }
  }

  if(
    $applied_duds->{initrd_needs_depmod} &&
    -d "$tmp_dir/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}/updates"
  ) {
    if($kernel->{initrd_layout} eq 'install') {
      my $d = "$tmp_dir/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}";

      for my $m (glob "$d/updates/*${kext_glob}") {
        next unless -f $m;
        my $dst = kernel_module_exists "$d/initrd", $m;
        if($dst ne "") {
          unlink $dst;
          system "cp $m $d/initrd";
        }
      }

      my $i_dir = "$orig_initrd_00_lib/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}/initrd";

      if($orig_initrd_00_lib && -d $i_dir) {
        for my $m (glob "$tmp_dir/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}/updates/*${kext_glob}") {
          next unless -f $m;
          my $dst = kernel_module_exists $i_dir, $m;
          unlink $dst if $dst ne "";
          system "mv $m $i_dir";
        }

        print "running depmod in initrd\n" if $opt_verbose >= 1;
        run_depmod $orig_initrd_00_lib, $kernel->{orig_version};

        rmdir "$tmp_dir/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}/updates";

        my $comp = $mksquashfs_has_comp ? "-comp xz" : "";

        if($opt_no_compression->{squashfs}) {
          if(!$mksquashfs_has_comp) {
            die "mksquashfs version too old to allow setting compression algorithm\n";
          }
          else {
            $comp = "-no-compression";
          }
        }

        mkdir "$tmp_dir/parts";
        unlink "$tmp_dir/parts/00_lib";

        my $err = system "mksquashfs $orig_initrd_00_lib $tmp_dir/parts/00_lib $comp -noappend -no-progress >/dev/null 2>&1";
        die "mksquashfs failed\n" if $err;
      }
    }
    else {
      print "running depmod in initrd\n" if $opt_verbose >= 1;
      run_depmod $tmp_dir, $kernel->{orig_version};
    }
  }

  if($opt_no_docs) {
    system "rm -rf $tmp_dir/usr/share/{doc,info,man}";
    rmdir "$tmp_dir/usr/share";
    rmdir "$tmp_dir/usr";
  }

  # make it possible to directly add linuxrc.rpm - it's a bit special
  if(-f "$tmp_dir/usr/sbin/linuxrc") {
    rename "$tmp_dir/usr/sbin/linuxrc", "$tmp_dir/init";
    print "initrd: linuxrc detected, renamed to /init\n";
  }

  my $compr = 'cat';
  # don't go for -9 with xz as it doesn't lead to noticeably reduced size
  # but substancially limits multithreading
  $compr = "xz --quiet --threads=0 --check=crc32 -c" if $initrd_format eq "xz";
  $compr = "gzip --quiet -9 -c" if $initrd_format eq "gz";
  $compr = "zstd --quiet --threads=0 -19 -c" if $initrd_format eq "zst";

  chmod 0755, $tmp_dir;

  system "( cd $tmp_dir; find . | cpio --quiet -o -H newc --owner 0:0 | $compr ) >> $tmp_initrd";

  # system "ls -lR $tmp_dir";

  return $tmp_initrd;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_instsys_suse()
#
# Combine the various instsys parts and append to existing image (SUSE layout).
#
# This will only _append_ the new parts to the original.
#
sub add_instsys_suse
{
  return if !@opt_instsys && !@opt_rescue;

  my $x = get_kernel_initrd;
  die "oops: no initrd?\n" unless $x;

  my $instsys_location;
  my $instsys_fname;
  my $rescue_location;
  my $rescue_fname;
  my $has_liveos;

  if($x->{initrd} =~ m#(boot/[^/]+)/#) {
    $instsys_location = "$1/root";
    $rescue_location = "$1/rescue";

    $instsys_fname = fname $instsys_location;
    $rescue_fname = fname $rescue_location;
  }

  if(@opt_instsys && !$instsys_fname) {
    $instsys_location = "LiveOS/squashfs.img";
    $instsys_fname = fname $instsys_location;

    $has_liveos = 1 if $instsys_fname;
  }

  if(@opt_instsys && !$instsys_fname) {
    die "no root file system on media found\n";
  }

  if(@opt_rescue && !$rescue_fname) {
    die "no rescue file system on media found\n";
  }

  if(@opt_instsys) {
    if($has_liveos) {
      add_instsys_live $instsys_location, \@opt_instsys;
    }
    else {
      add_instsys_classic $instsys_location, \@opt_instsys;
    }
  }

  if(@opt_rescue) {
    add_instsys_classic $rescue_location, \@opt_rescue;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_instsys_rh()
#
# Combine the various instsys parts and append to existing image (RH layout).
#
# This will only _append_ the new parts to the original.
#
sub add_instsys_rh
{
  return if !@opt_instsys;

  my $instsys_location = "images/install.img";
  my $instsys_fname = fname $instsys_location;

  die "no root file system on media found\n" if !$instsys_fname;

  # Determine whether this is just a squashfs image or an ext4 image
  # (with name LiveOS/rootfs.img) inside a squashfs image.
  my $has_liveos;
  for (`unsquashfs -ls $instsys_fname 2>/dev/null`) {
    $has_liveos = 1, last if m#squashfs-root/LiveOS#;
  }

  if($has_liveos) {
    add_instsys_live $instsys_location, \@opt_instsys;
  }
  else {
   add_instsys_classic $instsys_location, \@opt_instsys;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_instsys_classic(image_name, file_list)
#
# Handle classic case (both RH and SUSE).
#
# image_name: file name (on media) of squashfs image containing the root file system.
# file_list: ref to array with list of files to add.
#
sub add_instsys_classic
{
  my $image_location = $_[0];
  my $file_list = $_[1];

  my $image_fname = copy_file $image_location;

  print "identified root file system: $image_location\n";

  my $new_files = prepare_new_instsys_files $file_list;

  # note: squashfs handling needs root for xattrs
  my $tmp_root = $tmp->dir();
  my $err = susystem "unsquashfs -no-progress -dest $tmp_root/root $image_fname >/dev/null";
  die "extracting root file system failed\n" if $err;

  susystem "sh -c 'tar -C $new_files -cf - . | tar -C $tmp_root/root --keep-directory-symlink -xpf -'";

  # mksquashfs expects the image *not* to exist
  unlink $image_fname or die "$image_fname: $!\n";

  my $err = susystem "mksquashfs $tmp_root/root $image_fname -comp xz -all-root -no-progress >/dev/null";

  # change owner so that files can be garbage collected
  susystem "chown -R $< $image_fname $tmp_root/root";

  die "mksquashfs failed to append to $image_location\n" if $err;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_instsys_live(image_name)
#
# Handle Live image case (both RH and SUSE).
#
# image_name: file name (on media); squashfs image containing an (usually ext4) image "LiveOS/rootfs.img"
# file_list: ref to array with list of files to add.
#
# Note: the size of the included file system image (LiveOS/rootfs.img) is not adjusted.
#
sub add_instsys_live
{
  my $image_location = $_[0];
  my $file_list = $_[1];

  my $image_fname = fname $image_location;

  print "Identified Live system: $image_location\n";

  check_root "Sorry, can't update Live root file system; you need root privileges.";

  my $new_files = prepare_new_instsys_files $file_list;

  # extract image, add new stuff, and repack image
  my $image_new = copy_file $image_location;

  # note: squashfs handling needs root for xattrs
  my $tmp_live = $tmp->dir();
  my $err = susystem "unsquashfs -no-progress -dest $tmp_live/root $image_fname LiveOS/rootfs.img >/dev/null 2>&1";
  die "extracting LiveOS/rootfs.img failed\n" if $err;

  susystem "chmod 755 $tmp_live/root";
  susystem "chmod 644 $tmp_live/root/LiveOS/rootfs.img";
  susystem "chown -R $< $tmp_live/root";
  susystem "chmod 644 $image_new";

  if(defined $instsys_size) {
    my $root_size = -s "$tmp_live/root/LiveOS/rootfs.img";
    $root_size >>= 9;
    $instsys_size += $root_size if $opt_instsys_size =~ /^\+/;
    $instsys_size = $root_size - $instsys_size if $opt_instsys_size =~ /^\-/;

    die "target live root fs size too small: $instsys_size blocks\n" if $instsys_size <= 0;

    printf "target live root file system size: %.2f GiB\n", $instsys_size / (1 << 21);

    if($instsys_size > $root_size) {
      if(open my $f, "+<", "$tmp_live/root/LiveOS/rootfs.img") {
        truncate $f, $instsys_size << 9;
        close $f;
      }

      $err = susystem "resize2fs -f $tmp_live/root/LiveOS/rootfs.img >/dev/null 2>&1";
      die "resizing live root fs failed\n" if $err;
    }

    if($instsys_size < $root_size) {
      my $log = $tmp->file();
      $err = susystem "resize2fs -f $tmp_live/root/LiveOS/rootfs.img ${instsys_size}s >$log 2>&1";
      if($err) {
        my $r = `cat $log`;
        $r = $r =~ /No space left on device/ ? " (no space left on device)" : "";
        die "resizing live root fs failed$r\n";
      }

      if(open my $f, "+<", "$tmp_live/root/LiveOS/rootfs.img") {
        truncate $f, $instsys_size << 9;
        close $f;
      }
    }
  }

  my $tmp_mnt = $tmp->dir();
  die "\nLiveOS mount failed\n" if susystem "mount -oloop '$tmp_live/root/LiveOS/rootfs.img' $tmp_mnt";

  if(
    $opt_rebuild_initrd &&
    $kernel->{modules_replaced}
  ) {
    print "removing old kernel tree (live-root): /$kernel->{target_lib_dir}/modules/$kernel->{orig_version}\n" if $opt_verbose >= 2;
    susystem "rm -rf $tmp_mnt/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}";
    susystem "rm -rf $tmp_mnt/$kernel->{target_lib_dir}/firmware" unless $kernel->{keep_firmware};
  }

  susystem "sh -c 'tar -C $new_files --owner=0 --group=0 -cf - . | tar -C $tmp_mnt --keep-directory-symlink -xpf -'";
  susystem "chmod 0755 $tmp_mnt";

  if(
    $applied_duds->{instsys_needs_depmod} &&
    -d "$tmp_mnt/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}/updates"
  ) {
    print "running depmod in live-root\n" if $opt_verbose >= 1;
    run_depmod $tmp_mnt, $kernel->{orig_version};
  }

  for my $i (glob "$tmp_mnt/boot/* $tmp_mnt/boot/.*") {
    susystem "rm -f '$i'" unless -e $i;
  }

  susystem "umount $tmp_mnt";

  my $err = susystem "mksquashfs $tmp_live/root $image_new -comp xz -all-root -noappend -no-progress >/dev/null 2>&1";
  die "mksquashfs failed to rebuild $image_location\n" if $err;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_new_instsys_files(file_list)
#
# Unpack all to-be-added instsys parts into a single directory and return this directory.
#
# file_list: ref to array with list of files to add.
#
sub prepare_new_instsys_files
{
  my $file_list = $_[0];
  my $tmp_dir = $tmp->dir();

  for my $i (@$file_list) {
    my $type = get_archive_type $i;

    if($type) {
      unpack_archive $type, $i, $tmp_dir;
    }
    else {
      print STDERR "Warning: ignoring root file system part $i\n";
    }
  }

  if($opt_no_docs) {
    system "rm -rf $tmp_dir/usr/share/{doc,info,man}";
    rmdir "$tmp_dir/usr/share";
    rmdir "$tmp_dir/usr";
  }

  return $tmp_dir;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# new_kernel_initrd_name(name)
#
# Calculate new file name for a copy of 'name', if one should be needed.
#
# It essentially adds "_%02d" to the file name ensuring the new file does not yet exists.
#
sub new_ki_name
{
  my $name = $_[0];
  my $idx = $_[1];

  my $ext = ($name =~ s/(\.[^.\/]+)$//) ? $1 : "";

  my $new = sprintf "%s%d%s", $name, $idx, $ext;

  return !fname($new) ? $new : undef;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# get_kernel_initrd()
#
# Return hash with kernel/initrd pair used for booting.
#
sub get_kernel_initrd
{
  my $x;
  my $cnt;

  for my $b (sort keys %$boot) {
    next if $opt_arch && $opt_arch ne $_;
    if($boot->{$b}{initrd} && $boot->{$b}{kernel}) {
      if(!$x) {
        $x = { initrd => $boot->{$b}{initrd}, kernel => $boot->{$b}{kernel}};
        $x->{initrd_alt} = $boot->{$b}{initrd_alt} if $boot->{$b}{initrd_alt};
        $x->{kernel_alt} = $boot->{$b}{kernel_alt} if $boot->{$b}{kernel_alt};
      }
      $cnt++;
    }
  }

  if($cnt > 1 && !$warned->{multi_arch}) {
    $warned->{multi_arch} = 1;
    print "Warning: more than one kernel/initrd pair to choose from\n";
    print "(Use '--arch' option to select a different one.)\n";
    print "Using $x->{kernel} & $x->{initrd}.\n";
  }

  if($opt_new_boot_entry && $x->{kernel} =~ m#/s390x/#) {
    die "sorry, --add-entry option does not work for s390x\n";
  }

  # look for potential initrd & kernel names
  if($x && $opt_new_boot_entry) {
    for (my $i = 1; $i < 100; $i++) {
      # FIXME: mybe also check initrd_alt + kernel_alt
      my $n_k = new_ki_name($x->{kernel}, $i);
      my $n_i = new_ki_name($x->{initrd}, $i);
      if(!fname($n_k) && !fname($n_i)) {
        $x->{kernel_copy} = $n_k;
        $x->{initrd_copy} = $n_i;
        last;
      }
    }
  }

  return $x;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_kernel_initrd()
#
# Put new kernel/initrd into the image (at the correct location).
#
sub update_kernel_initrd
{
  my $x = get_kernel_initrd;

  return if !$x;

  if($add_initrd) {
    if($x->{initrd_copy}) {
      my $old = fname $x->{initrd};
      my $new = new_file $x->{initrd_copy};

      system "cp '$old' '$new'";
      $x->{initrd} = $x->{initrd_copy};
      delete $x->{initrd_copy};
    }
    else {
      copy_file $x->{initrd};
    }

    if(my $n = fname $x->{initrd}) {
      if($opt_rebuild_initrd) {
        system "cp '$add_initrd' '$n'";
      }
      else {
        system "cat '$add_initrd' >> '$n'";
      }
    }
  }
  else {
    delete $x->{initrd_copy};
  }

  # FIXME: maybe also duplicate initrd_alt when a copy is required?
  if($x->{initrd_alt}) {
    my $i_copy = copy_file $x->{initrd_alt};
    my $i_orig = copy_file $x->{initrd};
    if(-f $i_copy && -f $i_orig) {
      unlink $i_copy;
      if(!link $i_orig, $i_copy) {
        die "link: $i_orig -> $i_copy: $!\n";
      }
    }
  }

  if($add_kernel) {
    if($x->{kernel_copy}) {
      my $old = fname $x->{kernel};
      my $new = new_file $x->{kernel_copy};

      system "cp '$old' '$new'";
      $x->{kernel} = $x->{kernel_copy};
      delete $x->{kernel_copy};
    }
    else {
      copy_file $x->{kernel};
    }

    if(my $n = fname $x->{kernel}) {
      system "cp '$add_kernel' '$n'";
    }
  }
  else {
    delete $x->{kernel_copy};
  }

  # FIXME: maybe also duplicate kernel_alt when a copy is required?
  if($x->{kernel_alt}) {
    my $k_copy = copy_file $x->{kernel_alt};
    my $k_orig = copy_file $x->{kernel};
    if(-f $k_copy && -f $k_orig) {
      unlink $k_copy;
      if(!link $k_orig, $k_copy) {
        die "link: $k_orig -> $k_copy: $!\n";
      }
    }
  }

  $kernel->{current} = $x;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# get_initrd_format()
#
# Analyze original initrd parts and remember compression type.
#
# Raise an error if you are about the combine initrd parts with different
# compression types. While it _would_ technically be ok for the kernel to do
# this, this is really a nightmare on the user level side.
#
sub get_initrd_format
{
  my $f;

  return if $initrd_format;

  if(my $x = get_kernel_initrd) {
    my $c = get_archive_type fname($x->{initrd});
    print "initrd format: $c\n" if $opt_verbose >= 1;
    if($c =~ /\.(gz|xz|zst)($|\.\[)/) {
      if($f) {
        die "differing initrd formats: $f & $1\n" if $1 ne $f;
      }
      else {
        $f = $1;
      }
    }
    else {
      print STDERR "Warning: $x->{initrd}: uncompressed initrd\n";
      $f = 'cat';
    }
  }

  print "initrd compression type: $f\n" if $opt_verbose >= 1;

  $initrd_format = $f;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# unpack_orig_initrd()
#
# Locate original initrd and unpack it into a temporary directory.
#
sub unpack_orig_initrd
{
  if(my $x = get_kernel_initrd) {
    my $f = fname($x->{initrd});
    if(-f $f) {
      if($orig_initrd_filename eq $f) {
        print "keeping initrd file: $f\n" if $opt_verbose >= 2;
        return;
      }
      $orig_initrd_filename = $f;
      print "unpacking initrd file: $f\n" if $opt_verbose >= 2;
      $orig_initrd = $tmp->dir();
      my $type = get_archive_type $f;
      if($type) {
        unpack_archive $type, $f, $orig_initrd;
        if(-d "$orig_initrd/parts") {
          my $last_part;
          $last_part = (glob "$orig_initrd/parts/??_*")[-1];
          if($last_part =~ m#/(\d\d)_[^/]*$#) {
            $initrd_has_parts = $1 + 1;
          }
        }
      }
      else {
        undef $orig_initrd;
      }
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# extract_installkeys()
#
# Get 'installkey.gpg' from the ooriginal initrd.
#
# Older SUSE install initrds has the gpg keys in a file installkey.gpg. To
# be able to add keys we have to extract the file first, add the keys, and
# then write the new file.
#
# Current SUSE initrds don't have this file and use keys in
# /usr/lib/rpm/gnupg/keys directly.
#
sub extract_installkeys
{
  return if !$opt_sign;

  unpack_orig_initrd if !$orig_initrd;

  die "initrd unpacking failed\n" if !$orig_initrd;

  if(-f "$orig_initrd/installkey.gpg") {
    $initrd_installkeys = "$orig_initrd/installkey.gpg";
    print "old style initrd found\n" if $opt_verbose >= 1;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# create_cd_ikr()
#
# Needed to handle s390x systems.
#
sub create_cd_ikr
{
  local $_;

  my $ikr = $_[0];
  my $ins = $_[1];

  my $src = $ins;
  $src =~ s#/[^/]*$##;

  my $dst = $ikr;
  $dst =~ s#/[^/]*$##;

  my $initrd_name = "initrd";
  my $initrd_siz_name = "${initrd_name}.siz";
  my $initrd_off_name = "${initrd_name}.off";

  my $new_initrd_siz = copy_or_new_file "$src/$initrd_siz_name";
  if(open my $f, ">", $new_initrd_siz) {
    syswrite $f, pack("N", -s fname("$src/$initrd_name"));
    close $f;
  }

  my @layout;

  my $off_layout;

  if(open my $s, fname($ins)) {
    while(<$s>) {
      next if /^\s*\*/;
      push @layout, { name => "$src/$1", file => fname("$src/$1"), ofs => oct($2) } if /^\s*(\S+)\s+(\S+)/;
      $off_layout = $layout[-1] if $1 eq $initrd_off_name;
    }
    close $s;
  }

  die "$ins: nothing to do?\n" if !@layout;

  # create initrd offset entry, if it shows up in layout file
  if($off_layout) {
    for (@layout) {
      my $fname = $_->{file};
      if($fname =~ m#/${initrd_name}$#) {
        my $ofs = $_->{ofs};
        my $new_initrd_ofs = copy_or_new_file "$src/$initrd_off_name";
        if(open my $f, ">", $new_initrd_ofs) {
          syswrite $f, pack("N", $ofs);
          close $f;
        }
        $off_layout->{file} = fname($off_layout->{name});

        last;
      }
    }
  }

  my $new_ikr = copy_or_new_file $ikr;

  if(open my $d, ">", $new_ikr) {
    for (@layout) {
      my $fname = $_->{file};
      my $is_parmfile;
      $is_parmfile = 1 if $fname =~ m#/parmfile$#;
      if(open my $f, $fname) {
        sysread $f, my $buf, -s($f);
        close $f;
        sysseek $d, $_->{ofs}, 0;
        if($is_parmfile) {
          # write at least 1 block
          my $pad = 0x200 - length($buf);
          $buf .= "\x00" x $pad if $pad > 0;
          # remove newlines from parmfile
          $buf =~ s/\n+/ /g;
        }
        syswrite $d, $buf;
        # print "$fname: $_->{ofs} ", length($buf), "\n";
      }
      else {
        die "$_->{name}: $!\n";
      }
    }

    sysseek $d, 4, 0;
    syswrite $d, pack("N",0x80010000);

    # align to 4k
    sysseek $d, -s($d) | 0xfff, 0;
    syswrite $d, "\x00";

    close $d;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# merge_options(options, new_options)
#
# Update boot options with new_options and return updated option string.
#
sub merge_options
{
  my $opts = $_[0];
  my $new_opts = $_[1];

  # avoid tricky stuff...
  if($opts =~ /"/ || $new_opts =~ /"/) {
    $opts .= " $new_opts";
  }
  else {
    my @opts;
    my %new_opts;
    my @new_opts_order;
    for my $n (split " ", $new_opts) {
      if($n =~ /(.*?)=(.*)/) {
        push @new_opts_order, $1;
        $new_opts{$1} = $2;
      }
      else {
        push @new_opts_order, $n;
        $new_opts{$n} = undef;
      }
    }

    for my $opt (split " ", $opts) {
      my $k = $opt;
      $k =~ s/=.*//;
      if(exists $new_opts{$k}) {
        $opt = $k;
        $opt .= "=$new_opts{$k}" if defined $new_opts{$k};
        delete $new_opts{$k};
      }
      if(exists $new_opts{"-$k"}) {
        $opt = undef;
        delete $new_opts{$k};
      }
      push @opts, $opt if defined $opt;
    }

    for my $opt (@new_opts_order) {
      my $k = $opt;
      if(exists $new_opts{$opt}) {
        if($opt !~ /^-/) {
          $opt .= "=$new_opts{$opt}" if defined $new_opts{$opt};
          delete $new_opts{$k};
          push @opts, $opt;
        }
      }
    }

    $opts = join ' ', @opts;
  }

  print "boot options update:\n  \"$_[0]\"\n+ \"$_[1]\"\n= \"$opts\"\n" if $opt_verbose >= 3;

  return $opts;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# isolinux_add_option()
#
# Add new boot option to isolinux.cfg.
#
sub isolinux_add_option
{
  my $n = shift;
  my $b = shift;
  my $m = shift;

  if(open my $f, $n) {
    my @f = <$f>;
    close $f;

    print "updating isolinux config file: $n\n" if $opt_verbose >= 2;

    # if we should add a new entry, base it loosely on the 'linux' entry
    if($opt_new_boot_entry) {
      my %label;
      my $ext;
      my $comment;
      for (@f) {
        $label{$1} = 1 if /^\s*label\s+(\S+)/i;
      }
      # find first unused label
      for (; $ext < 99; $ext++) {
        last if !$label{"linux$ext"};
      }
      my $ent;
      # and insert a new entry after the 'linux' entry
      for (@f) {
        if($ent) {
          $ent .= $_;
          next if !/^\s*$/;

          if($ent !~ s/^(\s*menu label\s.*)/$1 - $opt_new_boot_entry/m) {
            $ent =~ s/^(\s*label.*)/$1\n  menu label Installation - $opt_new_boot_entry/m;
          }

          my $k = $kernel->{current}{kernel};
          $k =~ s#.*/##;
          $ent =~ s/^(\s*kernel\s).*/$1$k/m;

          my $i = $kernel->{current}{initrd};
          $i =~ s#.*/##;
          $ent =~ s/^(\s*append\s)(.*)/$1 . merge_options($2, "initrd=$i $b")/em;

          $_ .= $ent;
          print "added isolinux config entry:\n$ent" if $opt_verbose >= 2;
          last;
        }
        elsif(/^\s*label\s+linux/i) {
          $ent = "label linux$ext\n";
          $comment = "  linux" . sprintf("%-5s", $ext) . " - Installation - $opt_new_boot_entry";
        }
      }

      if($m && $comment) {
        if(open my $f, $m) {
          local $/ = undef;
          my $x = <$f>;
          close $f;
          $x =~ s/(^\s+linux\s+-\s.*)$/$1\n$comment/m;
          if(open my $f, ">", $m) {
            print $f $x;
            close $f;
          }
        }
      }
    }
    else {
      if($b) {
        @f = map { chomp; $_ = "$1 " . merge_options($2, $b) if /^(\s*append\s.*initrd=\S+)\s(.*)/; "$_\n" } @f;
      }
    }

    if(open my $f, ">", $n) {
      print $f @f;
      close $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# grub2_add_option()
#
# Add new boot option to grub.cfg.
#
sub grub2_add_option
{
  my $n = shift;
  my $b = shift;

 # that's what an 'install' entry must look like
  my $inst_regexp = '(?i)([\'|"])([^\'"]*(?:install|live)[^\'"]*)\\1';

  if(open my $f, $n) {
    my @f = <$f>;
    close $f;

    print "updating grub2 config file: $n\n" if $opt_verbose >= 2;

    # if we should add a new entry, base it loosely on the 'Installation' entry
    if($opt_new_boot_entry) {

      my $ent;
      # insert a new entry after the 'install' entry
      for (@f) {
        if($ent) {
          $ent .= $_;
          next if !/^\s*\}?\s*$/;
          $ent =~ s/$inst_regexp/$1$2 - $opt_new_boot_entry$1/;
          my $k = $kernel->{current}{kernel};
          $ent =~ s#(\slinux(efi)?\s+)(\S+)(.*?)\n#"$1/$k " . merge_options($4, $b) . "\n"#e;
          my $i = $kernel->{current}{initrd};
          $ent =~ s#(\sinitrd(efi)?\s+)(\S+)#$1/$i#;
          $_ .= $ent;
          print "added grub2 config entry:\n$ent" if $opt_verbose >= 2;
          last;
        }
        elsif(/^\s*menuentry\s+$inst_regexp/) {
          $ent .= $_;
        }
      }
    }
    else {
      if($b) {
        @f = map { chomp; $_ = "$1 " . merge_options($3, $b) if /^(\s*\$?linux(efi)?)\s(.*)/; "$_\n" } @f;
      }
    }

    if(open my $f, ">", $n) {
      print $f @f;
      close $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# yaboot_add_option()
#
# Add new boot option to yaboot.txt.
#
sub yaboot_add_option
{
  my $n = shift;
  my $b = shift;
  my $m = shift;

  if(open my $f, $n) {
    my @f = <$f>;
    close $f;

    # if we should add a new entry, base it loosely on the 'install' entry
    if($opt_new_boot_entry) {
      my %label;
      my $ext;
      my $comment;
      for (@f) {
        $label{$1} = 1 if /^\s*label=(\S+)/i;
      }
      # find first unused label
      for (; $ext < 99; $ext++) {
        last if !$label{"install$ext"};
      }
      my $ent;
      # and append a new entry at the end

      my $k = $kernel->{current}{kernel};
      $k =~ s#.*/##;
      my $i = $kernel->{current}{initrd};
      $i =~ s#.*/##;
      $b = " $b" if $b;
      $ent = "image[64bit]=$k\n  label=install$ext\n  append=\"quiet sysrq=1$b\"\n  initrd=$i\n\n";
      $comment = "  Type  \"install$ext\" to start Installation - $opt_new_boot_entry";

      pop @f if $f[-1] =~ /^\s*$/;
      push @f, $ent;

      if($m && $comment) {
        if(open my $f, $m) {
          local $/ = undef;
          my $x = <$f>;
          close $f;
          $x =~ s/(^\s*Type\s+"install"\s.*)$/$1\n$comment/m;
          if(open my $f, ">", $m) {
            print $f $x;
            close $f;
          }
        }
      }
    }
    else {
      if($b) {
        $_ = "$1$2 $b\"\n" if/^(\s*append\s*=\s*")\s*(.*?)\s*"\s*$/;
      }
    }

    if(open my $f, ">", $n) {
      print $f @f;
      close $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_boot_options()
#
# Add new booot option. Modifies files according to used boot loader.
#
sub update_boot_options
{
  return unless defined $opt_boot_options || $opt_new_boot_entry;

  # print Dumper($boot);

  for my $b (sort keys %$boot) {
    if($boot->{$b}{bl}{isolinux}) {
      my $n = copy_file "$boot->{$b}{bl}{isolinux}{base}/isolinux.cfg";
      my $m;
      $m = copy_file "$boot->{$b}{bl}{isolinux}{base}/message" if $opt_new_boot_entry;
      isolinux_add_option $n, $opt_boot_options, $m;
    }
    if($boot->{$b}{bl}{grub2}) {
      my $n = copy_file $boot->{$b}{bl}{grub2}{config} || copy_file "$boot->{$b}{bl}{grub2}{base}/grub.cfg";
      grub2_add_option $n, $opt_boot_options;
    }
    if($boot->{$b}{bl}{yaboot}) {
      my $n = copy_file "$boot->{$b}{bl}{yaboot}{base}/yaboot.cnf";
      my $m;
      $m = copy_file "$boot->{$b}{bl}{yaboot}{base}/yaboot.txt" if $opt_new_boot_entry;
      yaboot_add_option $n, $opt_boot_options, $m;
    }
    if($boot->{$b}{bl}{efi}) {
      my $n = copy_file $boot->{$b}{bl}{efi}{base};
      if(defined $n) {
        my $tmp = $tmp->file();
        if(!system "mcopy -n -i $n ::/efi/boot/grub.cfg $tmp 2>/dev/null") {
          grub2_add_option $tmp, $opt_boot_options;
          if(system "mcopy -D o -i $n $tmp ::/efi/boot/grub.cfg") {
            print STDERR "Warning: failed to update grub.cfg\n";
          }
        }
      }
    }
    if($b eq 'efi') {
      my $n = copy_file "$boot->{$b}{base}/grub.cfg";
      grub2_add_option $n, $opt_boot_options;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_normal()
#
# Wipe files we really don't want to see in our image.
#
sub prepare_normal
{
  # cleaning up bad isos a bit
  for ( "glump" ) {
    my $f = fname($_);
    push @{$mkisofs->{exclude}}, $f if $f;
  }

  push @{$mkisofs->{exclude}}, "TRANS.TBL";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Exclude files from iso.
#
# exclude_files(ref_to_file_list)
#
# ref_to_file_list is an array ref with file name patterns (regexp) to exclude
#
sub exclude_files
{
  my $list = $_[0];

  my $ex = join "|", @$list;

  for (sort keys %$files) {
    if(m#^($ex)$#) {
      my $f = fname($_);
      push @{$mkisofs->{exclude}}, $f if $f && !$files_to_keep->{$f};
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Register files that must _not_ be removed from iso.
#
# keep_files(ref_to_file_list)
#
# ref_to_file_list is an array ref with file name patterns (regexp) to exclude
#
sub keep_files
{
  my $list = $_[0];

  my $ex = join "|", @$list;

  for (sort keys %$files) {
    if(m#^($ex)$#) {
      my $f = fname($_);
      $files_to_keep->{$f} = 1;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_micro()
#
# Remove all files not needed to run the installer itself. Basicallly whis removes
# the rpms from the repository in the image.
#
sub prepare_micro
{
  exclude_files [
    (map { "suse/$_" } @boot_archs, "i586", "noarch"),
    "(aarch64|ppc64le|s390x|x86_64)", "src", "nosrc", "noarch",
    "docu",
    "ls-lR\\.gz",
    "INDEX\\.gz",
    "ARCHIVES\\.gz",
    "ChangeLog",
    "updates",
    "linux",
    "autorun.inf",
    ".*\\.ico",
    ".*\\.exe",
    "Module-.*",
    "Product-.*",
    "repodata",
    "install",
    "AppStream",
    "BaseOS",
    "Packages",
    ".treeinfo",
  ];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_nano()
#
# Remove all files not needed to run linuxrc. This will also remove the
# installation system (as for the network iso).
#
sub prepare_nano
{
  prepare_micro;

  exclude_files [
    "boot/.*/.*\\.rpm",
    "boot/.*/bind",
    "boot/.*/common",
    "boot/.*/gdb",
    "boot/.*/rescue",
    "boot/.*/root",
    "boot/.*/sax2",
    "boot/.*/libstoragemgmt",
    "boot/.*/branding",
    # be more specific, else we'll kill the grub themes
    "boot/[^/]*/openSUSE",
    "boot/[^/]*/SLES",
    "boot/[^/]*/SLED",
    "boot/.*/.*-xen",
    "control\\.xml",
    "gpg-.*",
    "NEWS",
    "license\\.tar\\.gz",
    "(|.*/)directory\\.yast",
    "suse",
    "(aarch64|ppc64le|s390x|x86_64)",
    "repodata",
    "images/install.img",
    # keep LiveOS dir, so we still know this is a Live medium
    "LiveOS/squashfs.img",
  ];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_pico()
#
# Remove all files not needed to run the boot loader. This makes only sense
# for testing.
#
sub prepare_pico
{
  prepare_nano;

  exclude_files [
    "boot/.*/linux",
    "boot/.*/initrd",
    "boot/.*/biostest",
    "boot/.*/en\\.tlk",
  ];

  if(!$opt_efi) {
    exclude_files [
      "boot/.*/efi",
      "boot/.*/grub2.*",
      "EFI",
    ]
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# set_mkisofs_metadata()
#
# Construct iso metadata from existing iso and product files found in the
# source image.
#
sub set_mkisofs_metadata
{
  my $media;
  my $vol_id = $opt_volume;

  # first, try using old values, if we remaster an image
  if($sources[0]{type} eq 'iso') {
    if(open my $f, "isoinfo -d -i $sources[0]{real_name} 2>/dev/null |") {
      while(<$f>) {
        $vol_id = $1 if !defined $vol_id && /^Volume id:\s*(.*?)\s*$/ && $1 ne "" && $1 ne "CDROM";
        $opt_vendor = $1 if !defined $opt_vendor && /^Publisher id:\s*(.*?)\s*$/ && $1 ne "";
        $opt_application = $1 if !defined $opt_application && /^Application id:\s*(.*?)\s*$/ && $1 ne "";
        $opt_preparer = $1 if !defined $opt_preparer && /^Data preparer id:\s*(.*?)\s*$/ && $1 ne "";
      }
      close $f;
      undef $opt_application if $opt_application =~ /^GENISOIMAGE/;
    }
  }

  # else, build new ones based on media.1 dir
  for (sort sort keys %$files) {
    $media = $_, last if /^media\.\d+$/;
  }

  if($media) {
    if(open my $f, "<", fname("$media/build")) {
      my $x = <$f>;
      close $f;
      chomp $x;
      my $m .= $1 if $media =~ /\.(\d+)$/;
      if(!defined $opt_application) {
        $opt_application = $x;
        $opt_application .= "-Media$m" if defined $m;
      }
      if(!defined $vol_id) {
        $vol_id = $x;
        $vol_id =~ s/\-?Build.*$//;
        # 25 = 32 - length("-Media1")
        $vol_id = trim_volume_id $vol_id, 25;
        $vol_id .= "-Media$m" if defined $m;
      }
    }

    if(open my $f, "<", fname("$media/media")) {
      my $x = <$f>;
      my $v = <$f>;
      close $f;
      chomp $x;
      chomp $v;
      $x = "SUSE LINUX GmbH" if $x eq "SUSE" || $x eq "openSUSE";
      $opt_vendor = $x if $x ne "" && !defined $opt_vendor;
      if(!defined $vol_id) {
        $vol_id = $v;
        $vol_id =~ s/\-?Build.*$//;
      }
    }

    if(open my $f, "<", fname("$media/info.txt")) {
      local $/;
      my $x = <$f>;
      close $f;
      if($x =~ /\n([^\n]+)\n\s*$/) {
        $x = $1;
        $x =~ s/^\s*|\s*$//g;
        $x =~ s/\.//;
        $opt_preparer = $x if $x ne "" && !defined $opt_preparer;
      }
    }
  }

  $vol_id = trim_volume_id $vol_id;

  print "volume id: $vol_id\n" if $vol_id && $opt_verbose >= 1;

  my $vol_id_from_config;

  if(open my $f, "<", fname("boot/grub2/grub.cfg")) {
    while(<$f>) {
      if(/\sroot=live:CDLABEL=(\S+)/) {
        $vol_id_from_config = trim_volume_id $1;
        last;
      }
    }
    close $f;
  }

  if(open my $f, "<", fname("isolinux/isolinux.cfg")) {
    while(<$f>) {
      if(/append .* inst.stage2=hd:LABEL=(\S+)/) {
        $vol_id_from_config = trim_volume_id $1;
        last;
      }
    }
    close $f;
  }

  print "volume id (boot config): $vol_id_from_config\n" if $vol_id_from_config && $opt_verbose >= 1;

  my $vol_id_from_initrd;

  if($media_style eq 'suse' && $media_variant eq 'live') {
    unpack_orig_initrd if !$orig_initrd;
    die "initrd unpacking failed\n" if !$orig_initrd;

    if(-d "$orig_initrd/etc/cmdline.d") {
      my $config = `cat $orig_initrd/etc/cmdline.d/*.conf`;
      $vol_id_from_initrd = $1 if $config =~ /^root=live:(?:CD)?LABEL=(\S+)/m;
    }
  }

  print "volume id (initrd): $vol_id_from_initrd\n" if $vol_id_from_initrd && $opt_verbose >= 1;

  $vol_id ||= $vol_id_from_config;
  $vol_id ||= $vol_id_from_initrd;

  if($vol_id_from_config && $vol_id ne $vol_id_from_config) {
    print "Warning: volume id \"$vol_id\" does not match expected volume id from boot config \"$vol_id_from_config\".\n";
    $vol_id = $opt_volume ? $vol_id : $vol_id_from_config;
    print "Using volume id \"$vol_id\".\n";
  }

  if($vol_id_from_initrd && $vol_id ne $vol_id_from_initrd) {
    print "Warning: volume id \"$vol_id\" does not match expected volume id from initrd \"$vol_id_from_initrd\".\n";
    $vol_id = $opt_volume ? $vol_id : $vol_id_from_initrd;
    print "Using volume id \"$vol_id\".\n";
  }

  $opt_volume = $vol_id;

  # if nothing worked, put in some defaults
  $opt_vendor = "mkmedia $VERSION" if !defined $opt_vendor;
  $opt_preparer = "mkmedia $VERSION" if !defined $opt_preparer;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# trim_volume_id(vol_id, max_len:32)
#
# Try to trim volume id to maximum length.
#
# -  vol_id: volume id
# - max_len: maximum length (default 32)
#
# Note that the returned volume id is not guaranteed to be shorter than the
# maximum length. This function only *tries* to achieve it.
#
# Return volume id.
#
sub trim_volume_id
{
  my $vol_id = $_[0];
  my $max_len = $_[1] || 32;

  # strip off '-FOO' parts until length is below maximum
  while(length $vol_id > $max_len && $vol_id =~ s/\-([^\-])*$//) { }

  return $vol_id;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Add a file's check sum to /content.
#
# add_to_content_file($content, $type, $file_name, $pattern)
#
sub add_to_content_file
{
  my $cont = shift;
  my $type = shift;
  my $name = shift;
  my $pattern = shift;

  my $match = $name;
  $name =~ s#.*/## if $type eq "META";

  if($match =~ m#$pattern# && !$cont->{$type}{$name}{new}) {
    my $digest = Digest::SHA->new($cont->{bits});
    my $f = fname($type eq "META" ? "suse/setup/descr/$name" : $name);
    if(-f $f) {
      # print "$name\n";
      $digest->addfile($f);
      my $sum = $digest->hexdigest;
      $cont->{$type}{$name}{new} = "$cont->{bits} $sum";
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_content_or_checksums()
#
# Create a new /content or /CHECKSUMS file and return 1 if it is different
# from the existing one (meaning it needs to be re-signed).
#
sub update_content_or_checksums
{
  return 0 unless $media_style eq 'suse' && $media_variant eq 'install';
  return $has_content ? update_content : update_checksums;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_content()
#
# Create a new /content file and return 1 if it is different from the
# existing one.
#
sub update_content
{
  my $changed = 0;

  # don't modify content if it doesn't exist or we're not going to re-sign it
  return $changed if !$has_content || !$opt_sign;

  my $content_file = fname "content";

  my $cont;

  # first, read file
  # ($content_file may be undefined - which is ok)
  if(open(my $f, $content_file)) {
    while(<$f>) {
      next if /^\s*$/;
      if(/^((META|HASH|KEY)\s+SHA(\d+)\s+)(\S+)(\s+(\S+))/) {
        my $type = $2;
        my $bits = $3;
        my $sum = "\L$4";
        my $name = $6;
        $cont->{bits} = $bits if !$cont->{bits};
        $cont->{$type}{$name}{old} = "$bits $sum";

        add_to_content_file $cont, $type, $name, '^';
      }
      else {
        $cont->{head} .= $_;
      }
    }
    close $f;
  }

  $cont->{bits} = 256 if !$cont->{bits};

  # then, adjust file list
  for (sort keys %$files) {
    next if m#directory\.yast$#;

    add_to_content_file $cont, "KEY", $_, '^gpg-pubkey-';
    add_to_content_file $cont, "HASH", $_, '^license.tar.gz$';
    add_to_content_file $cont, "HASH", $_, '^control.xml$';
    add_to_content_file $cont, "HASH", $_, '^boot/[^/]+/[^/]+$';
    add_to_content_file $cont, "HASH", $_, '^boot/.+/initrd[^/.]*$';
    add_to_content_file $cont, "HASH", $_, '^boot/.+/linux[^/.]*$';
    add_to_content_file $cont, "HASH", $_, '^docu/RELEASE-NOTES[^/]*$';
    add_to_content_file $cont, "META", $_, '^suse/setup/descr/[^/]+$';
    add_to_content_file $cont, "HASH", $_, '^images/[^/]+\.(xz|xml)$';
  }

  # print Dumper($cont);

  # compare new and old file checksums
  for my $type (qw (META HASH KEY)) {
    for (keys %{$cont->{$type}}) {
      if($cont->{$type}{$_}{new} ne $cont->{$type}{$_}{old}) {
        # print "changed: $_\n";
        $changed = 1;
        last;
      }
    }
    last if $changed;
  }

  # if something changed, write new file
  if($changed) {
    my $n = copy_or_new_file "content";

    if($n) {
      if(open my $f, ">", $n) {
        print $f $cont->{head};

        for my $type (qw (META HASH KEY)) {
          for (sort keys %{$cont->{$type}}) {
            next if !$cont->{$type}{$_}{new};
            printf $f "%-4s SHA%s  %s\n", $type, $cont->{$type}{$_}{new}, $_;
          }
        }

        close $f;
      }
    }
  }

  return $changed;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_checksums()
#
# Create a new /CHECKSUMS file and return 1 if it is different from the
# existing one.
#
sub update_checksums
{
  my $changed = 0;

  # don't modify CHECKSUMS if it doesn't exist or we're not going to re-sign it
  return $changed if $has_content || !$opt_sign;

  my $content_file = fname "CHECKSUMS";

  my $cont;

  # $cont is modelled after $cont in update_content() so that
  # it can be passed to add_to_content_file()
  $cont->{bits} = 256;

  # first, read existing file
  # ($content_file may be undefined - which is ok)
  if(open(my $f, $content_file)) {
    while(<$f>) {
      next if /^\s*$/;
      if(/^(\S+)\s+(\S+)/) {
        my $sum = "\L$1";
        my $name = $2;

        $cont->{HASH}{$name}{old} = "$cont->{bits} $sum";
        add_to_content_file $cont, "HASH", $name, '^';
      }
    }
    close $f;
  }

  # then, pick the files we want to be checksummed
  for (sort keys %$files) {
    next if m#directory\.yast$#;

    add_to_content_file $cont, "HASH", $_, '^boot/';
    add_to_content_file $cont, "HASH", $_, '^media\.1/';
    add_to_content_file $cont, "HASH", $_, '^docu/';
    add_to_content_file $cont, "HASH", $_, '^EFI/';
  }

  # compare new and old file checksums
  for (keys %{$cont->{HASH}}) {
    if($cont->{HASH}{$_}{new} ne $cont->{HASH}{$_}{old}) {
      $changed = 1;
      last;
    }
  }

  # if something changed, write new file
  if($changed) {
    my $n = copy_or_new_file "CHECKSUMS";

    if($n) {
      if(open my $f, ">", $n) {
        for (sort keys %{$cont->{HASH}}) {
          next if !$cont->{HASH}{$_}{new};
          my $hash = $cont->{HASH}{$_}{new};
          $hash = (split ' ', $hash)[1];
          printf $f "%s  %s\n", $hash, $_;
        }

        close $f;
      }
    }
  }

  return $changed;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_treeinfo()
#
# Create a new .treeinfo file and return 1 if it is different from the
# existing one.
#
sub update_treeinfo
{
  my $changed = 0;

  my $treeinfo_file = fname ".treeinfo";

  my $treeinfo = read_ini $treeinfo_file;

  if(!defined $treeinfo) {
    print "no .treeinfo\n";

    return;
  }

  # add variants
  for my $p_entry (@{$product_db->{list}}) {
    my $p = $p_entry->{name};
    if(!$treeinfo->{"variant-$p"}) {
      $changed = 1;
      $treeinfo->{"variant-$p"} = {
        id => $p,
        name => $p,
        packages => "$p/Packages",
        repository => $p,
        type => "variant",
        uid => $p,
      };
    }
  }

  my $checksum_files;
  my $platforms;
  for my $k (sort keys %$treeinfo) {
    if($k =~ /^images-(.*)/) {
      $platforms .= "," if $platforms;
      $platforms .= $1;
      for my $f (keys %{$treeinfo->{$k}}) {
        if(fname $treeinfo->{$k}{$f}) {
          $checksum_files->{$treeinfo->{$k}{$f}} = 1;
        }
        else {
          $changed = 1;
          delete $treeinfo->{$k}{$f};
        }
      }
    }
  }

  if($treeinfo->{stage2}{mainimage}) {
    $checksum_files->{$treeinfo->{stage2}{mainimage}} = 1;
  }

  my $variant;
  my $variants;
  for my $k (sort keys %$treeinfo) {
    if($k =~ /^variant-(.*)/) {
      if($variant) {
        $variants .= ",$1";
      }
      else {
        $variant = $variants = $1;
      }
    }
  }

  # FIXME - in skelcd-installer-xxx!!!
  $treeinfo->{general}{name} = "$treeinfo->{general}{family} $treeinfo->{general}{version}";

  # FIXME - in skelcd-installer-xxx!!!
  if($treeinfo->{general}{timestamp}) {
    $treeinfo->{general}{timestamp} =~ s/\..*//;
  }

  if(!$treeinfo->{general}{platforms}) {
    $changed = 1;
    $treeinfo->{general}{platforms} = $platforms;
  }

  if(!$treeinfo->{tree}) {
    $changed = 1;
    $treeinfo->{tree} = {
      arch => $treeinfo->{general}{arch},
      build_timestamp => $treeinfo->{general}{timestamp},
      platforms => $platforms,
    };
  }

  if(!$treeinfo->{release}) {
    $changed = 1;
    $treeinfo->{release} = {
      name => $treeinfo->{general}{family},
      ### FIXME - must come from skelcd-installer-xxx!!!
      short => "Liberty",
      version => $treeinfo->{general}{version},
    };
  }

  if(!$treeinfo->{header}) {
    $changed = 1;
    $treeinfo->{header} = {
      type => "productmd.treeinfo",
      version => "1.2",
    };
  }

  if(!$treeinfo->{media}) {
    $changed = 1;
    $treeinfo->{media} = {
      discnum => "1",
      totaldiscs => "1",
    };
  }

  if($treeinfo->{general}{repository} ne $variant) {
    $changed = 1;
    $treeinfo->{general}{repository} = $variant;
  }

  if($treeinfo->{general}{packagedir} ne "$variant/Packages") {
    $changed = 1;
    $treeinfo->{general}{packagedir} = "$variant/Packages";
  }

  if($treeinfo->{general}{variant} ne $variant) {
    $changed = 1;
    $treeinfo->{general}{variant} = $variant;
  }

  if(!$treeinfo->{general}{variants}) {
    $changed = 1;
    $treeinfo->{general}{variants} = $variants;
  }

  if($treeinfo->{tree}{variants} ne $variants) {
    $changed = 1;
    $treeinfo->{tree}{variants} = $variants;
  }

  for my $checksum_file (sort keys %$checksum_files) {
    my $digest = Digest::SHA->new(256);
    my $f = fname $checksum_file;
    if(-f $f) {
      $digest->addfile($f);
      my $sum = "sha256:" . $digest->hexdigest;
      if($treeinfo->{checksums}{$checksum_file} ne $sum) {
        $changed = 1;
        $treeinfo->{checksums}{$checksum_file} = $sum;
      }
    }
  }

  # print Dumper $treeinfo;

  if($changed) {
    my $new_treeinfo = copy_or_new_file ".treeinfo";
    write_ini $new_treeinfo, $treeinfo;
  }

  # generate media.repo file if one is missing
  my $new_mediarepo = copy_or_new_file "media.repo";
  if(! -s $new_mediarepo) {
    $changed = 1;
    my $name = "$treeinfo->{general}{name}";
    # my $name = "$treeinfo->{general}{family} $treeinfo->{general}{version}";
    (my $mr = <<"    = = = = = = = =") =~ s/^ +//mg;
      [InstallMedia]
      name=$name
      mediaid=None
      metadata_expire=-1
      gpgcheck=0
      cost=500
    = = = = = = = =
    if(open my $f, ">$new_mediarepo") {
      print $f $mr;
      close $f;
    }
  }

  return $changed;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# create_sign_key()
#
# Create a temporary gpg keyring and either add the provided gpg key or
# create a temporary key.
#
sub create_sign_key
{
  my $gpg_dir = $tmp->dir();

my $c = <<"= = = = = = = =";
%no-ask-passphrase
%no-protection
%transient-key
Key-Type: RSA
Key-Length: 2048
Name-Real: mkmedia Signing Key
Name-Comment: transient key
%pubring mkmedia.pub
%secring mkmedia.sec
%commit
= = = = = = = =

  if($opt_sign_key_id) {
    # step 1: export the public key, using the supplied id - this also ensures
    #         the key exists
    # step 2: get the canonical key id and creation date from the exported blob

    $sign_key_dir = $gpg_dir = "$ENV{HOME}/.gnupg";
    die "$sign_key_dir: no such gpg directory\n" unless -d $sign_key_dir;

    my $tmp_dir = $tmp->dir();
    system "gpg --homedir=$gpg_dir --export --armor --output $tmp_dir/key.pub $sign_passwd_option '$opt_sign_key_id'  >/dev/null 2>&1";

    my $keyid;
    my $date;

    if(-f "$tmp_dir/key.pub" && open(my $p, "gpg -v -v $tmp_dir/key.pub 2>&1 |")) {
      while(<$p>) {
        $keyid = $1 if !$keyid && /^:signature packet:.*keyid\s+([0-9a-zA-Z]+)/;
        $date = $1, last if !$date && $keyid && /created\s+(\d+)/;
      }
      close $p;
    }

    if(!$keyid || !$date) {
      die "$opt_sign_key_id: failed to extract public key\n";
    }

    my $cname = sprintf "gpg-pubkey-%08x-%08x.asc", hex($keyid) & 0xffffffff, $date;
    $sign_key_pub = "$tmp_dir/$cname";
    rename "$tmp_dir/key.pub", $sign_key_pub;

    $sign_key_id = $keyid;

    print "using signing key, keyid = $sign_key_id\n";

    return;
  }

  my $key;
  my $is_gpg21;

  if($opt_sign_key) {
    $key = $opt_sign_key;
    $key =~ s/^~/$ENV{HOME}/;
    die "$key: no such key file\n" unless -f $key;
  }
  else {
    if(open my $p, "| cd $gpg_dir ; gpg --homedir=$gpg_dir --batch --armor --debug-quick-random --gen-key - 2>/dev/null") {
      print $p $c;
      close $p;
    }
    $key = "$gpg_dir/mkmedia.sec";
    if(!-f $key) {
      $key = "$gpg_dir/mkmedia.pub";
      $is_gpg21 = 1;
    }
  }

  my $keyid;
  my $date;
  my $priv;
  my $pub;

  if(open my $p, "gpg -v -v $key 2>&1 |") {
    while(<$p>) {
      $priv = 1 if /BEGIN PGP PRIVATE KEY BLOCK/;
      $pub = 1 if /BEGIN PGP PUBLIC KEY BLOCK/;
      $keyid = $1 if !$keyid && /^:signature packet:.*keyid\s+([0-9a-zA-Z]+)/;
      $date = $1, last if !$date && $keyid && /created\s+(\d+)/;
    }
    close $p;
  }

  if(($priv || ($is_gpg21 && $pub)) && $date) {
    $sign_key_dir = $gpg_dir;
    system "gpg --homedir=$gpg_dir $sign_passwd_option --import $key  >/dev/null 2>&1";

    my $cname = sprintf "gpg-pubkey-%08x-%08x.asc", hex($keyid) & 0xffffffff, $date;
    $sign_key_pub = "$gpg_dir/$cname";
    system "gpg --homedir=$gpg_dir $sign_passwd_option --export --armor --output $sign_key_pub >/dev/null 2>&1";

    $sign_key_id = $keyid;

    if($opt_sign_key) {
      print "using signing key, keyid = $sign_key_id\n";
    }
    else {
      print "transient signing key created, keyid = $sign_key_id\n";
    }
  }
  else {
    if($pub) {
      die "$key: signing key is not a private key\n";
    }
    else {
      die "$key: signing key not usable\n";
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_sign_key()
#
# Add public part of mkmedia sign key to image so it's used by the installer.
#
sub add_sign_key
{
  return if !$sign_key_pub;

  my $tmp_dir = $tmp->dir();

  if($initrd_installkeys) {
    # old style, gpg key ring
    system "cp $initrd_installkeys $tmp_dir/installkey.gpg";
    system "gpg --homedir=$sign_key_dir --batch --no-default-keyring --ignore-time-conflict --ignore-valid-from --keyring $tmp_dir/installkey.gpg --import $sign_key_pub 2>/dev/null";
    unlink "$tmp_dir/installkey.gpg~";
  }
  else {
    # new style, directory of gpg keys
    system "mkdir -p $tmp_dir/usr/lib/rpm/gnupg/keys";
    system "cp $sign_key_pub $tmp_dir/usr/lib/rpm/gnupg/keys";
  }

  push @opt_initrds, $tmp_dir;

  my $name = $sign_key_pub;
  $name =~ s#.*/##;

  my $k = copy_or_new_file "$name";
  system "cp $sign_key_pub $k";

  print "signing key added to image and initrd\n" if $opt_verbose >= 1;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# sign_content_or_checksums()
#
# Re-sign 'content' or 'CHECKSUMS' with our own key if we modified it.
#
sub sign_content_or_checksums
{
  return if !$sign_key_dir;

  my $name = $has_content ? "content" : "CHECKSUMS";

  my $c = copy_file $name;
  return if !defined $c;

  my $k = copy_or_new_file "$name.key";

  copy_file "$name.asc";

  system "cp $sign_key_pub $k";

  print "re-signing '/$name'\n" if $opt_verbose >= 1;

  system "gpg --homedir=$sign_key_dir --local-user '$sign_key_id' --batch --yes --armor --detach-sign $sign_passwd_option $c";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Run 'file' system command.
#
# result = file_magic(file, pipe)
#
# -   file: the input file, or '-' if pipe is set
# -   pipe: (if set) the command to read from
# - result: everything 'file' returns
#
sub file_magic
{
  my $type = "file -b -k -L $_[0] 2>/dev/null";
  $type = "$_[1] | $type" if $_[1];

  return `$type`;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# result = fs_type(file, pipe, offset)
#
# -   file: the input file, or '-' if pipe is set
# -   pipe: (if set) the command to read from
# - offset: probe at offset
# - result: type hash
#
sub fs_type
{
  my $file = $_[0];
  my $pipe = $_[1];
  my $offset = $_[2] || 0;
  my $type = { };

  if($pipe) {
    if(open my $fd, "$pipe |") {
      my $buf;
      # 1 MiB seems to be the minimum for blkid to work
      my $i = read $fd, $buf, 1024*1024;
      close $fd;
      $file = $tmp->file();
      if(open my $fd, ">", $file) {
        syswrite $fd, $buf;
        close $fd;
      }
      else {
        undef $file;
      }
    }
  }

  if($file) {
    my $blkid = `blkid --offset '$offset' -p '$file' 2>/dev/null`;
    if($blkid =~ /\bUSAGE="filesystem"/ && $blkid =~ /\bTYPE="([^"]*)"/) {
      $type->{fs} = $1;
    }

    if($blkid =~ /\bUSAGE="crypto"/ && $blkid =~ /\bTYPE="([^"]*)"/) {
      $type->{crypto} = $1;
    }

    my $parti = `parti --json $file 2>/dev/null`;
    $parti = decode_json($parti) if $parti;

    if($parti->{gpt_primary}) {
      $type->{gpt} = $parti->{gpt_primary};
    }
    elsif($parti->{mbr}) {
      $type->{mbr} = $parti->{mbr};
    }
    $type->{eltorito} = $parti->{eltorito} if $parti->{eltorito};
  }

  return $type;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Get archive type;
#
# type = get_archive_type(file)
#
# - file: the archive name
# - type: something like 'tar.xz' or undef if the archive is unsupported.
#
# The special type '[start:length]' designates a part of the file of 'length' bytes
# beginning at 'start'. A 'length' of 0 means 'to the end of file'.
#
# type can ba a comma-separated list of individual types. Which makes only sense in
# relation to types containing '[start:length]' to indicate that different parts of
# a file can have different archive types.
#
sub get_archive_type
{
  my $file = $_[0];
  my $type;
  my $cmd;

  my $orig = $file;

  if(-d $file) {
    return 'dir';
  }

  if(! (-f $file || -b _)) {
    return undef;
  }

  if(! -r $file) {
    die "$orig: not readable; you need root privileges.\n";
  }

  my @types_found;

  do {
    my $t = file_magic $file, $cmd;

    if($t =~ /^RPM/) {
      $type = "cpio.rpm$type";
    }
    elsif($t =~ /^ASCII cpio archive \(SVR4/) {
      $type = "cpiox$type";
      if(!$cmd) {
        my $cpiox_stats = unpack_cpiox undef, $file;
        my $len = $cpiox_stats ? $cpiox_stats->{bytes} : 0;
        if($len < -s $file) {
          push @types_found, "$type.[0:$len]";
          $type = ".[$len:]";
          $cmd = "dd status=none bs=$len skip=1 if='$file' 2>/dev/null";
          $file = "-";
        }
      }
    }
    elsif($t =~ /\b(cpio|tar|rar) archive/i) {
      $type = "\L$1\E$type";
    }
    elsif($t =~ / QCOW /) {
      $type = "qcow$type";
    }
    elsif($t =~ /Zip archive data|Zip data|EPUB /) {
      $type = "zip$type";
    }
    elsif($t =~ /^(bzip2|gzip|XZ|Zstandard) compressed data/) {
      my $c = "\L$1";
      $c =~ s/zstandard/zstd/;
      if($cmd) {
        $cmd .= " | $c --quiet -dc";
      }
      else {
        $cmd = "$c --quiet -dc '$file'";
      }
      $file = "-";
      $c =~ s/bzip2/bz2/;
      $c =~ s/gzip/gz/;
      $c =~ s/zstd/zst/;
      $type = ".$c$type";
    }
    else {
      my $type_added = 0;
      my $fs_type = fs_type $file, $cmd;
      my $saved_type = $type;

      if($fs_type->{fs}) {
        $type = "fs_$fs_type->{fs}$saved_type";
        $type_added = 1;
      }

      if($fs_type->{crypto}) {
        $type = "$fs_type->{crypto}$saved_type";
        $type_added = 1;
      }

      if($saved_type !~ /,/) {
        if($fs_type->{gpt}) {
          for my $p (@{$fs_type->{gpt}{partitions}}) {
            if($p->{size}) {
              my $start = $p->{first_lba} * $fs_type->{gpt}{block_size};
              my $size = $p->{size} * $fs_type->{gpt}{block_size};
              my $fs_type2 = fs_type $file, $cmd, $start;
              my $fs2 = "fs_$fs_type2->{fs}" if $fs_type2->{fs};
              $fs2 ||= $fs_type2->{crypto};
              $fs2 .= "." if $fs2 ne "";
              $type .= ",${fs2}part$p->{number}.[$start:$size]$saved_type";
              $type_added = 1;
            }
          }
        }
        if($fs_type->{mbr}) {
          for my $p (@{$fs_type->{mbr}{partitions}}) {
            if($p->{size}) {
              my $start = $p->{first_lba} * $fs_type->{mbr}{block_size};
              my $size = $p->{size} * $fs_type->{mbr}{block_size};
              my $fs_type2 = fs_type $file, $cmd, $start;
              my $fs2 = "fs_$fs_type2->{fs}" if $fs_type2->{fs};
              $fs2 ||= $fs_type2->{crypto};
              $fs2 .= "." if $fs2 ne "";
              $type .= ",${fs2}part$p->{number}.[$start:$size]$saved_type";
              $type_added = 1;
            }
          }
        }
        if($fs_type->{eltorito}) {
          my $number = 1;
          for my $p (@{$fs_type->{eltorito}{catalog}}) {
            if($p->{size}) {
              my $start = $p->{first_lba} * $fs_type->{mbr}{block_size};
              my $size = $p->{size} * $fs_type->{mbr}{block_size};
              my $fs_type2 = fs_type $file, $cmd, $start;
              my $fs2 = "fs_$fs_type2->{fs}" if $fs_type2->{fs};
              $fs2 ||= $fs_type2->{crypto};
              $fs2 .= "." if $fs2 ne "";
              $type .= ",${fs2}eltorito$number.[$start:$size]$saved_type";
              $type_added = 1;
            }
            $number++ if $p->{boot};
          }
        }
      }

      $type =~ s/^,?//;

      return undef if !$type_added;
    }
  } while($type =~ /^\./);

  push @types_found, $type;
  $type = join ",", @types_found;

  # print "$file = $type\n";

  return $type;
}


# Unpack multiple concatenated cpio archives.
#
# The archives are expected to be in cpio ASCII format ('cpio -H newc').
# Between the idividual archives an arbitrary sequence of (binary) zeros is
# allowed. (This is what the kernel allows for the initramfs image.)
#
# unpack_cpiox(dst, file, part)
#
# -  dst: the directory to unpack to; if dst is undef, don't unpack anything (just parse)
# - file: the archive file name
# - part: the part number (1 based) of a multipart archive (0 = unpack all)
#
# If dst is undef, write nothing.
#
# Return hash with two elements:
#   - bytes = size of cpiox archive (data actually read)
#   - parts = number of individual cpiox archives parsed
#
# If 'part' is != 0, 'bytes' is the end of that part (right after the 'TRAILER!!!' entry).
# If 'part' is 0, 'bytes' is the end of valid cpiox data + any trailing 0 bytes.
#
sub unpack_cpiox
{
  my $dst = shift;
  my $file = shift;
  my $part = shift() + 0;

  my $cpio_cmd = 'cpio --quiet -dmiu --sparse --no-absolute-filenames 2>/dev/null';

  # the archive number we are looking for (1 based)
  my $cnt = 1;

  # input and output file handles
  my ($f, $p);

  # data transfer buffer
  my $buf;

  # search for cpio header in input stream on next read operation
  my $sync = 0;

  # track # of written bytes (reset at start of each cpio archive)
  my $write_ofs;

  # track # of read bytes
  my $read_ofs;

  # Read # of bytes from input and write to output.
  #
  # bytes = $read_write->(len)
  # -   len: number of bytes to transfer
  # - bytes: size of data actually transferred
  #
  # This function implicitly opens a new output pipe if none is open and data
  # need to be written.
  #
  # If the $sync variable is set search the input stream for a valid cpio
  # header (and reset $sync to 0).
  #
  my $read_write = sub
  {
    my $len = $_[0];

    # nothing to do
    return $len if !$len;

    # clear buffer
    undef $buf;

    # Search for next cpio header.
    #
    # This assumes there's a number of binary zeros in the input stream
    # until the next cpio header.
    # Actually this only looks for the next non-zero data blob.
    if($sync) {
      $sync = 0;
      while(sysread($f, $buf, 1) == 1 && $buf eq "\x00") { $read_ofs++ };
      $read_ofs += length $buf;
      $len -= length $buf;
    }

    # read $len bytes
    while($len) {
      my $x = sysread $f, substr($buf, length $buf), $len;
      last if !$x;
      $read_ofs += $x;
      $len -= $x;
    };

    # In case we did read something, write it to output pipe.
    if(length $buf) {
      # Open a new pipe if needed.
      # But only if part number matches or is 0 (== all parts).
      if($dst && !$p && ($part == 0 || $part == $cnt)) {
        open $p, "| ( cd $dst ; $cpio_cmd )" or die "failed to open cpio: $!\n";
        $write_ofs = 0;
      }

      # Write data and track output size for padding calculation at the end.
      if($p) {
        syswrite $p, $buf;
        $write_ofs += length $buf;
      }
    }

    return length $buf;
  };

  # Write padding bytes (pad with 0 to full 512 byte blocks) and close
  # output pipe.
  #
  # $write_pad_and_close->()
  #
  # This also sets a sync flag indicating that we should search for the next
  # valid cpio header in the input stream.
  #
  my $write_pad_and_close = sub
  {
    if($p) {
      my $pad = (($write_ofs + 0x1ff) & ~0x1ff) - $write_ofs;
      syswrite $p, "\x00" x $pad, $pad if $pad;
      close $p;
      undef $p;
    }

    # search for next cpio header in input stream
    $sync = 1;
  };

  # open archive and get going...
  if(open $f, $file) {
    my $len;

    # We have to trace the cpio archive structure.
    # Keep going as long as there's a header.
    while(($len = $read_write->(110)) == 110) {
      my $magic = substr($buf, 0, 6);
      my $head = substr($buf, 6);

      if($magic !~ /^07070[12]$/) {
        close $f;
        return { bytes => $read_ofs - $len, parts => $cnt - 1 } if !$dst;
        die "broken cpio header\n";
      }

      my $fname_len = hex substr $buf, 94, 8;
      my $data_len = hex substr $buf, 54, 8;

      $fname_len += (2, 1, 0, 3)[$fname_len & 3];
      $data_len = (($data_len + 3) & ~3);

      $read_write->($fname_len);

      my $fname = $buf;
      $fname =~ s/\x00*$//;

      $read_write->($data_len);

      # Look for cpio archive end marker.
      # If found, close cpio process. A new process will be started at the
      # next valid cpio archive header.
      if(
        $fname eq 'TRAILER!!!' &&
        $head =~ /^0{39}10{55}b0{8}$/i
      ) {
        $write_pad_and_close->();
        # exit if we're done
        if($cnt++ == $part) {
          close $f;
          return { bytes => $read_ofs, parts => $part };
        }
      }
    }

    # we're done, close input file...
    close $f;

    # ...and output file.
    $write_pad_and_close->();

    # If $len is != 0 this means we've seen something that's not a header of
    # a cpio archive entry.
    die "invalid cpio data\n" if $len;
  }
  else {
    die "error reading cpio archive: $!\n";
  }

  return { bytes => $read_ofs, parts => $cnt - 1 };
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Unpack archive file.
#
# unpack_archive(type, file, dir, part)
#
# - type: a type string as returned by get_archive_type
# - file: the archive
# -  dir: the directory to unpack to
# - part: is the part number of a multipart archive (0 = unpack all)
#
# Sample type strings:
#
#   "dir", "tar", "tar.gz", "tar.gz.xz", "cpiox.[0:1024],cpiox.xz.gz.[1024:]"
#
# Type string is a comma-separated list of individual sub_types.
#
sub unpack_archive
{
  my $type = $_[0];
  my $file = $_[1];
  my $dir = $_[2];
  my $part = $_[3];

  $type =~ s/,.*$// if $type =~ /^fs_/;

  for my $sub_type (split /,/, $type) {
    next if $sub_type eq '';

    my $cmd;
    my $cpiox;

    if($sub_type eq 'dir') {
      $cmd = "tar -C '$file' -cf - .";
      $sub_type = 'tar';
    }
    elsif(! -r $file) {
      die "$file: not readable; you need root privileges.\n";
    }

    for (reverse split /\./, $sub_type) {
      if(/^(bz2|gz|xz|zst|rpm)$/) {
        my $c;
        if($1 eq 'gz') {
          $c = 'gzip --quiet -dc';
        }
        elsif($1 eq 'xz') {
          $c = 'xz --quiet -dc';
        }
        elsif($1 eq 'zst') {
          $c = 'zstd --quiet --force -dc';
        }
        elsif($1 eq 'bz2') {
          $c = 'bzip2 --quiet -dc';
        }
        else {
          $c = 'rpm2cpio';
        }
        if($cmd) {
          $cmd .= " | $c";
        }
        else {
          $cmd = "$c '$file'";
        }
      }
      elsif(/\[(.*):(.*)\]/) {
        my $start = ($1 ne "" ? $1 : 0) + 0;
        my $len = ($2 ne "" ? $2 : 0) + 0;
        my $args;
        $args = sprintf "bs=1 skip=%d count=%d", $start, $len;
        $args = "bs=$start skip=1" if $start && !$len;
        $args = "bs=$len count=1" if !$start && $len;
        if($cmd) {
          $cmd .= " | dd status=none $args 2>/dev/null";
        }
        else {
          $cmd = "dd status=none $args if='$file' 2>/dev/null";
        }
      }
      elsif($_ eq 'tar') {
        $cmd = "cat '$file'" if !$cmd;
        $cmd .= " | tar -C '$dir' --keep-directory-symlink -xpf - 2>/dev/null";
        last;
      }
      elsif($_ eq 'zip') {
        if(!$cmd) {
          $cmd = "unzip -qX '$file' -d '$dir'";
        }
        else {
          my $t = $tmp->file();
          $cmd .=  " > '$t' ; unzip -qX '$t' -d '$dir'";
        }
        last;
      }
      elsif($_ eq 'rar') {
        my $abs_file = abs_path $file;
        if(!$cmd) {
          $cmd = "cd '$dir' ; unrar x -idq '$abs_file'";
        }
        else {
          my $t = $tmp->file();
          $cmd .=  " > '$t' ; cd '$dir' ; unrar x -idq '$t'";
        }
        last;
      }
      elsif($_ eq '7z') {
        my $abs_file = abs_path $file;
        if(!$cmd) {
          $cmd = "cd '$dir' ; 7z x '$abs_file' > /dev/null";
        }
        else {
          my $t = $tmp->file();
          $cmd .=  " > '$t' ; cd '$dir' ; 7z x '$t' > /dev/null";
        }
        last;
      }
      elsif($_ eq 'fs_iso9660') {
        my $abs_file = abs_path $file;
        if(!$cmd) {
          $cmd = "cd '$dir' ; isoinfo -R -X -j UTF-8 -i '$abs_file' ; chmod --quiet -R u+w .";
        }
        else {
          my $t = $tmp->file();
          $cmd .=  " > '$t' ; cd '$dir' ; isoinfo -R -X -j UTF-8 -i '$t' ; chmod --quiet -R u+w .";
        }
        last;
      }
      elsif($_ eq 'cpio') {
        $cmd = "cat '$file'" if !$cmd;
        $cmd .= " | ( cd '$dir' ; cpio --quiet -dmiu --sparse --no-absolute-filenames 2>/dev/null )";
        last;
      }
      elsif($_ eq 'cpiox') {
        if(!$cmd) {
          $cmd = $file;
        }
        else {
          $cmd .= " |";
        }
        $cpiox = 1;
        last;
      }
      else {
        die "error: cannot handle '$_': command so far: \"$cmd\"\n";
      }
    }

    # cpiox = concatenated compressed cpio archives as the kernel uses for initrd
    # must be SVR4 ASCII format, with or without CRC ('cpio -H newc')
    # in this case we have to parse the cpio stream and handle the 'TRAILER!!!' entries
    if($cpiox) {
      print "unpack_cpiox($cmd)\n" if $opt_verbose >= 2;
      my $cpiox_stats = unpack_cpiox $dir, $cmd, $part;
      if($cpiox_stats && $part) {
        $part -= $cpiox_stats->{parts};
        $part = -1 if !$part;		# 0 has special meaning
      }
    }
    elsif($cmd ne "") {
      print "$cmd\n" if $opt_verbose >= 2;
      system $cmd;
    }
    else {
      die "${\(abs_path $file)}: cannot unpack archive ($type)\n";
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $string = format_array(\@list, $indentation)
#
# Return joined list values with line breaks added if it gets too long.
#
sub format_array
{
  my $ar = shift;
  my $ind = shift;
  my $x;

  for (@$ar) {
    if(!defined $x) {
      $x = (" " x $ind) . $_;
    }
    else {
      my $xx = $x;
      $xx =~ s/^.*\n//s;
      my $l1 = length($xx) + 3;
      my $l2 = length($_);
      if($l1 + $l2 > 79) {
        $x .= ",\n" . (" " x $ind);
      }
      else {
        $x .= ", ";
      }
      $x .= $_;
    }
  }

  return $x;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# get_initrd_modules()
#
# Get list of modules that are in the initrd.
#
sub get_initrd_modules
{
  return if $kernel && $kernel->{target_lib_dir} ne "";

  my $unpack_dir = $tmp->dir();

  # kernel package layout expected in initrd
  if(-d "$orig_initrd/usr/lib/modules") {
    $kernel->{target_usrmerge} = 1;
    $kernel->{target_lib_dir} = "usr/lib";
  }
  else {
    $kernel->{target_usrmerge} = 0;
    $kernel->{target_lib_dir} = "lib";
  }

  if(-l "$orig_initrd/modules") {
    $_ = readlink "$orig_initrd/modules";
    if(m#/modules/([^/]+)#) {
      $kernel->{orig_version} = $1;
    }
  }
  elsif(opendir my $d, "$orig_initrd/$kernel->{target_lib_dir}/modules") {
    for my $v (readdir $d) {
      $kernel->{orig_version} = $v if $v =~ /^\d/;
    }
    closedir $d;
  }

  die "oops, incompatible initrd layout\n" unless $kernel->{orig_version};

  # Note:
  #   if($kernel->{initrd_layout} == 'install') is atm the same as if($initrd_has_parts)
  #   There might turn up other layouts in future, though.
  #
  if(-f "$orig_initrd/parts/00_lib") {
    $kernel->{initrd_layout} = 'install';
    rmdir $unpack_dir;
    if(system "unsquashfs -n -d $unpack_dir $orig_initrd/parts/00_lib >/dev/null 2>&1") {
      die "parts/00_lib: failed to unpack squashfs image - squashfs tools too old?\n";
    }
    $orig_initrd_00_lib = $unpack_dir;
  }
  else {
    $kernel->{initrd_layout} = 'live';
  }

  File::Find::find({
    wanted => sub {
      return if -l;	# we don't want links
      if(m#([^/]+)${kext_regexp}$#) {
        $kernel->{initrd_modules}{$1} = 1;
      }
      if(m#/module\.config$#) {
        $kernel->{initrd_module_config} = $_;
      }
    },
    no_chdir => 1
  }, "$orig_initrd/$kernel->{target_lib_dir}/modules/$kernel->{orig_version}", $unpack_dir);

  die "no initrd modules?\n" if !$kernel->{initrd_modules};
  die "no module config?\n" if $kernel->{initrd_layout} eq 'install' && !$kernel->{initrd_module_config};
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# unpack_kernel_rpms()
#
# Unpack all provided kernel packages in a temporary location.
#
sub unpack_kernel_rpms
{
  $kernel->{dir} = $tmp->dir();

  for (@opt_kernel_rpms) {
    my $type = get_archive_type $_;
    die "$_: don't know how to unpack this\n" if !$type;
    unpack_archive $type, $_, $kernel->{dir};
  }

  # if kernel and firmware have different ideas about usrmerge, move firmware files to match kernel package layout
  if(! -l "$kernel->{dir}/lib") {
    if(-d "$kernel->{dir}/lib/modules" && -d "$kernel->{dir}/usr/lib/firmware") {
      system "tar -C $kernel->{dir}/usr/lib -cf - firmware | tar -C $kernel->{dir}/lib --keep-directory-symlink -xpf -";
      system "rm -rf $kernel->{dir}/usr/lib/firmware";
      rmdir "$kernel->{dir}/usr/lib";
      rmdir "$kernel->{dir}/usr";
    }
    elsif(-d "$kernel->{dir}/usr/lib/modules" && -d "$kernel->{dir}/lib/firmware") {
      system "tar -C $kernel->{dir}/lib -cf - firmware | tar -C $kernel->{dir}/usr/lib --keep-directory-symlink -xpf -";
      system "rm -rf $kernel->{dir}/lib/firmware";
      rmdir "$kernel->{dir}/lib";
    }
  }

  # kernel package layout in new kernel rpms
  if(-d "$kernel->{dir}/usr/lib/modules") {
    $kernel->{usrmerge} = 1;
    $kernel->{lib_dir} = "usr/lib";
  }
  else {
    $kernel->{usrmerge} = 0;
    $kernel->{lib_dir} = "lib";
  }

  my $lib_dir = $kernel->{lib_dir};
  my $target_lib_dir = $kernel->{target_lib_dir};

  my $kernel_location;
  my $kernel_name_suffix;

  if(-d "$kernel->{dir}/boot" ) {
    my $version = (glob "$kernel->{dir}/boot/System.map-*")[0];
    if($version =~ m#/boot/System.map-([^/]+)#) {
      $kernel->{version} = $1;
      $kernel_location = "boot";
      $kernel_name_suffix = "-$kernel->{version}";
    }
  }
  else {
    my $version = (glob "$kernel->{dir}/$lib_dir/modules/*/System.map")[-1];
    if($version =~ m#/$lib_dir/modules/([^/]+)/#) {
      $kernel->{version} = $1;
      $kernel_location = "$lib_dir/modules/$1";
      $kernel_name_suffix = "";
    }
  }

  if(! $kernel->{version}) {
    die "Couldn't determine kernel version. No kernel package?\n";
  }

  # kernel image names, per architecture
  #
  #   aarch64:	Image, Image-*; vmlinux-*
  #   i586:	vmlinuz, vmlinuz-*; vmlinux-*
  #   ppc64le:	vmlinux, vmlinux-*
  #   s390x:	image, image-*; vmlinux-*
  #   x86_64:	vmlinuz, vmlinuz-*; vmlinux-*

  for my $name ( qw ( vmlinuz Image image vmlinux ) ) {
    if( -f "$kernel->{dir}/$kernel_location/$name$kernel_name_suffix" ) {
      $kernel->{image} = "$kernel->{dir}/$kernel_location/$name$kernel_name_suffix";
      $kernel->{name} = $name;
      last;
    }
  }

  die "no module dir?\n" if $kernel->{version} eq "";
  die "no kernel?\n" if !$kernel->{image};

  # if no firmware packages were specified and we have to build a completely
  # new initrd, keep existing firmware files
  if($opt_rebuild_initrd && ! -d "$kernel->{dir}/$lib_dir/firmware") {
    $kernel->{keep_firmware} = 1;
    if($kernel->{initrd_layout} eq 'install') {
      if($orig_initrd_00_lib && -d "$orig_initrd_00_lib/$target_lib_dir/firmware") {
        print "kernel firmware: keep existing version\n";
        system "cp -r $orig_initrd_00_lib/$target_lib_dir/firmware $kernel->{dir}/$lib_dir";
      }
      else {
        print "kernel firmware: original firmware files missing\n";
      }
    }
    else {
      if(-d "$orig_initrd/$target_lib_dir/firmware") {
        print "kernel firmware: keep existing version\n";
        system "cp -r $orig_initrd/$target_lib_dir/firmware $kernel->{dir}/$lib_dir";
      }
      else {
        print "kernel firmware: original firmware files missing\n";
      }
    }
  }

  for (glob "$kernel->{dir}/$lib_dir/modules/*") {
    s#.*/##;
    next if $_ eq $kernel->{version};
    print "warning: kmp version mismatch, adjusting: $_ --> $kernel->{version}\n";
    system "tar -C '$kernel->{dir}/$lib_dir/modules/$_' -cf - . | tar -C '$kernel->{dir}/$lib_dir/modules/$kernel->{version}' --keep-directory-symlink -xf -";
  }

=head
  # compat symlink needed for depmod
  if($kernel->{usrmerge}) {
    symlink("$lib_dir", "$kernel->{dir}/lib");
  }
  else {
    mkdir "$kernel->{dir}/usr", 0755;
    symlink("../lib", "$kernel->{dir}/usr/lib");
  }
=cut

  run_depmod $kernel->{dir}, $kernel->{version};

=head
  if($kernel->{usrmerge}) {
    unlink "$kernel->{dir}/lib";
  }
  else {
    unlink "$kernel->{dir}/usr/lib";
    rmdir "$kernel->{dir}/usr";
  }
=cut

  if(! -s "$kernel->{dir}/$lib_dir/modules/$kernel->{version}/modules.dep") {
    # squashfs is randomly picked, assuming it will always exist
    my $fmt = (glob "$kernel->{dir}/$lib_dir/modules/$kernel->{version}/kernel/fs/squashfs/squashfs.*")[0];
    $fmt =~ s#.*/squashfs##;
    $fmt .= " " if $fmt;

    die "failed to generate modules.dep - maybe kmod package too old to handle ${fmt}module format?\n";
  }

  if($opt_verbose >= 1) {
    my $u = $kernel->{target_usrmerge} ? " (usrmerge)" : "";
    print "original kernel: $kernel->{orig_version}$u\n";
    $u = $kernel->{usrmerge} ? " (usrmerge)" : "";
    print "new kernel: $kernel->{version}$u\n";
  }

  # print Dumper($kernel);
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# build_module_list()
#
# Build list of modules to include in the new initrd.
#
# This is based on the list of modules in the original initrd minus modules
# no longer exist plus modules needed to fulfill all module dependencies.
#
sub build_module_list
{
  my %mods_remove;

  for my $m (@opt_kernel_modules) {
    for (split /,/, $m) {
      s/${kext_regexp}$//;
      if(s/^-//) {
        $mods_remove{$_} = 1;
      }
      else {
        $kernel->{initrd_modules}{$_} = 2 if !$kernel->{initrd_modules}{$_};
      }
    }
  }

  my $lib_dir = $kernel->{lib_dir};
  my $target_lib_dir = $kernel->{target_lib_dir};

  die "no modules.dep\n" if !open my $f, "$kernel->{dir}/$lib_dir/modules/$kernel->{version}/modules.dep";

  # get module paths
  for (<$f>) {
    my @i = split;
    $i[0] =~ s/:$//;
    # older modutils put the full path into modules.dep
    # so remove the "/lib/modules/VERSION/" part if it exists
    @i = map { s#^/lib/modules/([^/]+)/##; $_ } @i;
    if($i[0] =~ m#([^/]+)${kext_regexp}$#) {
      $kernel->{modules}{$1} = $i[0];
      # resolve module deps
      if($kernel->{initrd_modules}{$1} && @i > 1) {
        shift @i;
        for my $m (@i) {
          if($m =~ m#([^/]+)${kext_regexp}$#) {
            $kernel->{initrd_modules}{$1} = 3 if !$kernel->{initrd_modules}{$1};
          }
        }
      }
    }
  }

  close $f;

  $kernel->{new_dir} = $tmp->dir();

  chmod 0755, $kernel->{new_dir};

  if($kernel->{initrd_layout} eq 'install') {
    File::Path::make_path "$kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}/initrd";
  }
  else {
    File::Path::make_path "$kernel->{new_dir}/$target_lib_dir";
  }

  for (sort keys %{$kernel->{initrd_modules}}) {
    if($kernel->{modules}{$_} && !$mods_remove{$_}) {
      if($kernel->{initrd_layout} eq 'install') {
        system "cp $kernel->{dir}/$lib_dir/modules/$kernel->{version}/$kernel->{modules}{$_} $kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}/initrd";
      }
      else {
        system "cd $kernel->{dir}/$lib_dir && cp --parents modules/$kernel->{version}/$kernel->{modules}{$_} $kernel->{new_dir}/$target_lib_dir";
      }
      push @{$kernel->{added}}, $_ if $kernel->{initrd_modules}{$_} > 1;
    }
    else {
      push @{$kernel->{missing}}, $_;
    }
  }

  # copy modules.order & modules.builtin

  if(-f "$kernel->{dir}/$lib_dir/modules/$kernel->{version}/modules.builtin") {
    system "cp -f $kernel->{dir}/$lib_dir/modules/$kernel->{version}/modules.builtin{,.modinfo} $kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}/";
  }

  if($kernel->{initrd_layout} eq 'install') {
    if(open my $f, "$kernel->{dir}/$lib_dir/modules/$kernel->{version}/modules.order") {
      if(open my $w, ">$kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}/modules.order") {
        while(<$f>) {
          chomp;
          s#.*/#initrd/#;
          print $w "$_\n" if -f "$kernel->{new_dir}/$lib_dir/modules/$kernel->{version}/$_";
        }
        close $w;
      }
      close $f;
    }
  }
  else {
    system "cp $kernel->{dir}/$lib_dir/modules/$kernel->{version}/modules.order $kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}";
  }

=head
  # compat symlink needed for depmod
  if($kernel->{target_usrmerge}) {
    symlink("$target_lib_dir", "$kernel->{new_dir}/lib");
  }
  else {
    mkdir "$kernel->{new_dir}/usr", 0755;
    symlink("../lib", "$kernel->{new_dir}/usr/lib");
  }
=cut

  if($opt_no_compression->{modules}) {
    my $dir = "$kernel->{new_dir}/$target_lib_dir/modules";

    if(-d $dir) {
      print "uncompressing kernel modules...\n";

      system "find $dir -type f -name \\*.ko.zst -exec zstd -d --quiet --rm '{}' \\;";
      system "find $dir -type f -name \\*.ko.xz -exec xz -d '{}' \\;";
      system "find $dir -type f -name \\*.ko.gz -exec gzip -d '{}' \\;";
    }
  }

  run_depmod $kernel->{new_dir}, $kernel->{version};

=head
  if($kernel->{target_usrmerge}) {
    unlink "$kernel->{new_dir}/lib";
  }
  else {
    unlink "$kernel->{new_dir}/usr/lib";
    rmdir "$kernel->{new_dir}/usr";
  }
=cut

  # now get firmware files

  my %fw;

  File::Find::find({
    wanted => sub {
      return if -l || ! -f;	# no links, only files
      if(m#([^/]+)${kext_regexp}$#) {
        my @l;
        chomp(@l = `modinfo -F firmware $_`);

        s#.*/##;
        s#${kext_regexp}$##;

        $fw{$_} = [ @l ] if @l;
      }
    },
    no_chdir => 1
  }, "$kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}");

  my $kv = $kernel->{version};
  my $fw_dir = "$kernel->{dir}/$lib_dir/firmware";

  my %fw_ok;
  my %fw_missing;

  for my $m (sort keys %fw) {
    for my $fw (@{$fw{$m}}) {
      my $ok = 0;
      for my $f (<$fw_dir/$fw $fw_dir/$kv/$fw $fw_dir/$fw.xz $fw_dir/$kv/$fw.xz $fw_dir/$fw.zst $fw_dir/$kv/$fw.zst>) {
        if(-r $f) {
          $f =~ s#^$fw_dir/##;
          system "install -m 644 -D '$fw_dir/$f' '$kernel->{new_dir}/$target_lib_dir/firmware/$f'\n";
          $fw_ok{$f} = 1;
          $ok = 1;
        }
      }
      if(!$ok) {
        $fw_missing{$fw} = 1;
        print "missing firmware: $fw ($m.ko)\n" if $opt_verbose >= 1;
      }
    }
  }

  if($opt_no_compression->{firmware}) {
    my $dir = "$kernel->{new_dir}/$target_lib_dir/firmware";

    if(-d $dir) {
      print "uncompressing kernel firmware...\n";

      # rename symlinks
      for my $suffix ("zst", "xz", "gz") {
        system "find $dir -type l -name \\*.$suffix -exec rename -sl .$suffix '' '{}' \\; -exec rename -l .$suffix '' '{}' \\;";
      }

      system "find $dir -type f -name \\*.zst -exec zstd -d --quiet --rm '{}' \\;";
      system "find $dir -type f -name \\*.xz -exec xz -d '{}' \\;";
      system "find $dir -type f -name \\*.gz -exec gzip -d '{}' \\;";

      my $broken = `find $dir -follow -type l`;
      print STDERR "firmware uncompressing left broken symlinks:\n$broken\n" if $broken ne "";
    }
  }

  printf "kernel firmware: %d/%d files updated\n", scalar(keys %fw_ok), scalar(keys %fw_ok) + scalar(keys %fw_missing);

  # print Dumper(\%fw);

  # adjust module.config file

  if(open my $f, $kernel->{initrd_module_config}) {
    $kernel->{module_config} = [ <$f> ];
    close $f;

    # print "got it\n";
    # FIXME: adjust config

    open my $f, ">$kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}/initrd/module.config";
    print $f @{$kernel->{module_config}};
    close $f;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_modules_to_initrd()
#
# Add new modules to initrd.
#
sub add_modules_to_initrd
{
  my $tmp_dir;

  my $lib_dir = $kernel->{lib_dir};
  my $target_lib_dir = $kernel->{target_lib_dir};

  if($initrd_has_parts) {
    $tmp_dir = $tmp->dir();

    mkdir "$tmp_dir/parts", 0755;

    my $p = sprintf "%02u_lib", $initrd_has_parts++;
    # XX_lib contains kernel modules - replace the original one if we are
    # going to rebuild the initrd anyway
    $p = "00_lib" if $opt_rebuild_initrd;

    File::Path::make_path "$tmp_dir/$target_lib_dir/modules/$kernel->{version}/initrd";

    my @base_modules = qw (loop squashfs lz4_decompress xxhash zstd_decompress);

    if(-f "$orig_initrd/.base_modules") {
      @base_modules = split ' ', `cat $orig_initrd/.base_modules`;
    }

    print "initrd base modules:\n", format_array \@base_modules, 2;
    print "\n";

    for (@base_modules) {
      for my $ext (@kext_list) {
        if(-f "$kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}/initrd/$_$ext") {
          rename "$kernel->{new_dir}/$target_lib_dir/modules/$kernel->{version}/initrd/$_$ext", "$tmp_dir/$target_lib_dir/modules/$kernel->{version}/initrd/$_$ext";
        }
      }
    }

    my $comp = $mksquashfs_has_comp ? "-comp xz" : "";

    if($opt_no_compression->{squashfs}) {
      if(!$mksquashfs_has_comp) {
        die "mksquashfs version too old to allow setting compression algorithm\n";
      }
      else {
        $comp = "-no-compression";
      }
    }

    my $err = system "mksquashfs $kernel->{new_dir} $tmp_dir/parts/$p $comp -noappend -no-progress >/dev/null 2>&1";
    die "mksquashfs failed\n" if $err;
  }
  else {
    $tmp_dir = $kernel->{new_dir};
  }

  # add module symlink
  if($kernel->{initrd_layout} eq 'install') {
    symlink "$target_lib_dir/modules/$kernel->{version}/initrd", "$tmp_dir/modules";

    my $cmd = "Exec:\t\tln -snf $lib_dir/modules/`uname -r`/initrd /modules\n";

    if(open my $f, "$orig_initrd/linuxrc.config") {
      my $cmd_found;
      my @lines;
      while(<$f>) {
        push @lines, $_;
        $cmd_found = 1 if $_ eq $cmd;
      }
      close $f;

      if(!$cmd_found) {
        open my $f, ">$tmp_dir/linuxrc.config";
        print $f $cmd;
        print $f @lines;
        close $f;
      }
    }
  }

  push @opt_initrds, $tmp_dir;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_modules_to_instsys()
#
# Add new kernel modules and firmware to instsys.
#
sub add_modules_to_instsys
{
  return if $kernel->{initrd_layout} eq 'install';

  if(! -d "$kernel->{dir}/boot") {
    mkdir "$kernel->{dir}/boot", 0755;

    my $k_dir = "$kernel->{lib_dir}/modules/$kernel->{version}";

    for my $i ("System.map", "config", "sysctl.conf", $kernel->{name}) {
      symlink "../$k_dir/$i", "$kernel->{dir}/boot/$i-$kernel->{version}";
    }
    symlink "../$k_dir/.$kernel->{name}.hmac", "$kernel->{dir}/boot/.$kernel->{name}-$kernel->{version}.hmac" if -f "$kernel->{dir}/$k_dir/.$kernel->{name}.hmac";
  }

  symlink "$kernel->{name}-$kernel->{version}", "$kernel->{dir}/boot/$kernel->{name}";

  unshift @opt_instsys, $kernel->{dir};
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# update_no_compression_settings()
#
# Merge $opt_no_compression with .no_compression setting in initrd and
# update initrd if necessary.
#
sub update_no_compression_settings
{
  die "oops: initrd not unpacked?\n" if !$orig_initrd;

  my $has_setting = 0;
  my $has_opt_setting = $opt_no_compression ? 1 : 0;

  if(open my $f, "$orig_initrd/.no_compression") {
    $has_setting = 1;
    if(!$opt_no_compression) {
      chomp (my $no_comp = <$f>);
      for my $c (split /,/, $no_comp) {
        $opt_no_compression->{$c} = 1;
      }
    }
    close $f;
  }

  my @keys = qw (firmware modules squashfs);
  my $comp_str;

  for my $i (@keys) {
    if($opt_no_compression->{$i}) {
      $comp_str .= "," if $comp_str ne "";
      $comp_str .= "$i";
    }
  }

  if(($has_opt_setting || $has_setting) && $comp_str ne "") {
    print "no compression for: $comp_str\n";

    # change only if necessary
    if($has_opt_setting) {
      my $tmp_dir = $tmp->dir();

      if(open my $f, ">$tmp_dir/.no_compression") {
        print $f "$comp_str\n";
        close $f;
      }

      push @opt_initrds, $tmp_dir;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# replace_kernel_mods()
#
# Replace kernel modules.
#
# Includes getting the list of modules in the original initrd, unpacking the
# kernel packages, and including the new modules to the new initrd.
#
sub replace_kernel_mods
{
  my @modules;
  my $unpack_dir;

  unpack_orig_initrd if !$orig_initrd;

  die "initrd unpacking failed\n" if !$orig_initrd;

  update_no_compression_settings;

  get_initrd_modules;

  unpack_kernel_rpms;

  build_module_list;

  print "kernel version: $kernel->{orig_version} --> $kernel->{version}\n";

  if($kernel->{added}) {
    print "kernel modules added:\n", format_array $kernel->{added}, 2;
    print "\n";
  }

  if($kernel->{missing}) {
    print "kernel modules missing:\n", format_array $kernel->{missing}, 2;
    print "\n";
  }

  add_modules_to_initrd;

  add_modules_to_instsys;

  $kernel->{modules_replaced} = 1;

  # now replace kernel

  if(my $x = get_kernel_initrd) {
    $add_kernel = $kernel->{image};
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# new_products_xml(old_xml, dir, name, alias, prio, ask_user)
#
# Add a product to an existing add_on_products.xml or create a new one.
#
# This doesn't use a full xml parser but assumes a reasonably formatted
# add_on_products.xml.
#
sub new_products_xml
{
  my ($old_xml, $dir, $name, $alias, $prio, $ask_user) = @_;
  my $new_xml;
  my @x;

  $ask_user = $ask_user ? 'true' : 'false';

  @x = split /^/m, $old_xml || <<'# template';
<?xml version="1.0"?>
<add_on_products xmlns="http://www.suse.com/1.0/yast2ns"
    xmlns:config="http://www.suse.com/1.0/configns">
    <product_items config:type="list">
    </product_items>
</add_on_products>
# template

  my $product = <<"# product";
        <product_item>
            <name>$name</name>
            <url>relurl://$dir?alias=$alias</url>
            <priority config:type="integer">$prio</priority>
            <ask_user config:type="boolean">$ask_user</ask_user>
            <selected config:type="boolean">true</selected>
            <check_name config:type="boolean">false</check_name>
        </product_item>
# product

  # inject the new product at the end of the list
  for (@x) {
    if(m#\s*</product_items>#) {
      $_ = $product . $_;
    }
  }

  $new_xml = join '', @x;

  return $new_xml;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# prepare_addon()
#
# If there are RPMs for an add-on specified in @opt_addon_packages, create
# an add-on on the media.
#
# The add-on is placed into /addons/<repo_alias>/ and a file /add_on_products.xml
# is created/updated on the iso.
#
# Details can be influenced via $opt_addon_name, $opt_addon_alias, $opt_addon_prio.
#
sub prepare_addon
{
  return if !@opt_addon_packages;

  my $addon_name = $opt_addon_name;

  if($addon_name eq "") {
    # ok, be creative...

    my $idx = 1;
    $idx++ while fname "addons/Add-On_$idx";

    $addon_name = "Add-On $idx";
  }

  my $addon_alias = $opt_addon_alias;

  # strip chars we don't like to create an alias from addon name
  if($addon_alias eq "") {
    $addon_alias = $addon_name;
    $addon_alias =~ s/\s+/_/g;
    $addon_alias =~ tr/a-zA-Z0-9._\-//cd;
  }

  die "error: '$addon_name' is not a suitable add-on name, please choose a different one\n" if $addon_alias eq "";
  die "error: 'addons/$addon_alias' already exists\n" if fname "addons/$addon_alias";

  print "creating add-on \"$addon_name\" (alias $addon_alias):\n";

  my $tmp_dir = $tmp->dir();
  my $repo_dir = "$tmp_dir/addons/$addon_alias";
  mkdir "$tmp_dir/addons", 0755;
  mkdir $repo_dir, 0755;

  for (@opt_addon_packages) {
    die "$_: not a RPM\n" unless -f && file_magic($_) =~ /^RPM/;
    system "cp", $_, $repo_dir;
    print "  - $_\n";
  }

  # create repo-md files
  run_createrepo $repo_dir;

  # create/update add_on_products.xml
  my $products_xml;

  my $f = fname "add_on_products.xml";
  if($f && open my $fh, "<", $f) {
    local $/;
    $products_xml = <$fh>;
    close $fh;
  }

  $products_xml = new_products_xml($products_xml, "addons/$addon_alias", $addon_name, $addon_alias, $opt_addon_prio);

  if(open my $fh, ">", new_file("add_on_products.xml")) {
    print $fh $products_xml;
    close $fh;
  }

  # add our add-on to the iso
  my $new_source = { dir => $tmp_dir, real_name => $tmp_dir, type => 'dir' };
  push @sources, $new_source;
  update_filelist [ $new_source ];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# check_mksquashfs_comp()
#
# Return 1 if mksquahsfs supports '-comp' option, else 0.
#
sub check_mksquashfs_comp
{
  my $comp_ok = 0;

  if(open my $f, "mksquashfs -help 2>&1 |") {
    while(<$f>) {
      $comp_ok = 1, last if /^\s*-comp\s/;
    }
    close $f;
  }

  print "mksquashfs has '-comp': $comp_ok\n" if $opt_verbose >= 2;

  return $comp_ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# check_tagmedia_signature_tag()
#
# Return 1 if tagmedia supports '--signature-tag' option, else 0.
#
sub check_tagmedia_signature_tag
{
  my $sig_ok = 0;

  if(open my $f, "tagmedia --help 2>&1 |") {
    while(<$f>) {
      $sig_ok = 1, last if /^\s*--signature-tag\s/;
    }
    close $f;
  }

  print "tagmedia has '--signature-tag': $sig_ok\n" if $opt_verbose >= 2;

  return $sig_ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# eval_size(size_string)
#
# Interpret size_string and return size in (512 byte)-blocks.
#
# size_string is either a numerical size like '64G' or a file or block
# device name. In this case the size of the file or block device is used.
#
# An optional '+' or '-' at the beginning is ignored.
#
sub eval_size
{
  my $size = $_[0];
  my $unit = { b => 9 - 9, k => 10 - 9, m => 20 - 9, g => 30 - 9, t => 40 - 9 };

  $size =~ s/^[+\-]//;

  return undef unless $size;

  if($size =~ /^(\d+)\s*([bkmgt]?)/i) {
    $size <<= $unit->{"\L$2"} if $2;
  }
  elsif($size =~ m#/dev/#) {
    my $s;
    my $x = `readlink -f $size 2>/dev/null`;
    if($x =~ m#/dev/([^/]+?)\s*$#) {
      my $dev = $1;
      for (</sys/block/$dev/size /sys/block/*/$dev/size>) {
        if(open(my $f, $_)) {
          $s = <$f> + 0;
          close $f;
          last;
        }
      }
    }
    $size = $s;
  }
  elsif(-s $size) {
    $size = (-s _) >> 9;
  }
  else {
    $size = undef;
  }

  return $size;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# add_initrd_option(key, value)
#
# Add linuxrc/dracut config option.
# - key: option name
# - value: option value
#
# Options are stored in /etc/linuxrc.d/61_mkmedia (linuxrc) or /etc/cmdline.d/61-mkmedia.conf (dracut) in the initrd.
#
sub add_initrd_option
{
  my ($key, $value) = @_;

  my $use_linuxrc = $media_style eq 'suse' && $media_variant eq 'install';

  my $initrd_cfg = $use_linuxrc ? "etc/linuxrc.d/61_mkmedia" : "etc/cmdline.d/61-mkmedia.conf";

  unpack_orig_initrd if !$orig_initrd;

  if(!$initrd_options) {
    $initrd_options = $tmp->dir();
    push @opt_initrds, $initrd_options;

    mkdir "$initrd_options/etc", 0755;
    if($use_linuxrc) {
      mkdir "$initrd_options/etc/linuxrc.d", 0755;
    }
    else {
      mkdir "$initrd_options/etc/cmdline.d", 0755;
    }

    if($orig_initrd && -f "$orig_initrd/$initrd_cfg") {
      system "cp $orig_initrd/$initrd_cfg $initrd_options/$initrd_cfg";
    }
  }

  my $l;

  if(open my $f, ">>$initrd_options/$initrd_cfg") {
    # be conservative: try to avoid quotes - some tools (atm: Agama) have problems with it
    if(!defined $value) {
      $l = "$key";
    }
    elsif($value =~ /\s/) {
      $l = "$key=\"$value\"";
    }
    else {
      $l = "$key=$value";
    }
    print $f "$l\n";
    close $f;
  }

  my $x = $use_linuxrc ? "linuxrc" : "dracut";

  print "added $x option: $l\n" if $opt_verbose >= 1;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# wipe_iso()
#
# Wipe iso9660 file system header.
#
sub wipe_iso
{
  die "$iso_file: $!\n" unless open $iso_fh, "+<", $iso_file;

  # keep some data:
  #   - application id: 0x80 bytes at 0x823e
  #   - tags set by tagmedia: 0x200 bytes starting at file offset 0x8373
  my $buf = read_sector 0x10;
  my $appid = substr $buf, 0x23e, 0x80;
  my $tags = substr $buf, 0x373, 0x200;
  $buf = "\x00" x 0x800;
  substr $buf, 0x23e, 0x80, $appid;
  substr $buf, 0x373, 0x200, $tags;

  write_sector 0x10, $buf;
  write_sector 0x11, ("\x00" x 0x800);

  close $iso_fh;
  undef $iso_fh;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# get_media_style(sources)
#
# - sources: array_ref containing a list of directories
#
# Look at sources and determine media style (suse vs. rh).
#
# Assume rh style if there's an '/isolinux' dir or a '.discinfo' file or
# there are '<FOO>/Packages' subdirectories.
#
sub get_media_style
{
  my $src = $_[0];
  my $style = 'suse';

  for my $s (@$src) {
    if(-f "$s->{dir}/.discinfo" || -f "$s->{dir}/Fedora-Legal-README.txt" ) {
      $style = 'rh';
      last;
    }
    for my $r (glob "$s->{dir}/*/Packages $s->{dir}/Packages $s->{dir}/isolinux") {
      if(-d $r) {
        $style = 'rh';
        last;
      }
    }
  }

  return $style;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# get_media_variant(sources)
#
# - sources: array_ref containing a list of directories
#
# Look at sources and determine media variant (install, selfinstall, or live).
#
# Assume a Live medium if there's an '/LiveOS' dir.
#
sub get_media_variant
{
  my $src = $_[0];
  my $variant = 'install';

  for my $s (@$src) {
    if(-d "$s->{dir}/LiveOS") {
      $variant = 'live';
      last;
    }
    if(-f "$s->{dir}/config.isoclient") {
      $variant = 'selfinstall';
      last;
    }
  }

  return $variant;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# analyze_products(sources)
#
# sources is an array_ref containing a list of directories to be scanned and
# checked for product repositories (aka "modules").
#
# Repositories can use the same directory on different source isos. So we
# have to place them in new locations on the final medium. To avoid
# temporary copies, we do this via mkisofs's graft points (cf. man mkisofs).
#
# Repositories on the first medium are not relocated.
#
# Only repomd-style repositories are considered.
#
# Note: this assumes the lines in media.1/products to be structured like
# 'dir product version'.
#
sub analyze_products
{
  my $src = $_[0];

  my $src_idx = 0;

  for my $s (@$src) {
    # check for signature file
    for my $f (glob "$s->{dir}/.* $s->{dir}/*") {
      if(-f $f) {
        if(open my $fd, $f) {
          my $buf;
          sysread $fd, $buf, length $magic_sig_id;
          close $fd;
          if($buf eq $magic_sig_id) {
            $f = substr $f, length "$s->{dir}/";
            print "existing signature file detected: $f\n" if $opt_verbose >= 1;
            $detected_signature_file = $f if !defined $detected_signature_file;
          }
        }
      }
    }

    # read top-level products file
    $_ = "$s->{dir}/media.1/products";
    if($media_style eq 'suse' && open my $f, $_) {
      my @fields;
      while(my $l = <$f>) {
        @fields = split /\s/, $l;
        # ... and for each product definition, analyze it
        my $ok = check_product($src_idx, $_, @fields) if @fields == 3;
        # If we find a valid product, skip the source dir (except if it's the first).
        # Instead, only the product (the repository) is added to the final medium.
        $s->{skip} = 1 if $ok && $src_idx > 0;
      }
      close $f;
    }
    else {
      for my $r (glob "$s->{dir}/*/Packages") {
        next if $r !~ m#/([^/]+)/Packages#;
        my $p = $1;

        my $ver = "";
        if(open $f, "$s->{dir}/$p/repodata/repomd.xml") {
          while(<$f>) {
            $ver = $1, last if m#<revision>(\S+?)</revision>#;
          }
          close $f;
        }

        push @{$product_db->{list}}, {
          base_dir => "$s->{dir}",
          product_dir => $p,
          name => $p,
          ver => $ver,
          dirs => [ "repodata", "Packages" ],
          label => "",
          src_idx => $src_idx,
          repo_dir => $p,
          include => 1
        };
      }
    }
    $src_idx++;
  }

  # Check whether we really need to append a label to the repo directory to make it unique.
  my $product_dirs;
  for my $p_entry (@{$product_db->{list}}) {
    $product_dirs->{$p_entry->{product_dir}}++;
  }

  # Undo the label appending step if the directory is unique.
  for my $p_entry (@{$product_db->{list}}) {
    if(
      $product_dirs->{$p_entry->{product_dir}} == 1 &&
      $p_entry->{label}
    ) {
      $p_entry->{repo_dir} = $p_entry->{product_dir}
    }
  }

  if($opt_verbose >= 3) {
    print "product_db:\n";
    print Dumper($product_db)
  }

  # inform the user
  print "Repositories:\n" if $product_db->{list};

  for (@{$product_db->{list}}) {
    next if !$_->{include};
    print "  $_->{name} [$_->{ver}]";
    print " ($_->{label})" if $_->{label};
    print "\n";
  }

  exit 0 if $opt_list_repos;

  return unless $media_style eq 'suse' && $media_variant eq 'install';

  # don't merge repos if the user doesn't want to
  return if !$opt_merge_repos;

  # rebuild products file
  my $prod_file = copy_or_new_file "media.1/products";

  # create/update add_on_products.xml
  my $products_xml;
  my $products_xml_updated;

  my $f = fname "add_on_products.xml";
  if($f && open my $fh, "<", $f) {
    local $/;
    $products_xml = <$fh>;
    close $fh;
  }

  # rewrite entire product file
  open my $prod_fd, ">$prod_file" or die "media.1/products: $!\n";

  # ... and append any products we found above

  # Exclude all products that are in subdirectories and re-add them as
  # needed via mkisofs graft points. That's needed as they might be on
  # different media originally.
  #
  for (@{$product_db->{list}}) {
    push @{$mkisofs->{exclude}}, $_->{base_dir} if $_->{product_dir};

    next if !$_->{include} || $opt_type =~ /^(micro|nano|pico)$/;

    # FIXME: add $label to name?
    print $prod_fd "/$_->{repo_dir} $_->{name} $_->{ver}\n";

    # include full product dir, not just repo-specific subdirs
    push @{$mkisofs->{grafts}}, "$_->{repo_dir}=$_->{base_dir}" if $_->{product_dir};

    if($opt_enable_repos =~ /^(1|yes|auto|ask)$/i) {
      my $ask_user = $opt_enable_repos =~ /^ask$/i;
      $products_xml = new_products_xml(
        $products_xml, "$_->{repo_dir}", $_->{repo_dir}, $_->{repo_dir},
        $opt_addon_prio, $ask_user
      );
      $products_xml_updated = 1;
    }
  }

  if($products_xml_updated && open my $fh, ">", new_file("add_on_products.xml")) {
    print $fh $products_xml;
    close $fh;
  }

  close $prod_fd;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# check_product(source_idx, product_file, base_dir, name, version)
#
# -   source_idx: # of source medium (0-based)
# - product_file: full path to 'media.1/products'
# -     base_dir: directory the repos are in
# -         name: product name
# -      version: some version string
#
# Analyze a single repomd repository and add result to global $product_db
# structure.
#
# This includes determining the directories belonging to the repository and
# repo type (binaries, debug, source).
#
sub check_product
{
  my ($src_idx, $prod, $dir, $name, $ver) = @_;

  my $base_dir = $prod;
  $base_dir =~ s#/media.1/products$#$dir#;
  $base_dir =~ s#/+$##;

  $dir =~ s#^/##;

  # skip if we did this already
  return 0 if $product_db->{checked}{$base_dir};

  $product_db->{checked}{$base_dir} = 1;

  # print "$base_dir: $name, $ver\n";

  # not repo-md
  return 0 unless -d "$base_dir/repodata";

  my %repodirs;
  my $debug = 0;

  # scan primary.xml for directories belonging to the repo and check if
  # there are debuginfo packages

  my $f;
  my @xml = glob "$base_dir/repodata/*-primary.xml.*";

  open $f, "gzip --quiet -dc $xml[0] |" if $xml[0] =~ /\.gz$/;
  open $f, "xz --quiet -dc $xml[0] |" if $xml[0] =~ /\.xz$/;
  open $f, "zstd --quiet --force -dc $xml[0] |" if $xml[0] =~ /\.zst$/;

  if(defined $f) {
    while(my $l = <$f>) {
      $repodirs{$1} = 1 if $l =~ m#<location href="([^/]+)/#;
      $debug = 1 if $l =~ m#<location href=".*-debug(info|source)-#;
    }
    close $f;
  }
  else {
    print "no primary.xml in repo\n";
  }

  # tag repo if it's a debuginfo or a source repo
  my @labels;
  push @labels, "sources" if $repodirs{src} || $repodirs{nosrc};
  push @labels, "debuginfo" if $debug;
  my $label = join ",", @labels;

  # Include label in new product base dir.
  # The reason is that on our module media the same directory is used for
  # binary, source, and debuginfo modules. So we'd have a file conflict when
  # putting them on the same medium..
  my $repo_dir = $dir;

  if($label) {
    $repo_dir = $name if !$repo_dir;
    $repo_dir .= "_$label";
  }

  # Check repo list if the repo should be included on the final medium.
  # See --include-repos option.
  # If unset, include everything.
  #
  # Products in media root dir are always included.
  my $inc = 1;
  if($opt_include_repos && $dir) {
    my @repos = split /,/, $opt_include_repos;
    $inc = grep { $_ eq $name } @repos;
  }

  # create internal product database entry
  push @{$product_db->{list}}, {
    base_dir => $base_dir,
    product_dir => $dir,
    name => $name,
    ver => $ver,
    dirs => [ "repodata", sort keys %repodirs ],
    debug => $debug,
    label => $label,
    src_idx => $src_idx,
    repo_dir => $repo_dir,
    include => $inc
  };

  return 1;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# crypto_cleanup(image_file, crypt_vol, crypt_loop, crypt_mount, esp_loop, esp_mount, iso_mount)
#
# Cleanup things in case we have to leave run_crypto_disk() early.
#
sub crypto_cleanup
{
  my ($image_file, $crypt_vol, $crypt_loop, $crypt_mount, $esp_loop, $esp_mount, $iso_mount) = @_;

  # unmount things that might have been mounted
  susystem "umount $crypt_mount" if -d $crypt_mount;
  susystem "umount $esp_mount" if -d $esp_mount;
  susystem "umount $iso_mount" if -d $iso_mount;

  # close the luks volume
  susystem "cryptsetup close $crypt_vol" if -b "/dev/mapper/$crypt_vol";

  # detach loop devices
  susystem "losetup -d $esp_loop" if -b $esp_loop;
  susystem "losetup -d $crypt_loop" if -b $crypt_loop;

  # remove the temporary image
  unlink $image_file;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_crypto_disk()
#
# Create an LUKS-encrypted install disk.
#
# Notes:
#   - this is not a hybrid image - you cannot use it as dvd image
#   - so far only for x86_64
#   - the image is UEFI and legacy BIOS bootable
#   - there are two partitions: an (unencrypted) EFI system partition and
#     the encrypted install partition
#   - everything except the grub binary is encrypted - including the boot
#     config files used for installation
#   - a lot of things have to be done with root permissions, even though
#     we are working only on image files :-(
#
# This function uses the iso produced in the first pass as basis.
#
sub run_crypto_disk
{
  my $grub_dir = $opt_grub_dir || "/usr/share/grub2/i386-pc";

  if(! -f "$grub_dir/boot.img") {
    die "\n$grub_dir/boot.img: no such file\nsorry, package grub2-i386-pc must be installed or --grub-dir option used\n";
  }

  die "\nfilesystem '$opt_crypto_fs' not supported\n" unless -x "/usr/sbin/mkfs.$opt_crypto_fs";

  # if set, it must have a "/" at the end (e.g. "foo/")
  my $top_dir = $opt_crypto_top_dir;
  die "\n$top_dir: top-dir value not allowed\n" if $top_dir =~ m#/# || $top_dir eq '.' || $top_dir eq '..';
  $top_dir .= "/" if $top_dir ne "";

  # EFI system partition start in MiB
  my $esp_start_mb = 1;

  # EFI system partition size in MiB
  my $esp_size_mb = 7;

  # install partition start in MiB
  my $crypt_start_mb = $esp_start_mb + $esp_size_mb;

  # we need a temporary device mapper target - just use our tmp dir name
  my $crypt_vol;
  $crypt_vol = $1 if $tmp_new =~ m#/tmp/([^/]+)#;
  die "\noops: can't generate device mapper name\n" if $crypt_vol eq "";

  # get the image size
  if(!$image_size) {
    my $size = (-s $iso_file) >> 20;
    # increase size slightly to ensure we have enough space
    $size = $size * 1.3;
    $size += $esp_size_mb + $esp_start_mb + 8;
    $image_size = int($size) << 11;
  }

  my $image_file = "${iso_file}.tmp.$crypt_vol";

  my ($crypt_loop, $crypt_mount, $esp_loop, $esp_mount, $iso_mount, $fh);

  # store the password to be used by cryptsetup later
  my $crypt_pw = $tmp->file('pw');
  open $fh, ">$crypt_pw" or die "\npassword file: $!\n";
  print $fh $opt_crypto_password;
  close $fh;

  # register cleanup function in case we error out
  END { local $?; crypto_cleanup $image_file, $crypt_vol, $crypt_loop, $crypt_mount, $esp_loop, $esp_mount, $iso_mount }

  # create the empty image
  open $fh, ">", $image_file;
  close $fh;
  truncate $image_file, $image_size << 9;

  # partition it
  system "parted -s '$image_file' mklabel msdos";
  system "parted -s '$image_file' mkpart p ${esp_start_mb}MiB ${crypt_start_mb}MiB";
  system "parted -s '$image_file' mkpart p ${crypt_start_mb}MiB 100%";
  system "parted -s '$image_file' set 1 boot on";
  system "sfdisk --change-id '$image_file' 1 0xef 2>/dev/null";

  # make an efi system partition (fat)
  fat_mkfs $tmp_fat, $esp_size_mb << 11, $esp_start_mb << 11, 1, "EFI_PART";

  # ... and copy it into the efi system partition
  system "dd if='$tmp_fat' of='$image_file' bs=1b seek=" . ($esp_start_mb << 11) . " conv=notrunc status=none ; sync";

  # get loop device for install partition
  $crypt_loop = `${sudo}losetup --show -f -o ${\($crypt_start_mb << 20)} '$image_file'`;
  chomp $crypt_loop;

  die "\noops: no loop device\n" unless -b $crypt_loop;

  show_progress 10;

  # create luks container
  susystem "cryptsetup --batch-mode --force-password --key-file=$crypt_pw luksFormat --type luks1 $opt_luks $crypt_loop";
  susystem "cryptsetup --key-file=$crypt_pw open $crypt_loop $crypt_vol";

  # add filesystem
  susystem "mkfs.$opt_crypto_fs -q /dev/mapper/$crypt_vol";

  # get filesystem uuid
  my $fs_uuid=`${sudo}blkid -o value -s UUID /dev/mapper/$crypt_vol`;
  chomp $fs_uuid;

  # ... and luks partition uuid
  my $luks_uuid=`${sudo}blkid -o value -s UUID $crypt_loop`;
  chomp $luks_uuid;

  # grub prefers the uuid without dashes ('-')
  my $grub_uuid = $luks_uuid;
  $grub_uuid =~ tr/-//d;

  die "\noops: cryptsetup failed\n" if $fs_uuid eq "" || $luks_uuid eq "";

  show_progress 30;

  # get loop device for efi system partition
  $esp_loop = `${sudo}losetup --show -f -o ${\($esp_start_mb << 20)} --sizelimit ${\($esp_size_mb << 20)} '$image_file'`;
  chomp $esp_loop;

  die "\noops: no loop device\n" unless -b $esp_loop;

  # create temporary mount points
  $esp_mount = $tmp->dir('esp');
  $crypt_mount = $tmp->dir('crypt');
  $iso_mount = $tmp->dir('iso');

  # mount install partition, efi partition, and the prepared iso image
  die "\ncrypto mount failed\n" if susystem "mount /dev/mapper/$crypt_vol $crypt_mount";
  die "\nesp mount failed\n" if susystem "mount -oumask=0 $esp_loop $esp_mount";
  die "\niso mount failed\n" if susystem "mount -oloop,ro '$iso_file' $iso_mount";

  # some checks
  die "\nsorry, only x86_64 media supported atm\n" unless -d "$iso_mount/boot/x86_64/grub2-efi";
  die "\nsorry, efi boot is required\n" unless -d "$iso_mount/EFI/BOOT";

  # now our partitions are prepared and mounted

  show_progress 50;

  my $tmp_dir = $tmp->dir();

  my $title = $opt_crypto_title || "openSUSE";

  # ---  1. grub2 legacy setup  ---

  (my $grub_mods = <<"  = = = = = = = =") =~ s/\s+/ /g;
    gfxmenu gfxterm
    video videoinfo vga vbe
    biosdisk linux
    ext2 btrfs xfs jfs reiserfs iso9660 tar memdisk probe
    cryptodisk luks gcry_rijndael gcry_sha1 gcry_sha256
    all_video boot cat chain configfile echo
    font gzio halt
    jpeg minicmd normal part_apple part_msdos part_gpt
    password_pbkdf2 png reboot search search_fs_uuid
    search_fs_file search_label sleep test video fat loadenv
  = = = = = = = =

  # copy grub boot block to the required place for grub2-mkimage
  File::Path::make_path "$esp_mount/BOOT/grub2/i386-pc";
  system "cp $grub_dir/boot.img $esp_mount/boot/grub2/i386-pc";

  my $title_centered = $title;
  if(length $title <= 78) {
    $title_centered = (" " x ((80 - length $title) / 2)) . $title;
  }

  # This is the initial grub config - just ask for password and mount
  # the luks volume.
  # The real grub config with the install menu is inside the luks volume.
  (my $load_init_cfg = <<"  = = = = = = = =") =~ s/^ +//mg;
    search --no-floppy --file /EFI/BOOT/grub.cfg --set
    configfile (\$root)/EFI/BOOT/grub.cfg
  = = = = = = = =

  (my $load_cfg = <<"  = = = = = = = =") =~ s/^ +//mg;
    locale_dir=\$prefix/locale
    lang=en_US
    clear
    echo
    echo "$title_centered"
    echo
    while ! cryptomount -u "$grub_uuid" ; do
      echo
      echo Incorrect passphrase.
      echo
    done
    root=crypto0
    set prefix=(\$root)/${top_dir}boot/x86_64/grub2-efi
  = = = = = = = =

  (my $grub_po = <<"  = = = = = = = =") =~ s/^ +//mg;
    msgid "GNU GRUB  version %s"
    msgstr "$title"

    msgid "Attempting to decrypt master key..."
    msgstr " "

    msgid "Enter passphrase for %s%s%s (%s): "
    msgstr "Enter passphrase: "
  = = = = = = = =

  open $fh, ">$tmp_dir/load.cfg";
  print $fh $load_init_cfg;
  close $fh;

  # The default grub password dialog looks dead ugly; (ab)use
  # localization to make it look nice.
  # The en.mo file is placed into a memdisk which is then embedded into the
  # grub image.
  File::Path::make_path "$tmp_dir/memdisk/locale";
  open $fh, "| msgfmt -o $tmp_dir/memdisk/locale/en.mo -";
  print $fh $grub_po;
  close $fh;
  system "cd $tmp_dir/memdisk ; tar -cf ../memdisk.tar *";

  system "grub2-mkimage -d '$grub_dir' -O i386-pc -m $tmp_dir/memdisk.tar -p '(memdisk)'" .
    " -c $tmp_dir/load.cfg -o $esp_mount/boot/grub2/i386-pc/core.img $grub_mods";

  # grub2-bios-setup behaves a bit weird; I've seen no way to stop it from
  # trying to figure out things on its own. So it needs root permissions
  # to trace things through the loop device.
  susystem "grub2-bios-setup -s -d $esp_mount/boot/grub2/i386-pc '$image_file'";

  # the grub stuff is no longer needed, clear the efi partition
  system "rm -r $esp_mount/boot";

  # ---  grub2 legacy setup done  ---

  # ---  2. grub2 efi setup  ---

  # copy the efi config from the prepared iso and add our en.mo file
  system "cp -r $iso_mount/EFI $esp_mount/";
  system "cp $tmp_dir/memdisk/locale/en.mo $esp_mount/EFI/BOOT/locale";

  if($opt_grub_efi_dir) {
    if(-f "$opt_grub_efi_dir/grub.efi" ) {
      system "cp '$opt_grub_efi_dir/grub.efi' $esp_mount/EFI/BOOT";
    }
    else {
      die "\n$opt_grub_efi_dir/grub.efi: no such file\n";
    }
  }

  # adjust the startup message to indicate we've booted via efi
  # $load_cfg =~ s/BIOS/UEFI/g;

  # we have to explicitly load the final grub config (in the legacy case it
  # is done automatically)
  $load_cfg .= "configfile \$prefix/efi.cfg\n";

  # write initial grub config
  open $fh, ">$esp_mount/EFI/BOOT/grub.cfg";
  print $fh $load_cfg;
  close $fh;

  # ---  grub2 efi setup done  ---

  show_progress 60;

  # ---  3. install partition setup  ---

  # 3.1. copy everything

  # maybe put everything into a separate directory
  susystem "mkdir $crypt_mount/$top_dir" if $top_dir;

  # copy everything except the efi config - it's already on the efi system
  # partition
  system "${sudo}tar -C $iso_mount --exclude EFI -cf - . | ${sudo}tar -C $crypt_mount/$top_dir --keep-directory-symlink -xpf -";

  show_progress 90;

  # move locale settings to the correct place
  susystem "cp -r $iso_mount/EFI/BOOT/locale $crypt_mount/${top_dir}boot/x86_64/grub2-efi";

  # sanitize permissions (everything is ro on an iso9660 fs)
  susystem "chmod -R u+w $crypt_mount";

  # the el-torito efi image is not needed
  susystem "rm -f $crypt_mount/${top_dir}boot/x86_64/efi";

  # 3.2. adjust grub install config

  # There's only a grub config for efi on our media. We derive the legacy
  # config from it - it needs just a few modifications.

  # get it
  my $grub_cfg = `cat $iso_mount/EFI/BOOT/grub.cfg`;
  die "\nno grub config found\n" if $grub_cfg eq "";

  # strip things we don't want (it's been setup in the initial grub config)
  $grub_cfg =~ s/^search .*\n//m;
  $grub_cfg =~ s/^prefix=.*\n//m;
  $grub_cfg =~ s/^insmod efi_.*\n//mg;

   # adjust paths
  if($top_dir ne "") {
    $grub_cfg =~ s#/boot/#/${top_dir}boot/#g
  }

  # write grub efi config

  # Due to permission issues (the install partition is only root-writable)
  # write it to a tmp file and copy later.
  open $fh, ">$tmp_dir/grub.cfg";
  print $fh $grub_cfg;
  close $fh;

  # it's the efi config
  susystem "cp $tmp_dir/grub.cfg $crypt_mount/${top_dir}boot/x86_64/grub2-efi/efi.cfg";

  # convert efi config to legacy config

  # Basically replace linuxefi/initrdefi with linux/initrd and replace the
  # 'local boot' entry.
  $grub_cfg =~ s/\b(linux|initrd)efi\b/$1/g;

  (my $local_boot = <<"  = = = = = = = =") =~ s/^ {4}//mg;
    menuentry "Boot from Hard Disk" --class opensuse --class gnu-linux --class gnu --class os {
      set root=hd1
      chainloader (hd1)+1
    }
  = = = = = = = =

  $grub_cfg =~ s/^menuentry "Boot from Hard Disk".*?^\}\n/$local_boot/sm;

  # write grub legacy config

  # Due to permission issues (the install partition is only root-writable)
  # write it to a tmp file and copy later.
  open $fh, ">$tmp_dir/grub.cfg";
  print $fh $grub_cfg;
  close $fh;

  susystem "cp $tmp_dir/grub.cfg $crypt_mount/${top_dir}boot/x86_64/grub2-efi/grub.cfg";

  # adjust initrd

  # Now inject code to decrypt and mount the install partition. Also, point
  # the install source to the luks volume.

  # find the initrd
  my $initrd_file = "$crypt_mount/${top_dir}boot/x86_64/loader/initrd";
  $initrd_file = "$crypt_mount/${top_dir}boot/x86_64/initrd" unless -f $initrd_file;
  die "\nsorry, no initrd found\n" unless -f $initrd_file || $opt_type eq 'pico';

  File::Path::make_path "$tmp_dir/initrd/etc/linuxrc.d";

  # Store the password in the initrd so the user doesn't have to enter it twice.
  # (Note the initrd is on the encrypted volume.)
  # To avoid accidentally leaking the password when the user hands out the
  # initrd to someone else, encrypt the password file with the luks and
  # filesystem uuid as key. The uuid info is separate from the initrd. So even
  # if you copy the whole install souces from the unlocked luks volume, it
  # doesn't leak your password.
  open $fh, "| gpg --passphrase '$luks_uuid $fs_uuid' -c --batch --cipher-algo aes256 -o $tmp_dir/initrd/.password 2>/dev/null";
  print $fh "$opt_crypto_password";
  close $fh;

  die "\noops: password setup failed\n" unless -s "$tmp_dir/initrd/.password";

  # strip the final slash
  my $t = ${top_dir};
  $t =~ s#/$##;

  # Inject a linuxrc config entry to unlock the luks volume and point the
  # install source to the volume.
  # Note: we cannot unlock the volume directly here but have to append the
  # call to the 'early_setup' script as the config files are parsed _before_
  # udev sets up the devices.
  # The install option can still be overriden by anything passed via kernel
  # command line.
  (my $linuxrc_setup = <<"  = = = = = = = =") =~ s/^ +//mg;
    exec=echo /scripts/crypt_setup >>/scripts/early_setup
    install=hd:/$t?device=/dev/mapper/install.luks
  = = = = = = = =

  # for repo-md, instsys location should be set explicitly
  if($repomd_instsys_location) {
    my $dir = "/$t/$repomd_instsys_location";
    # be careful to have only single slashes
    $dir =~ s#//#/#g;
    $linuxrc_setup .= "instsys=hd:$dir?device=/dev/mapper/install.luks\n";
  }

  open $fh, ">$tmp_dir/initrd/etc/linuxrc.d/90_crypto";
  print $fh $linuxrc_setup;
  close $fh;

  # Store a small part of the id in the script to help identify the right
  # volume.
  # It's strictly not necessary but this way we don't waste too much efford
  # on systems with lots of volumes.
  my $short_uuid = substr $luks_uuid, 0, 4;

  # Here's the script. It iterates over all volumes, tries to decrypt the
  # password with the uuids it sees, and on success sets up the luks volume
  # and deletes the password files and itself.
  (my $crypt_setup = <<"  = = = = = = = =") =~ s/^ {4}//mg;
    #! /bin/bash
    cd /dev/disk/by-uuid
    for uuid in $short_uuid* ; do
      gpg --passphrase \"\$uuid $fs_uuid\" -d --batch --cipher-algo aes256 -o /.password. /.password 2>/dev/null
      if [ -f /.password. ] ; then
        cryptsetup --key-file=/.password. open /dev/disk/by-uuid/\$uuid install.luks
        break
      fi
    done
    rm -f /.password* /scripts/crypt_setup
  = = = = = = = =

  # add the script to our initrd tree
  mkdir "$tmp_dir/initrd/scripts";
  open $fh, ">$tmp_dir/initrd/scripts/crypt_setup";
  print $fh $crypt_setup;
  close $fh;
  chmod 0755, "$tmp_dir/initrd/scripts/crypt_setup";

  # pack, compress, and append our initrd stuff to the initrd
  system "( cd $tmp_dir/initrd ; find . | cpio --quiet -o -H newc --owner 0:0 | xz --check=crc32 -c ) > $tmp_dir/initrd.xz";
  susystem "sh -c 'cat $tmp_dir/initrd.xz >> $initrd_file'";

  # ---  install partition setup done  ---

  # That's it, we're done.

  show_progress 100;

  # rename our temporary image to the final name
  rename $image_file, $iso_file;

  print "\n";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# read_ini(file)
#
# - file: file name
#
# Read ini-style config file.
#
# Return content as hash reference.
#
sub read_ini
{
  my $file = $_[0];
  my $ini;
  my $section;

  if(open my $f, $file) {
    while(<$f>) {
      chomp;
      s/\s*;.*//;
      next if /^\s*$/;
      if(/^\s*\[([^]]+)\]/) {
        $section = $1;
        next;
      }
      next if !defined $section;
      if(/^\s*([^=>]+?)\s*+=\s*+(.*?)\s*$/) {
        $ini->{$section}{$1} = $2;
      }
    }
    close $f;
  }

  return $ini;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# write_ini(file, ini_hash)
#
# -     file: file name
# - ini_hash: hash reference with ini data (as returned by read_ini)
#
# Write ini-style config file.
#
sub write_ini
{
  my $file = $_[0];
  my $ini = $_[1];

  if(open my $f, ">$file") {
    for my $s (sort keys %{$ini}) {
      print $f "[$s]\n";
      for my $k (sort keys %{$ini->{$s}}) {
        print $f "$k = $ini->{$s}{$k}\n";
      }
      print $f "\n";
    }
    close $f;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# create_efi_image(file)
#
# - file: image file name
#
# Create FAT image of sufficient size and copy 'EFI' directory into it.
#
sub create_efi_image
{
  return unless fname "EFI";

  print "updating UEFI image: $_[0]\n";

  my $file = copy_or_new_file($_[0]);

  my $efi_dir = $tmp->dir();

  for my $x (sort keys %$files) {
    if($x =~ m#^EFI($|/)#) {
      system "tar -C '$files->{$x}' --mode=u+w -cf - '$x' | tar -C '$efi_dir' --keep-directory-symlink -xpf -";
    }
  }

  # efi image size in 512 byte blocks; giving one extra MiB free space
  my $efi_size = ((split " ", `du --apparent-size -x -B 1M -s $efi_dir`)[0] + 1) << 11;

  # create FAT fs
  open my $fh, ">", $file;
  close $fh;
  truncate $file, $efi_size << 9;
  system "mformat -i '$file' -s 32 -h 64 -c 1 -d 1 -v 'EFIBOOT' ::";

  # copy files
  system "mcopy -i '$file' -s -D o '$efi_dir/EFI' ::";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# rebuild_efi_image(efi_image_name)
#
# - efi_image_name: efi image file name (in ISO image)
#
# Check whether there were changes to the 'EFI' directory and we need to
# rebuild the el-torito efi image.
#
# Return 1 if the image needs to be rebuilt.
#
sub rebuild_efi_image
{
  my $efi_image_name = $_[0];

  my $source_list;

  $source_list->{$files->{$efi_image_name}} = 1 if $files->{$efi_image_name};

  for my $x (sort keys %$files) {
    if($x =~ m#^EFI($|/)#) {
      $source_list->{$files->{$x}} = 1;
    }
  }

  my $count = keys %$source_list;

  return 1 if $count > 1;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub read_config
{
  my $file = $_[0];

  my $cfg = {};

  if(open my $fd, $file) {
    while(<$fd>) {
      next if /^\s*#/;
      if(/^\s*(\S+)\s*[:=]\s*(.*?)\s*$/) {
        my $key = $1;
        my $val = $2;
        $val =~ s/^(["'])(.*)\1$/$2/;
        $cfg->{"\L$key"} = $val;
      }
    }
    close $fd;
  }

  return $cfg;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $arch = get_file_arch($file)
#
# Return architecture for $file or undef if it couldn't be determined.
#
# $file may optionally be xz, gzip, or zstd compressed (with file extensions
# .xz, .gz, or .zst, respectively)
#
sub get_file_arch
{
  my $file = $_[0];

  # Use objdump's 'file format' to determine architecture tag.
  # Note that objdump's 'architecture' entry does not differentiate between
  # 'ppc64' and 'ppc64le'.
  my $arch_map = {
    'elf32-i386' => 'i386',
    'elf32-littlearm' => 'armv7hl',	# same for 'armv6hl'
    'elf32-powerpc' => 'ppc',
    'elf32-s390' => 's390',
    'elf64-ia64-little' => 'ia64',
    'elf64-littleaarch64' => 'aarch64',
    'elf64-littleriscv' => 'riscv64',
    'elf64-powerpc' => 'ppc64',
    'elf64-powerpcle' => 'ppc64le',
    'elf64-s390' => 's390x',
    'elf64-x86-64' => 'x86_64',
  };

  if($file =~ /\.(gz|xz|zst)$/) {
    my $compr = 'gzip -dc';
    $compr = 'xz -dc' if $1 eq 'xz';
    $compr = 'zstd -dc' if $1 eq 'zst';
    my $tmp_file = $tmp->file();
    system "$compr $file > $tmp_file";
    $file = $tmp_file;
  }

  for (`objdump -f $file 2>/dev/null`) {
    if(/ file format (\S+)$/) {
      my $ar = $arch_map->{$1};
      die "$file: unsupported elf arch \"$1\"\n" if !$ar;
      return $ar;
    }
  }

  return undef;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# apply_duds_1()
#
# Apply all DUDs in @opt_duds - step 1 (analyze + iso).
#
# Update iso as needed.
#
# The critical part is that a DUD may replace the initrd. So we do it in two
# steps: first look only for 'iso' parts and add them to the @sources list.
# For this, avoid relying on initrd content too much and delay for example
# module/kernel related things to apply_duds_2.
#
# Nevertheless there *is* a chicken-egg problem; the replaced initrd should
# not introduce a changed 'dist' tag and have the same compression as the
# old one.
#
# Just don't overdo things.
#
# In step 2 the main DUD parts are applied.
#
sub apply_duds_1
{
  return undef if !@opt_duds;

  unpack_orig_initrd if !$orig_initrd;
  die "initrd unpacking failed\n" if !$orig_initrd;

  my $linuxrc_config = read_config "$orig_initrd/linuxrc.config";

  my $dud;

  $dud->{count} = 0;

  for my $dir (glob "$orig_initrd/[0-9]*/linux/suse") {
    next unless -d $dir;
    next unless $dir =~ m#/([0-9]+)/linux/suse$#;
    $dud->{count} = $1 + 0 if $1 > $dud->{count};
  }

  $dud->{arch} = get_file_arch "$orig_initrd/usr/bin/sh";
  $dud->{dist} = "*";

  $dud->{dir} = "/linux/suse/$dud->{arch}-$dud->{dist}";

  $dud->{installer} = $media_style eq 'rh' ? "anaconda" : "agama";

  if($media_style eq 'suse' && $media_variant eq 'install') {
    die "Error: UpdateDir not set in initrd, cannot apply driver updates\n" if $linuxrc_config->{updatedir} eq "";
    $dud->{installer} = 'yast';
    $dud->{dir} = $linuxrc_config->{updatedir};
    ($dud->{arch} = $1, $dud->{dist} = $2) if $dud->{dir} =~ m#/([^/-]+)-([^/-]+)$#;
  }

  print "going to apply driver updates in $dud->{dir}\n" if $opt_verbose >= 1;

  $dud->{initrd} = $tmp->dir();
  $dud->{instsys} = $tmp->dir();
  $dud->{iso} = $tmp->dir();
  $dud->{module_dir} = $tmp->dir();

  for my $d (@opt_duds) {
    my $tmp_dir = $tmp->dir();

    my $type = get_archive_type $d;

    if($type) {
      unpack_archive $type, $d, $tmp_dir;
    }
    else {
      die "Error: failed to apply driver update $d\n";
    }

    for my $dir (glob "$tmp_dir/linux/suse $tmp_dir/[0-9]*/linux/suse") {
      next unless -d $dir;
      for my $sub_dir (glob "$dir/$dud->{arch}-$dud->{dist}") {
        next unless -d $sub_dir;
        $dud->{count}++;
        push @{$dud->{dir_list}}, { count => $dud->{count}, dir => $sub_dir };
        apply_single_dud $dud, $dud->{count}, $sub_dir, 1;
        last;
      }
    }
  }

  if($dud->{iso_added}) {
    my $new_source = { dir => $dud->{iso}, real_name => $dud->{iso}, type => 'dir' };
    push @sources, $new_source;
    update_filelist [ $new_source ];
    unpack_orig_initrd;
  }

  return $dud;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# apply_duds_2()
#
# Apply all DUDs in @opt_duds - step 2 (main part).
#
# Update initrd, instsys, boot options as needed.
#
sub apply_duds_2
{
  my $dud = $_[0];

  return if !$dud->{dir_list};

  # to get $kernel hash
  get_initrd_modules;
  $dud->{kernel_dir} = "$kernel->{target_lib_dir}/modules/$kernel->{orig_version}";

  print "going to apply driver updates in $dud->{dir}\n" if $opt_verbose >= 1;

  for my $d (@{$dud->{dir_list}}) {
    apply_single_dud $dud, $d->{count}, $d->{dir}, 0;
  }

  push @opt_initrds, $dud->{initrd} if $dud->{initrd_added};
  push @opt_instsys, $dud->{instsys} if $dud->{instsys_added};
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# apply_single_dud(dud, count, dir, iso_only)
#
# Apply single DUD in $dir; $dud is hash describing DUD.
# $dud is updated as needed.
#
# Update iso, initrd, instsys, boot options as needed.
#
sub apply_single_dud
{
  my $dud = $_[0];
  my $count = $_[1];
  my $dir = $_[2];
  my $iso_only = $_[3];

  print "apply single dud: dir = $dir, count = $count, iso = $iso_only\n" if $opt_verbose >= 2 && !$iso_only;

  $dud->{config} = read_config "$dir/dud.config";

  $dud->{name} = $dud->{config}{updatename};
  $dud->{id} = $dud->{config}{updateid};

  print "applying driver update\n  - id: $dud->{id}\n  - name: $dud->{name}\n" if !$iso_only;

  my $log;
  my $log_id = $dud->{id};
  $log_id =~ tr [./][__];

  if($iso_only) {
    if(-d "$dir/iso" ) {
      $dud->{iso_added} = 1;
      $dud->{has_iso_dir}{$dir} = 1;
      system "tar -C '$dir/iso' -cf - . | tar -C '$dud->{iso}' --keep-directory-symlink -xpf -";
      system "rm -rf '$dir/iso'";
    }
    return;
  }
  else {
    print "  - iso updated\n" if $dud->{has_iso_dir}{$dir};
  }

  if(-d "$dir/initrd" ) {
    print "  - initrd updated\n";
    $dud->{initrd_added} = 1;
    system "tar -C '$dir/initrd' -cf - . | tar -C '$dud->{initrd}' --keep-directory-symlink -xpf -";
    system "rm -rf '$dir/initrd'";
  }

  if(-d "$dir/inst-sys" ) {
    print "  - inst-sys updated\n";
    $dud->{instsys_added} = 1;
    system "tar -C '$dir/inst-sys' -cf - . | tar -C '$dud->{instsys}' --keep-directory-symlink -xpf -";
    system "rm -rf '$dir/inst-sys'";
  }

  if(-d "$dir/modules" ) {
    print "  - modules updated\n";
    for my $m (glob "$dir/modules/*${kext_glob}") {
      next unless -f $m;
      my $version = (split " ", `modinfo -F vermagic $m 2>/dev/null`)[0];
      die "$m: no kernel version found\n" if $version eq "";
      my $k_dir = "$dud->{kernel_dir}/updates";
      if($dud->{installer} ne "yast") {
        File::Path::make_path "$dud->{instsys}/$k_dir";
        system "cp $m $dud->{instsys}/$k_dir";
        $dud->{instsys_added} = 1;
        $dud->{instsys_needs_depmod} = 1;
      }
      File::Path::make_path "$dud->{initrd}/$k_dir";
      system "mv $m $dud->{initrd}/$k_dir";
      $dud->{initrd_added} = 1;
      $dud->{initrd_needs_depmod} = 1;
      $m =~ s#.*/##;
      $dud->{modules}{$m} = $version;
      $log .= "kmod: $m ($version)\n";
      print "Warning: wrong version for kernel module $m: $version (expected $kernel->{orig_version})\n" if $version ne $kernel->{orig_version};
    }
    system "rm -rf '$dir/modules'";
  }

  my $keep_keys = 0;
  my $config_options = 0;
  my $boot_options = 0;

  for my $k (sort keys %{$dud->{config}}) {
    next if $k =~ /^update/;
    if($k eq 'boot') {
      $boot_options++;
      $opt_boot_options .= " $dud->{config}{boot}";
      $log .= "boot: $dud->{config}{boot}\n";
      next;
    }
    if($dud->{installer} eq 'yast') {
      $keep_keys++;
    }
    else {
      $config_options++;
      add_initrd_option $k, $dud->{config}{$k};
      if(defined $dud->{config}{$k}) {
        $log .= "config: $k=$dud->{config}{$k}\n";
      }
      else {
        $log .= "config: $k\n";
      }
    }
  }

  print "  - config options added\n" if $config_options;
  print "  - boot options added\n" if $boot_options;

  if($keep_keys || -d "$dir/install") {
    my $c = sprintf "%02u", $count;
    my $new_dir = "$dud->{initrd}/$c/linux/suse", $dud->{initrd};
    File::Path::make_path $new_dir;

    my $top_dir = $dir;
    $top_dir =~ s#.*/##;

    # keep at least dud name and id
    print "  - keeping some parts in /$c/linux/suse/$top_dir\n";

    # move remaining parts, if any
    # final part in $dir might be symlink!
    rename abs_path($dir), "$new_dir/$top_dir";

    $dud->{initrd_added} = 1;
  }

  if($log_id ne "" && $log ne "") {
    if(open my $fd, ">>", "$dud->{initrd}/.update.initrd.$log_id") {
      print $fd $log;
      close $fd;
    }
    if(open my $fd, ">>", "$dud->{instsys}/.update.$log_id") {
      print $fd $log;
      close $fd;
    }
    if(open my $fd, ">>", "$dud->{iso}/.update.iso.$log_id") {
      print $fd $log;
      close $fd;
    }
  }

  if($opt_verbose >= 2) {
    print "driver update data:\n";
    print Dumper $dud;
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# kernel_module_exists(dir, module_file)
#
# return full path for module file in dir or undef
#
sub kernel_module_exists
{
  my $dir = $_[0];
  my $old_file = $_[1];

  my $name = $old_file;

  return undef unless $name =~ s/${kext_regexp}$//;

  $name =~ s#.*/##;

  my $new_file;

  for my $n (glob "$dir/$name$kext_glob") {
    $new_file = $n, last if -f $n;
  }

  return $new_file;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# run_depmod($dir, $version)
#
# Run depmod command for directory $dir and kernel version $version.
#
# Takes care of usrmerge by building a separate lib & usr/lib tree and symlinking
# actual module dir there.
#
# Might need root priv when running in live-root but since mkmedia enforces root
# in this case anyway, it does not really matter.
# In cany case, the check (related to $needs_root) is kept.
#
sub run_depmod
{
  my $dir = $_[0];
  my $version = $_[1];

  if(!$depmod) {
    $depmod->{dir} = $tmp->dir();

    # prepare base tree
    File::Path::make_path "$depmod->{dir}/usr/lib";
    symlink "usr/lib", "$depmod->{dir}/lib";

    # use our own depmod.conf
    $depmod->{conf} = "$depmod->{dir}/depmod.conf";
    open my $fd, ">", $depmod->{conf};
    print $fd "search updates extra weak-updates kgraft built-in\n";
    print $fd "make_map_files no\n";
    close $fd;
  }

  unlink "$depmod->{dir}/usr/lib/modules";

  if(-d "$dir/usr/lib/modules") {
    symlink "$dir/usr/lib/modules", "$depmod->{dir}/usr/lib/modules";
  }
  elsif("$dir/lib/modules") {
    symlink "$dir/lib/modules", "$depmod->{dir}/usr/lib/modules";
  }
  else {
    die "depmod: unsupported kernel tree\n";
  }

  my $needs_root = ! -w $dir;
  my $err;

  if($needs_root) {
    $err = susystem "depmod -a -C '$depmod->{conf}' -b '$depmod->{dir}' '$version' >$depmod->{dir}/stderr 2>&1";
  }
  else {
    $err = system "depmod -a -C '$depmod->{conf}' -b '$depmod->{dir}' '$version' >$depmod->{dir}/stderr 2>&1";
  }

  if($err) {
    my $log;
    if(open my $fd, "$depmod->{dir}/stderr") {
      local $/;
      $log = <$fd>;
      close $fd;
    }
    chomp $log;
    die "$log\nError: depmod failed\n";
  }
}
