#! /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';

  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("/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;
  }
}


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

use Getopt::Long;
use File::Find;
use File::Path;
use Cwd 'abs_path';

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

our $VERSION = "0.0";

my @all_archs = qw ( x86_64 aarch64 armv7l i386 ia64 ppc ppc64 ppc64le s390 s390x );
my $REPLACEABLE_YAST = '3.1.135';

# 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 get_file_arch;
sub file_type;
sub copy_dud;
sub analyze_dud;
sub cleanup_old_duds;
sub new_dud;
sub analyze_ycp_files;
sub set_mkisofs_metadata;
sub write_dud;
sub fix_duds;
sub show_dud;
sub nice_arch_list;
sub show_single_dir;
sub get_service_pack;
sub set_format;
sub import_sign_key;
sub get_sign_key_name;
sub sign_file;
sub get_obs_key;
sub repack_as_rpm;
sub sign_rpm;
sub create_adddir;
sub installer_from_dist;
sub installer_from_config;
sub get_rpm_scripts;
sub parse_alternatives;
sub apply_alternatives;
sub check_osc;

my %config;
my $opt_create;
my $opt_show;
my @opt_dist;
my @opt_arch;
my $opt_prio = 50;
my @opt_name;
my @opt_exec;
my @opt_hook;
my $opt_no_docs = 1;
my $opt_save_temp;
my %opt_install;
my @opt_config;
my @opt_condition;
my $opt_format;
my $opt_sign;
my $opt_sign_direct;
my $opt_sign_key;
my $opt_sign_key_id;
my $opt_dud_prefix;
my $opt_vendor;
my $opt_preparer;
my $opt_application;
my $opt_volume;
my $opt_obs_keys;
my $opt_fix_yast = 1;
my $opt_fix_usr_src = 1;
my $opt_fix_dist = 1;
my $opt_fix_adddir = 1;
my $opt_check_hooks = 1;
my @opt_initrd;
my $opt_installer;
my @opt_iso;

# global variables
my $dud;
my @files;
my @dists;
my $dud_cnt = 0;
my %arch;
my $use_all_archs = 0;
my $format_archive = "cpio";
my $format_compr = "gz";
my $format_rpm = 0;		# re-package as rpm if set
my $sign_key_dir;
my $sign_key_ok;
my $sign_key_id;
my $obs;
my $pubkey_info;
my $yast_version = 0;
my $mkisofs = -x '/usr/bin/mkisofs' ? '/usr/bin/mkisofs' : '/usr/bin/genisoimage';
my $hooks;

# linuxrc versions in service packs
my $servicepack;
$servicepack->{10}{0} = "2.0.46";
$servicepack->{10}{1} = "2.0.67";
$servicepack->{10}{2} = "2.0.79";
$servicepack->{10}{3} = "2.0.91";
$servicepack->{10}{4} = "2.0.97";
$servicepack->{11}{0} = "3.3.34";
$servicepack->{11}{1} = "3.3.59";
$servicepack->{11}{2} = "3.3.81";
$servicepack->{11}{3} = "3.3.91";
$servicepack->{11}{4} = "3.3.108";

my %dracut_hooks;
@dracut_hooks{qw (
  cleanup cmdline emergency mount netroot pre-mount pre-pivot pre-shutdown pre-trigger pre-udev shutdown shutdown-emergency
  initqueue/finished initqueue/online initqueue/settled initqueue/timeout
)} = ( 1 .. 100 );

GetOptions(
  'create|c=s'       => sub { $opt_create = 1; $dud = $_[1] },
  'show|s=s'         => sub { $opt_show = 1; $dud = $_[1] },
  'arch|a=s'         => \@opt_arch,
  'dist|d=s'         => \@opt_dist,
  'install|i=s'      => sub { if($_[1] ne "") { @opt_install{split /,/, $_[1]} = ( 1 .. 7 ) } else { $opt_install{""} = 1 } },
  'prio|p=i'         => \$opt_prio,
  'name|n=s'         => \@opt_name,
  'exec|x=s'         => \@opt_exec,
  'dracut-hook|hook=s' => \@opt_hook,
  'config=s'         => \@opt_config,
  'condition=s'      => \@opt_condition,
  'may-replace-yast' => sub { $opt_fix_yast = 0 },
  'no-docs'          => \$opt_no_docs,
  'keep-docs'        => sub { $opt_no_docs = 0 },
  'detached-sign'    => \$opt_sign,
  'sign'             => sub { $opt_sign = 1; $opt_sign_direct = 1 },
  'sign-key=s'       => \$opt_sign_key,
  'sign-key-id=s'    => \$opt_sign_key_id,
  'obs-keys'         => \$opt_obs_keys,
  'force'            => sub { $opt_fix_yast = $opt_fix_usr_src = $opt_fix_dist = $opt_fix_adddir = $opt_check_hooks = 0 },
  'fix-yast!'        => \$opt_fix_yast,
  'fix-usr-src!'     => \$opt_fix_usr_src,
  'fix-dist!'        => \$opt_fix_dist,
  'fix-adddir!'      => \$opt_fix_adddir,
  'check-hooks!'     => \$opt_check_hooks,
  'format=s'         => \$opt_format,
  'prefix=i'         => \$opt_dud_prefix,
  'volume=s'         => \$opt_volume,
  'vendor=s'         => \$opt_vendor,
  'preparer=s'       => \$opt_preparer,
  'application=s'    => \$opt_application,
  'installer=s'      => \$opt_installer,
  'initrd=s'         => \@opt_initrd,
  'iso=s'            => \@opt_iso,
  'save-temp'        => \$opt_save_temp,
  'version'          => sub { print "$VERSION\n"; exit 0 },
  'help'             => sub { usage 0 },
) || usage 1;

if(!%opt_install) {
  @opt_install{qw ( instsys repo rpm )} = ( 1, 1, 1 );
}

for (sort keys %opt_install) {
  usage 1 unless /^(instsys|repo|rpm|$)/;
}

usage 1 if $opt_installer !~ /^(|agama|anaconda|yast)$/i;

usage 1 unless $opt_show xor $opt_create;

usage 2 if $opt_show && @ARGV;

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

@opt_arch = map { /^i.86$/ ? "i386" : $_ } @opt_arch;

if(open my $f, "$ENV{HOME}/.mkdudrc") {
  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;
}

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

if($opt_obs_keys) {
  check_osc;
  my $f;
  if(open($f, "$ENV{HOME}/.oscrc") || open($f, "$ENV{HOME}/.config/osc/oscrc")) {
    while(<$f>) {
      if(m#^\[(https?://([^/\]]+))#) {
        $obs->{server}{$2} = $1;
      }
    }
    close $f;
  }

  for (sort keys %{$obs->{server}}) {
    my $x = $_;
    $x =~ s/^[^\.]*\.//;
    $obs->{server_short}{$x} = $obs->{server}{$_};
  }
}

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

my $tmp_dud = $tmp->dir('dud');
my $tmp_old = $tmp->dir('old');
my $tmp_new = $tmp->dir('new');
my $tmp_mnt = $tmp->dir('mnt');
my $tmp_err = $tmp->file('err');
my $tmp_archive = $tmp->file('dud.xxx');
$sign_key_dir = $tmp->dir('gpg');
chmod 0700, $sign_key_dir;

set_format;

import_sign_key;

if($opt_create) {
  file_type $_ for (@ARGV);
  file_type $_, 1 for (@opt_initrd);
  file_type $_, 2 for (@opt_iso);

  my $need_dist;
  for (@files) {
    $need_dist = 1, last if $_->{type} ne "dud";
  }

  $need_dist ||= @opt_config || @opt_exec || @opt_hook || @opt_name || @opt_initrd || @opt_iso;

  if($need_dist) {
    die "Error: distribution arg is required; use --dist.\n" if !@opt_dist;
    my %d;
    @dists = @opt_dist;

    if($opt_fix_dist) {
      map { tr/-//d } @dists;
      # kubic is part of tumbleweed
      map { s/^(tumbleweed|tw|kubic).*/tw/g } @dists;
      # map 'micro' to 'suse-microos'
      map { s/^micro(\d)/suse-microos$1/ } @dists;
      # map 'casp' to the new 'caasp'
      map { s/^casp(\d)/caasp$1/ } @dists;
      # CaaSP should be aligned with the respective SLES
      # caasp1.0 = sle12 (-sp2)
      # cassp2.0 = sle12 (-sp3)
      # caasp3.0 = sle12 (-sp3)
      # caasp4.0 = sle15
      push @dists, "sles12" if grep { /^caasp[123]\./ } @dists;
      push @dists, "sles15" if grep { /^caasp4\./ } @dists;
      push @dists, "13.2" if grep { $_ eq "leap42.1" } @dists;
      @d{map { /^sle([sd]?)(\d+)/i ? $1 eq "" && $2 <= 15 ? ("sles$2", "sled$2") : "sle\L$1$2" : "\L$_" } @dists} = ();
      @dists = sort keys %d;
      @d{map { /^(\d+)\.(\d+)$/ && $1 == 15 ? ("leap$1.$2", "sles$1", "sled$1") : $_ } @dists} = ();
      @d{map { /^(\d+)$/ && $1 == 15 ? ("sles$1", "sled$1") : $_ } @dists} = ();
      @d{map { /^(\d+)\.(\d+)$/ && $1 >= 16 ? ("leap$1.$2", "sles$1") : $_ } @dists} = ();
      @d{map { /^(\d+)$/ && $1 >= 16 ? ("sle$1") : $_ } @dists} = ();
      @d{map { /^sle[sd](\d+)$/ && $1 >= 16 ? ("sle$1") : $_ } @dists} = ();
      for (16 .. 19) { delete $d{"sles$_"}; delete $d{"sled$_"}; }
      @dists = sort keys %d;
      @dists = grep { !(/^(\d+)(\.(\d+))?$/ && $1 >= 15) } @dists;
    }

    for (@dists) {
      if(!/^((leap|kubic|casp|caasp|suse-microos)?\d+\.\d+|tw|sle[sd]\d+|sle1[6-9]|((fedora|rhel|sll)\d+))$/) {
        if(!$opt_fix_dist) {
          print STDERR "***  Note: using unsupported dist \"$_\"\n";
        }
        else {
          die "Error: unsupported dist \"$_\" (e.g. sle15, tw; use --help for supported dists, --no-fix-dist to override)\n";
        }
      }
    }
  }

  # cleanup old driver update sources
  File::Find::find(sub {
   unlink $File::Find::name if $_ eq 'TRANS.TBL';
  }, $tmp_old);

  for (@files) {
    $arch{$_->{arch}} = 1 if defined $_->{arch};
  }

  @arch{@opt_arch} = () if @opt_arch;

  if(!%arch) {
    %arch = ( $all_archs[0] => 1 );
    $use_all_archs = 1;  
  }

  fix_duds \@files;

  if(new_dud()) {
    unshift @files, { type => 'dud', file => $tmp_dud };
  }

  # print STDERR Dumper(\@files);

  $dud = write_dud \@files, $dud;

  exit 0 unless defined $dud;

  # clear list...

  undef @files;

  # ... and fall through to '--show'
}

file_type $dud;

# print STDERR Dumper(\@files);

$dud_cnt = 0;

for (@files) {
  if($_->{type} eq 'dud') {
    show_dud $_;
  }
  else {
    print STDERR "$_->{file}: not a driver update\n";
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# usage($exit_code)
#
# Print help text and exit.
#
sub usage
{
  print <<"= = = = = = = =";
Usage: mkdud [OPTIONS] [SOURCES]
Manage driver updates.

General options:

      --version                 Show mkdud version.
      --save-temp               Keep temporary files.
      --help                    Write this help text.

Verify driver update:

  -s, --show DUD                Verify DUD and print summary.

Create driver update:

  -c, --create DUD              Create new driver update DUD from SOURCES.
  -a, --arch ARCH               Build for target ARCH (default: auto detected from SOURCES).
  -d, --dist DIST               Specify the product the DUD is for (e.g. sle15).
      --condition SCRIPT        Run SCRIPT and apply DUD only if SCRIPT has exit status 0.
  -p, --prio NUM                Set repository priority to NUM (default: 50).
  -n, --name NAME               Set driver update name.
  -x, --exec COMMAND            Run command just after the driver update has been loaded.
      --dracut-hook HOOK:CMD    Insert CMD into dracut hook HOOK. See man page for details.
  -i, --install METHODS         Package install method. METHODS is a comma-separated list
                                of: instsys, repo, rpm (default: 'instsys,repo,rpm').
      --initrd RPM|DIR          Add RPM or DIR to initrd (option can be repeated).
      --iso DIR                 Add DIR to installation ISO image (option can be repeated).
      --installer INSTALLER     Set installation program. Either agama or yast
                                (default: auto-detected based on dist).
      --config KEY=VALUE        Set initrd config option KEY to VALUE (option can be repeated).
                                You can set boot options using KEY 'boot'.
      --no-docs                 Don't include package documentation (default).
      --keep-docs               Include package documentation.
      --no-fix-yast             Allow driver update to replace /sbin/yast.
      --no-fix-dist             Allow to specify an arbitrary distribution name with --dist.
      --no-fix-usr-src          Allow driver update to include /usr/src/packages.
      --no-fix-adddir           Do not include an updated adddir script.
      --no-check-hooks          Do not restrict dracut hook names.
      --format FORMAT           Specify archive format for DUD (default: cpio.gz).
                                FORMAT=((cpio|tar|iso)[.(gz|xz)])|rpm.
      --prefix NUM              First directory prefix of driver update.
      --sign                    Sign the driver update.
      --detached-sign           Sign the driver update creating a detached signature.
      --sign-key KEY_FILE       Use this key for signing.
      --sign-key-id KEY_ID      Use this key id for signing (anything gpg accepts).
      --volume                  Set ISO volume id (if using format 'iso').
      --vendor                  Set ISO publisher id (if using format 'iso').
      --preparer                Set ISO data preparer id (if using format 'iso').
      --application             Set ISO application id (if using format 'iso').
      --obs-keys                Retrieve and add project keys from the openSUSE build service
                                as needed to verify the RPMs in SOURCES.

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

  exit shift;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $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;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# file_type($src)
#
# Analyze $src and add type and other info to global @files list.
#
sub file_type
{
  local $_;
  my $dud;
  my @i;
  my $gpg = "gpg --homedir=$sign_key_dir --yes --output - 2>/dev/null";
  my $gpg_sign;

  my $for_instsys = $_[1] == 0;
  my $for_initrd = $_[1] == 1;
  my $for_iso = $_[1] == 2;

  if(!-e $_[0]) {
    print STDERR "$_[0]: error: no such file or directory\n";
    return;
  }

  $_ = `file -b -k -L $_[0] 2>/dev/null`;

  if(/^RPM/) {
    my $ft = { type => ($for_initrd ? 'rpm_initrd' : $for_iso ? 'rpm_iso' : 'rpm'), file => $_[0] };

    my $f = `rpm --nosignature -qp --qf '%{NAME}\t%{VERSION}\t%{RELEASE}.%{ARCH}' $_[0] 2>$tmp_err`;
    if($f eq "") {
      print STDERR "failed to read rpm: $_[0]\n";
      my $x;
      open $x, $tmp_err;
      print STDERR $_ while (<$x>);
      close $x;
      exit 1;
    }

    my $scripts = get_rpm_scripts $_[0];
    my $links = parse_alternatives $scripts->{postin};

    $ft->{alternatives} = $links if $links;

    if($for_instsys) {
      # check if rpm contains a driver update
      # (a directory /linux/suse/foo-bar or /<NUMBER>/linux/suse/foo-bar exists)
      for my $line (`rpm --nosignature -qp -lv $_[0] 2>$tmp_err`) {
        my @field = split ' ', $line;
        next unless $field[0] =~ /^d/;
        if($field[8] =~ m#/(\d+/)?linux/suse/\S+-\S+$#) {
          $dud = 'cpio.rpm';
          last;
        }
      }

      # Check if yast base package is to be replaced and remember its version:
      if($f =~ /^yast2\t(.+)\t/) {
        $yast_version = $1;
        # print STDERR "yast version: $yast_version\n";
      }
    }

    ($ft->{canonical_name} = $f) =~ s/\t/-/g;

    my $ar = $f =~ /\.([^.]+)$/ ? $1 : undef;
    $ar = "i386" if $ar =~ /^i.86$/;

    $ft->{arch} = $ar if defined $ar && $ar ne 'noarch';

    my $d = `rpm --nosignature -qp --qf '%{BUILDTIME}' $_[0] 2>$tmp_err`;
    $d = gmtime $d if $d;

    $ft->{date} = $d;

    if(!$dud) {
      push @files, $ft;

      if($opt_obs_keys && $opt_install{repo}) {
        my $x = `rpm --nosignature -qp -i $_[0] 2>$tmp_err`;
        if($x =~ /^Signature\s*:.*Key ID/m) {
          $x = `rpm --nosignature -qp --qf '%{DISTURL}' $_[0] 2>$tmp_err`;
          $x = get_obs_key $x, $_[0];
          push @files, { type => 'pubkey', file => $x } if $x;
        }
      }

      return;
    }
  }
  elsif($for_instsys && $_[0] =~ m#${kext_regexp}$#) {
    my $ft = { file => $_[0] };

    my $ar = get_file_arch $_[0];
    $ft->{arch} = $ar if defined $ar;

    $ft->{type} = 'module';
    @i = split " ", `modinfo -F vermagic $_[0] 2>/dev/null`;
    $ft->{version} = $i[0];
    my $v = `modinfo -F version $_[0] 2>/dev/null`;
    chomp $v;
    $ft->{mod_version} = $v if $v !~ /^\s*$/;

    push @files, $ft;

    return;
  }
  elsif($for_instsys && /^ELF/) {
    @i = split /\s*,\s*/;

    my $ft = { file => $_[0] };

    my $ar = get_file_arch $_[0];
    $ft->{arch} = $ar if defined $ar;

    if($i[0] =~ /executable/) {
      $ft->{type} = 'bin';
      push @files, $ft;
    }
    elsif($i[0] =~ /shared/) {
      $ft->{type} = 'lib';
      push @files, $ft;
    }

    return;
  }
  elsif($for_instsys && / (cpio|tar) archive/) {
    $dud = $1;
  }
  elsif($for_instsys && /^(gzip|XZ) compressed data/) {
    my $cmd = "\L$1";
    my $f = $cmd ne 'gzip' ? $cmd : 'gz';
    my $z = `$cmd -dc $_[0] | file -b -`;
    $dud = "$1.$f" if $z =~ / (cpio|tar) archive/;
    $dud = "iso.$f" if $z =~ / ISO 9660 CD-ROM /;
  }
  elsif($for_instsys && / ISO 9660 CD-ROM /) {
    if(!$>) {
      system "mount -oro,loop $_[0] $tmp_mnt";
      file_type "$tmp_mnt", 1;
      system "umount $tmp_mnt";
    }
    else {
      print STDERR "$_[0]: error: need root permissions to analyze iso images\n";
    }
    $dud = 'dummy';
  }
  elsif(-d $_[0]) {
    if($for_instsys) {
      if($_[0] =~ /(^|\/)y2update\/*$/) {
        push @files, { type => 'y2update', file => $_[0] };

        return;
      }

      if(-f "$_[0]/driverupdate") {
        $dud = 'dummy';
        file_type "$_[0]/driverupdate";
      }

      if(-d "$_[0]/linux/suse") {
        $dud = 'dir';
      }
      elsif(grep { m#/\d+/linux/suse$# } glob "$_[0]/[0-9]*/linux/suse") {
        $dud = 'dir';
      }
    }

    if(!$dud) {
      my $ft = { type => ($for_initrd ? 'initrd' : $for_iso ? 'iso' : 'instsys'), file => $_[0] };

      File::Find::find(sub {
        if(-f $_) {
          my $f = `file -b -L $_ 2>/dev/null`;

          if($f =~ /^ELF/) {
            my $ar = get_file_arch $_;
            $ft->{arch} = $ar if defined $ar;
          }
        }
      }, $_[0]);

      push @files, $ft;

      return;
    }
  }
  elsif($for_instsys && -f $_[0] && $_[0] =~ m#(^|/)(update\.(pre|post|post2)|module\.(order|config))$#) {
    push @files, { type => $2, file => $_[0] };

    return;
  }
  elsif($for_instsys && -f $_[0] && $_[0] =~ m#(^|/)(.*\.(ycp|ybc|rb))$#) {
    push @files, { type => $3, file => $_[0] };

    return;
  }
  elsif($for_instsys && -f $_[0] && -s _ && -T _) {
    open my $f, $_[0];
    local $/;	# complete file
    my $l = <$f>;
    close $f;
    if($l =~ /^#!/) {
      push @files, { type => 'bin', file => $_[0] } if -x $_[0];
      return;
    }
    elsif($l =~ /^-----BEGIN PGP PUBLIC KEY BLOCK-----/m) {
      push @files, { type => 'pubkey', file => $_[0] };
      return;
    }
    else {
      push @files, { type => 'doc', file => $_[0] };
      return;
    }
  }
  elsif($for_instsys) {
    for (`gpg --homedir=$sign_key_dir --verify $_[0] 2>&1`) {
      chomp;
      $gpg_sign = $1, last if /^gpg: Signature made\s*(.*)$/;
    }
    if($gpg_sign) {
      my $z = `$gpg $_[0] | file -b -`;
      if($z =~ /^(gzip|XZ) compressed data/) {
        my $cmd = "\L$1";
        my $f = $cmd ne 'gzip' ? $cmd : 'gz';
        my $z = `$gpg $_[0] | $cmd -dc | file -b -`;
        $dud = "$1.$f" if $z =~ / (cpio|tar) archive/;
       $dud = "iso.$f" if $z =~ / ISO 9660 CD-ROM /;
      }
    }
  }

  if($dud) {
    my $duds = 0;

    my $old = sprintf "%s/%04d", $tmp_old, $dud_cnt++;
    die "$old: $!\n" unless mkdir $old;
    if($dud =~ /^(cpio|tar)(\.(gz|xz|rpm))?$/) {
      my $cmd = "cpio --quiet -dmiu --no-absolute-filenames";
      $cmd = "tar -xpf -" if $1 eq "tar";
      my $compr = 'cat';
      $compr = 'gzip -dc' if $3 eq 'gz';
      $compr = 'xz -dc' if $3 eq 'xz';
      $compr = 'rpm2cpio' if $3 eq 'rpm';
      if($gpg_sign) {
        system "$gpg $_[0] | $compr | ( cd $old ; $cmd 2>/dev/null)";
      }
      else {
        system "$compr $_[0] | ( cd $old ; $cmd 2>/dev/null)";
      }
    }
    elsif($dud =~ /^iso\.(gz|xz)$/) {
      my $compr = 'gzip -dc';
      $compr = 'xz -dc' if $1 eq 'xz';
      my $tmp_file = $tmp->file();
      if($gpg_sign) {
        system "$gpg $_[0] | $compr > $tmp_file";
      }
      else {
        system "$compr $_[0] > $tmp_file";
      }
      if(!$>) {
        system "mount -oro,loop $tmp_file $tmp_mnt";
        file_type "$tmp_mnt", 1;
        system "umount $tmp_mnt";
      }
      else {
        print STDERR "$_[0]: error: need root permissions to analyze iso images\n";
      }
      unlink $tmp_file;
      $dud = 'dummy';
      $duds = 1;
    }
    elsif($dud eq 'dir') {
      copy_dud $_[0], $old;
    }
    elsif($dud eq 'dummy') {
      $duds = 1;
    }

    $duds = analyze_dud $old, $gpg_sign if $dud ne 'dummy';

    return if $duds;
  }

  print STDERR "$_[0]: error: don't know what to do with it\n" unless $_[1];
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# copy_dud($src, $dst)
#
# Copy driver update. Copy only files we think belong
# to the update.
#
sub copy_dud
{
  local $_;
  my $src = shift;
  my $dst = shift;
  my $file_cnt = 0;
  my $other_dirs = 0;

  for (<$src/*>) {
    if(-f $_) { $file_cnt++; next }
    if(-d $_) {
      (my $fn = $_) =~ s#^.*/##;
      if($fn eq 'linux' && -d "$_/suse") {
        system "cp -r $_ $dst/linux";
      }
      elsif($fn =~ /^\d+$/ && -d "$_/linux/suse") {
        system "cp -r $_ $dst/$fn";
      }
      else {
        $other_dirs++;
      }
    }
  }

  # print "dirs = $other_dirs, files = $file_cnt\n";

  # assume this is a pure driver update and copy the files, too
  if(!$other_dirs) {
    for (<$src/*>) {
      system "cp $_ $dst" if -f $_ && !m#/driverupdate$#;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $duds = analyze_dud($dir)
#
# Look for driver updates in $dir and register them in file list.
# Return number of updates found.
#
sub analyze_dud
{
  local $_;
  my $src = shift;
  my $sign = shift;
  my $duds = 0;
  my $global_files = 0;

  if(-d "$src/linux/suse") {
    my $ft = { type => 'dud', file => $src };

    $ft->{sign} = $sign if $sign;

    push @files, $ft;

    $duds++;
  }
  else {
    $global_files = 1;
  }

  for (<$src/[0-9]*/linux/suse>) {
    next unless s#(/\d+)/linux/suse$#$1#;

    my $ft = { type => 'dud', file => $_ };
    $ft->{sign} = $sign if $sign;

    if($global_files) {
      $global_files = 0;
      $ft->{global_files} = $src;
    }

    push @files, $ft;

    $duds++;
  }

  return $duds;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $ok = new_dud($dir)
#
# Create new driver update in $tmp_dud.
#
# Return 1 if we succeeded.
#
sub new_dud
{
  local $_;

  my $dud_ok = 0;

  my @dists = @dists;
  my $dist = shift @dists;

  analyze_ycp_files \@files;

  mkdir "$tmp_dud/linux", 0755;
  mkdir "$tmp_dud/linux/suse", 0755;

  my $has_pubkeys;

  # one id per update, not for every arch
  my $id;
  chomp($id = `uuidgen 2>/dev/null`);
  $id = sprintf("%06x%06x%04x", rand(1<<24), rand(1<<24), rand(1<<16)) unless $id;

  my $installer = installer_from_dist($dist);

  for my $arch (sort keys %arch) {
    my $base = "$tmp_dud/linux/suse/$arch-$dist";

    mkdir $base, 0755;

    for (@dists) {
      symlink "$arch-$dist", "$tmp_dud/linux/suse/$arch-$_";
    }

    open my $cfg, ">$base/dud.config";
    print $cfg "# created by mkdud $VERSION\n";
    print $cfg "UpdateID:\t$id\n";

    for (@opt_condition) {
      my $x = $_;
      $x =~ s#.*/##;
      next if $x eq "";
      print $cfg "Exec:\t\t/update/*/install/mkdud.$id.if $x\n";
    }

    print $cfg "UpdateInstaller:\t$installer\n";

    my $has_update_name = @opt_name;

    # special case: allow otherwise empty update if a name was given explicitly
    $dud_ok = 1 if $has_update_name;

    print $cfg "UpdateName:\t$_\n" for (@opt_name);

    # ---------------------------------------------------------
    # install, inst-sys, modules, y2update

    my @rpms;
    my $scripts;

    if($installer ne 'yast' && @opt_exec) {
      for my $x (@opt_exec) {
        push @$hooks, { dir => "pre-pivot", cmd => $x, script_name => "99-zz-mkdud.sh" };
      }
    }

    if($installer ne 'yast' && (@opt_hook || $hooks)) {
      for my $x (@opt_hook) {
        # pre-pivot:99-xxx.sh
        # pre-pivot:foobar

        die "invalid dracut hook spec: $x\n" if $x !~ /^([^:]+):(.+)$/;

        my $hook_dir = $1;
        my $cmd = $2;

        die "invalid dracut hook: $hook_dir\n" if !$dracut_hooks{$hook_dir} && $opt_check_hooks;

        my $script_name;
        my $f;

        for($f = 0; $f < @files; $f++) {
          my $n = $files[$f]{file};
          $n =~ s#.*/##;
          last if $n eq $cmd;
        }

        undef $f if $f >= @files;

        if($cmd =~ /^\d\d-\S+\.sh$/) {
          die "$cmd: dracut hook script missing\n" unless defined $f;
          $script_name = $cmd;
          undef $cmd;
        }
        else {
          undef $f;
          $script_name = "99-zz-mkdud.sh";
        }

        my $h = { dir => $hook_dir, script_name => $script_name };

        $h->{cmd} = $cmd if $cmd;

        if(defined $f) {
          $h->{file} = $files[$f];
          splice @files, $f, 1;
        }

        push @$hooks, $h;
      }

      # print Dumper $hooks;

      if($hooks) {
        $dud_ok = 1;
        mkdir "$base/initrd", 0755;
        open my $f, ">>", "$base/initrd/.update.initrd.$id";
        close $f;
        File::Path::mkpath("$base/initrd/var/lib/dracut/hooks", { mode => 0755 });

        for my $h (@$hooks) {
          my $dir = "$base/initrd/var/lib/dracut/hooks/$h->{dir}";
          File::Path::mkpath($dir, { mode => 0755 });
          my $s = "$dir/$h->{script_name}";
          if($h->{cmd}) {
            my $sh = "#! /usr/bin/sh\n";
            $sh = "" if -f $s;
            open my $fd, ">>", "$dir/$h->{script_name}";
            print $fd $sh;
            print $fd "$h->{cmd}\n";
            chmod 0755, $fd;
            close $fd;
          }
          elsif($h->{file}) {
            system "cp '$h->{file}{file}' '$s'";
            chmod 0755, $s;
          }
        }
      }
    }

    for (@files) {
      next if $_->{arch} && $_->{arch} ne $arch;

      if($_->{type} eq 'doc') {
        $dud_ok = 1;
        system "cp '$_->{file}' $tmp_dud";
      }

      if($_->{type} eq 'instsys') {
        $dud_ok = 1;
        mkdir "$base/inst-sys", 0755;
        open my $f, ">>", "$base/inst-sys/.update.$id";
        close $f;
        system "cp -a '$_->{file}'/* $base/inst-sys";
      }

      if($_->{type} eq 'initrd') {
        $dud_ok = 1;
        mkdir "$base/initrd", 0755;
        open my $f, ">>", "$base/initrd/.update.initrd.$id";
        close $f;
        system "cp -a '$_->{file}'/* $base/initrd";
      }

      if($_->{type} eq 'iso') {
        $dud_ok = 1;
        mkdir "$base/iso", 0755;
        open my $f, ">>", "$base/iso/.update.iso.$id";
        close $f;
        system "cp -a '$_->{file}'/* $base/iso";
      }

      if($_->{type} eq 'rpm') {
        print $cfg "UpdateName:\t$_->{canonical_name}\t$_->{date}\n" if !@opt_name;
        $has_update_name = 1;

        push @rpms, $_;

        if($opt_install{repo} || $opt_install{rpm}) {
          $dud_ok = 1;
          mkdir "$base/install", 0755;
          system "cp '$_->{file}' '$base/install/$_->{canonical_name}.rpm'";
        }

        if($opt_install{instsys}) {
          $dud_ok = 1;
          mkdir "$base/inst-sys", 0755;
          open my $f, ">>", "$base/inst-sys/.update.$id";
          print $f "package: $_->{canonical_name}\n";
          close $f;
          system "rpm2cpio $_->{file} | ( cd $base/inst-sys ; cpio --quiet --sparse -dimu --no-absolute-filenames )";

          # for agama, go via /etc/alternatives
          # for yast, link directly
          my $direct_links = $installer eq 'yast' ? 1 : 0;
          apply_alternatives "$base/inst-sys", $_->{alternatives}, $direct_links;
        }
      }

      if($_->{type} eq 'rpm_initrd') {
        $dud_ok = 1;
        mkdir "$base/initrd", 0755;
        open my $f, ">>", "$base/initrd/.update.initrd.$id";
        print $f "package: $_->{canonical_name}\n";
        close $f;
        system "rpm2cpio $_->{file} | ( cd $base/initrd ; cpio --quiet --sparse -dimu --no-absolute-filenames )";

        # link directly, not going via /etc/alternatives
        apply_alternatives "$base/initrd", $_->{alternatives}, 1;
      }

      # FIXME: this is unlikely to be useful...
      if($_->{type} eq 'rpm_iso') {
        $dud_ok = 1;
        mkdir "$base/iso", 0755;
        open my $f, ">>", "$base/iso/.update.iso.$id";
        print $f "package: $_->{canonical_name}\n";
        close $f;
        system "rpm2cpio $_->{file} | ( cd $base/iso ; cpio --quiet --sparse -dimu --no-absolute-filenames )";
      }

      if($_->{type} eq 'bin' || $_->{type} eq 'lib') {
        $dud_ok = 1;
        mkdir "$base/install", 0755;
        system "cp '$_->{file}' $base/install/";
      }

      if($_->{type} eq 'pubkey') {
        $dud_ok = 1;
        File::Path::mkpath("$base/inst-sys/usr/lib/rpm/gnupg/keys", { mode => 0755 });
        my $n = get_sign_key_name($_->{file});
        if($n) {
          system "cp '$_->{file}' '$base/inst-sys/usr/lib/rpm/gnupg/keys/$n'";
        }
      }

      if($_->{type} eq 'module.order') {
        $dud_ok = 1;
        mkdir "$base/modules", 0755;
        system "cat '$_->{file}' >>$base/modules/module.order";
      }

      if($_->{type} eq 'module.config') {
        $dud_ok = 1;
        mkdir "$base/modules", 0755;
        system "cat '$_->{file}' >>$base/modules/module.config";
      }

      if($_->{type} eq 'module') {
        $dud_ok =1;
        mkdir "$base/modules", 0755;
        system "cp '$_->{file}' $base/modules";
        my $v = "\t$_->{mod_version}" if defined $_->{mod_version};
        print $cfg "UpdateName:\t$_->{file}\t$_->{version}.$_->{arch}$v\n" if !@opt_name;
        $has_update_name = 1;
      }

      if($_->{type} eq 'y2update') {
        $dud_ok = 1;
        system "cp -r '$_->{file}' $base";
      }
      elsif($_->{type} eq 'ycp' || $_->{type} eq 'ybc' || $_->{type} eq 'rb') {
        if(!$_->{location}) {
          print STDERR "$_->{file}: error: don't know where to put it\n";
        }
        else {
          $dud_ok = 1;
          my $d = "$base/y2update/$_->{location}";
          File::Path::mkpath($d);
          if(-d $d) {
            system "cp '$_->{file}' $d";
          }
          else {
            print STDERR "$d: failed to create directory\n"
          }
        }
      }
    }

    if(-d "$base/inst-sys" ) {
      system "chmod 755 `find $base/inst-sys -type d`";

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

      if(glob("$base/inst-sys/usr/lib/rpm/gnupg/keys/*.asc")) {
        $has_pubkeys = 1;
      }
    }

    if(-d "$base/initrd" ) {
      system "chmod 755 `find $base/initrd -type d`";

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

      if(glob("$base/initrd/usr/lib/rpm/gnupg/keys/*.asc")) {
        $has_pubkeys = 1;
      }
    }

    if($installer eq "yast" && $opt_fix_yast) {
      if(lstat "$base/inst-sys/sbin/yast") {
        if(vercmp($yast_version, $REPLACEABLE_YAST) < 0 ) {
          print STDERR
            "***  Note: prevented driver update from replacing /sbin/yast.\n" .
            "***    yast2 >=$REPLACEABLE_YAST is considered safely replacable.\n" .
            "***    If you really need to replace yast, use --no-fix-yast.\n";
          unlink "$base/inst-sys/sbin/yast";
        }
      }
    }

    if($installer eq "yast" && $opt_fix_usr_src) {
      if(-e "$base/inst-sys/usr/src/packages") {
        print STDERR
          "***  Note: prevented driver update from including /usr/src/packages.\n" .
          "***    If you really need this directory, use --no-fix-usr-src.\n";
        system "rm -rf $base/inst-sys/usr/src/packages";
      }
    }

    if($installer eq "yast" && $opt_fix_adddir) {
      # only for sle11, sle12, leap42.X, caasp1.X, caasp2.X, caasp3.X
      if(grep { /^(sle[sd]?1[12]|leap42\.|caasp[1-3]\.)/ } (@dists, $dist)) {
        my $danger = 0;
        $danger ||= -e "$base/inst-sys/usr/bin/$_" for qw (chmod chown cp ln mktemp mv readlink rm);
        if($danger) {
          print STDERR
            "***  Note: included an updated adddir script.\n" .
            "***    If you do not need this, use --no-fix-adddir.\n";
          mkdir "$base/install", 0755;
          create_adddir "$base/install/adddir";

          push @opt_exec, "cp adddir /sbin";
        }
      }
    }

    print $cfg "UpdateName:\tUpdate $id\n" if !$has_update_name;

    if($installer eq 'yast' && @opt_exec) {
      $dud_ok = 1;
      mkdir "$base/install", 0755;

      print $cfg "Exec:\t\t/update/*/install/mkdud.$id.sh\n";

      my $c = <<'= = = = = = = =';
#! /bin/bash

# script generated by mkdud <version>

# locate dud directory
dud=${0%/install/*}

[ -d "$dud" ] || exit 1

export dud

PATH=$dud/install:/bin:/sbin:/usr/bin:/usr/sbin

cd $dud/install

# run these commands

<binary>

# remove driver update when you're done:

# rm -rf $dud/*
= = = = = = = =

      $c =~ s#<version>#$VERSION#;
      $c =~ s#<binary>#join("\n", @opt_exec)#e;

      open my $x, ">$base/install/mkdud.$id.sh";
      print $x $c;
      close $x;

      chmod 0755, "$base/install/mkdud.$id.sh";
    }

    if($installer eq 'yast' && @opt_condition) {
      $dud_ok = 1;
      mkdir "$base/install", 0755;

      my $c = <<'= = = = = = = =';
#! /bin/bash

# script generated by mkdud <version>

# locate dud directory
dud=${0%/install/*}

[ -d "$dud" ] || exit 1

export dud

PATH=$dud/install:/bin:/sbin:/usr/bin:/usr/sbin

cd $dud/install

[ -x "./mkdud.$1" ] || exit 1

"./mkdud.$1" || {
  echo "The following Driver Update will *NOT* be applied:" >&2
  echo "The following Driver Update will *NOT* be applied:"

  rm -rf $dud/*

  exit 1
}

exit 0
= = = = = = = =

      $c =~ s#<version>#$VERSION#;

      open my $x, ">$base/install/mkdud.$id.if";
      print $x $c;
      close $x;

      chmod 0755, "$base/install/mkdud.$id.if";

      for (@opt_condition) {
        if(/^ServicePack(\d+)$/) {
          my $sp = get_service_pack $dist, $1;
          if($sp) {
            open my $x, ">$base/install/mkdud.$_";
            print $x $sp;
            close $x;
            chmod 0755, "$base/install/mkdud.$_";
          }
          else {
            die "error: no condition check for $dist $_\n";
          }
        }
        elsif(-f) {
          my $x = $_;
          $x =~ s#.*/##;
          if($x ne "") {
            system "cp $_ $base/install/mkdud.$x";
            chmod 0755, "$base/install/mkdud.$x";
          }
        }
        else {
          die "error: no such condition file: $_\n";
        }
      }
    }

    if(@rpms && $opt_install{repo} && $installer eq "yast") {
      $dud_ok = 1;
      mkdir "$base/install", 0755;

      my $c = <<'= = = = = = = =';
#! /usr/bin/perl

# script generated by mkdud <version>

$dst = "/add_on_products.xml";

$prio = _prio_;

($base = $0) =~ s#(/[^/]*){2}$##;
($id = $base) =~ s#^.*/##;

mkdir "$base/repo", 0755;
system "mv $base/install/*.rpm $base/repo";

$id += 0;
$id3 = sprintf "%03u", $id;

@f = split /^/m, <<'# 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

$product = <<"# product";
        <product_item>
            <name>Driver Update $id</name>
            <url>dir:///update/$id3/repo?alias=DriverUpdate$id</url>
            <priority config:type="integer">$prio</priority>
        </product_item>
# product

@f = (<F>) if open F, $dst;

open F, ">", $dst;
for (@f) {
  print F $product if m#\s*</product_items>#;
  print F;
}
close F;

= = = = = = = =

      $c =~ s#<version>#$VERSION#;
      $c =~ s/_prio_/$opt_prio/;
      $c =~ s/"mv /"ln / if $opt_install{rpm};

      push @{$scripts->{'update.pre'}}, $c;

      $c = <<'= = = = = = = =';
#! /bin/bash

# script generated by mkdud <version>

dir=${0%/*/*}
dir=${dir#/*/}

repo="baseurl=dir:(//)?/$dir/repo"

for i in `grep -El $repo /etc/zypp/repos.d/*` ; do
  [ -f "$i" ] && rm "$i"
done
= = = = = = = =

      $c =~ s#<version>#$VERSION#;

      push @{$scripts->{'update.post2'}}, $c;
    }

    if($has_pubkeys && $installer eq 'yast') {
      my $c = <<'= = = = = = = =';
#! /bin/bash

# script generated by mkdud <version>

dir=${0%/*/*}
dir=${dir#/*/}

repo="baseurl=dir:///$dir/repo"

touch /installkey.gpg
gpg --batch --homedir /root/.gnupg --no-default-keyring --ignore-time-conflict --ignore-valid-from --keyring /installkey.gpg --import /usr/lib/rpm/gnupg/keys/*
= = = = = = = =

      $c =~ s#<version>#$VERSION#;

      push @{$scripts->{'update.pre'}}, $c;
    }

    for (@files) {
      if($_->{type} =~ /^update\.(pre|post|post2)/) {
        my $s = `cat $_->{file}`;
        push @{$scripts->{$_->{type}}}, $s;
      }
    }

    if($scripts) {
      $dud_ok = 1;
      mkdir "$base/install", 0755;

      for (sort keys %$scripts) {
        my $s = $scripts->{$_};

        open my $x, ">$base/install/$_";

        if(@$s > 1) {
          print $x "# /bin/bash\n\n# generated by mkdud $VERSION\n\ndir=\$\{0\%/\*\}\n\n";

          for(my $i = 0; $i < @$s; $i++) {
            print $x sprintf("\$dir/%02u_%s\n", $i + 1, $_);
            open my $f, ">", sprintf("%s/install/%02u_%s", $base, $i + 1, $_);
            print $f $s->[$i];
            chmod 0755, $f;
            close $f;
          }
        }
        else {
          print $x $s->[0];
        }

        chmod 0755, $x;
        close $x;
      }
    }

    for (@opt_config) {
      $dud_ok = 1;
      if(/^(\S+?)\s*[:=](.*)$/) {
        my $key = $1;
        my $val = $2;
        $val =~ s/^\s*|\s*$//g;
        print $cfg "$key:\t$val\n";
      }
    }

    close $cfg;
  }

  if($use_all_archs) {
    my @a = @all_archs;
    shift @a;

    for my $arch (@a) {
      for ($dist, @dists) {
        symlink "$all_archs[0]-$_", "$tmp_dud/linux/suse/$arch-$_";
      }
    }
  }

  return $dud_ok;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# analyze_ycp_files(\@files)
#
# Go through file list and assign locations to all ycp, ycb, and rb files.
# ycb files are placed along their corresponding ycp files.
#
sub analyze_ycp_files
{
  my $files = shift;
  local $_;

  my %ycp;

  for (@$files) {
    if($_->{type} eq 'ycp' || $_->{type} eq 'rb') {
      open my $f, $_->{file};
      while(my $x = <$f>) {
        last if $x =~ /^\s*(\{|module)/;	# search util real code starts...
        last if $. > 100;			# but only at first 100 lines
        if($x =~ /\bFile:\s*(\S+)\/[^\/]+\.(ycp|rb)\b/) {
          $_->{location} = $1;
          $ycp{$_->{file}} = $1;
        }
        elsif($x =~ /\bFile:\s*$/) {
          $x = <$f>;
          if($x =~ /^\s*(\*|#)\s*(\S+)\/[^\/]+\.(ycp|rb)\b/) {
            $_->{location} = $2;
            $ycp{$_->{file}} = $2;
          }
        }
      }
      close $f;
    }
  }

  for (@$files) {
    if($_->{type} eq 'ybc') {
      my $ycp = $_->{file};
      $ycp =~ s/\.ybc$/\.ycp/;
      if(defined $ycp{$ycp}) {
        $_->{location} = $ycp{$ycp};
      }
      else {
        print STDERR "$_->{file}: warning: don't know where to put it, assuming 'modules'\n";
        $_->{location} = "modules";
      }
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub set_mkisofs_metadata
{
  my $id = "dud";

  # get update id
  for my $d (@files) {
    next if $d->{type} ne 'dud';

    for (glob("$d->{file}/linux/suse/*")) {
      next if -l $_;
      if(open my $f, "$_/dud.config") {
        while(<$f>) {
          next if /^\s*#/;
          if(/^\s*(\S+)\s*[:=]\s*(.*?)\s*$/) {
            my $key = $1;
            my $val = $2;
            # print "$key -- >$val<\n";
            if("\L$key" eq 'updateid') {
              $id = "dud $val";
            }
          }
        }
        close $f;
      }
    }
  }

  $opt_application = $id if !defined $opt_application;
  $opt_volume = "OEMDRV" if !defined $opt_volume;
  $opt_vendor = "mkdud $VERSION" if !defined $opt_vendor;
  $opt_preparer = "mkdud $VERSION" if !defined $opt_preparer;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $dud_dir = write_dud(\@files, $file_name)
#
# Write driver update to $file_name.
#
sub write_dud
{
  my $files = shift;
  my $file_name = shift;
  local $_;

  my @duds;

  for (@$files) {
    push @duds, $_ if $_->{type} eq 'dud';
  }

  if(!@duds) {
    print STDERR "Empty driver update not created.\n";

    return undef;
  }

  my $tmp_src = (@duds == 1 && !defined($opt_dud_prefix)) ? $duds[0]{file} : $tmp_new;

  if(@duds > 1 || defined($opt_dud_prefix)) {
    my $dud_cnt = defined($opt_dud_prefix) ? $opt_dud_prefix : 1;
    $dud_cnt = 1 if $dud_cnt < 0;
    for my $d (@duds) {
      my $n = sprintf "%s/%02d", $tmp_new, $dud_cnt++;
      die "$n: $!\n" unless mkdir $n;
      die "$d->{file}/linux -> $n/linux: $!" unless rename "$d->{file}/linux", "$n/linux";

      if($d->{global_files}) {
        for (glob("$d->{global_files}/*")) {
          system "cp '$_' $tmp_new" if -f $_;
        }
      }

      for (glob("$d->{file}/*")) {
        system "cp '$_' $n" if -f $_;
      }
    }
  }

  my $cmd_archive = 'find . | cpio --quiet -o -H newc -R 0:0';
  $cmd_archive = 'tar -cf - .' if $format_archive eq 'tar';

  if($format_archive eq 'iso') {
    set_mkisofs_metadata;
    $cmd_archive = "$mkisofs -l -r -pad -input-charset utf8";
    $cmd_archive .= " -V '" . substr($opt_volume, 0, 32) . "'";
    $cmd_archive .= " -A '" . substr($opt_application, 0, 128) . "'";
    $cmd_archive .= " -p '" . substr($opt_preparer, 0, 128) . "'";
    $cmd_archive .= " -publisher '" . substr($opt_vendor, 0, 128) . "'";
    $cmd_archive .= " $tmp_src 2>/dev/null";
  }

  my $compr = 'cat';
  $compr = 'gzip -9c' if $format_compr eq 'gz';
  $compr = 'xz --check=crc32 -c' if $format_compr eq 'xz';

  system "cd $tmp_src; $cmd_archive | $compr >$tmp_archive";

  if($format_rpm) {
    $tmp_archive = repack_as_rpm $tmp_archive;
  }

  if($opt_sign) {
    if($format_rpm) {
      sign_rpm $tmp_archive;
    }
    else {
      sign_file $tmp_archive;
      if(!$opt_sign_direct) {
        system "cp ${tmp_archive}.asc ${file_name}.asc";
        print "created detached signature ${file_name}.asc\n";
      }
    }
  }

  system "cp -T $tmp_archive $file_name" and die "failed to create driver update\n";

  return $tmp_src;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# fix_duds($files)
#
# Make sure every driver update has a mininal dud.config.
#
sub fix_duds
{
  my $files = shift;
  local $_;

  for my $d (@$files) {
    next if $d->{type} ne 'dud';

    my $id;
    chomp($id = `uuidgen 2>/dev/null`);
    $id = sprintf("%06x%06x%04x", rand(1<<24), rand(1<<24), rand(1<<16)) unless $id;

    for (glob("$d->{file}/linux/suse/*")) {
      next if -l $_;
      next if -s "$_/dud.config";
      open my $f, ">$_/dud.config";
      print $f "UpdateID:\t$id\n";
      print $f "UpdateName:\tUpdate $id\n";
      close $f;
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $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;

  local $_;

  my $x;

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

  return $x;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# show_dud($dud)
#
# Print driver update summary.
#
# $dud is a reference to a file type element.
#
sub show_dud
{
  my $dud = shift;
  local $_;

  my $log;
  my $section;

  $dud_cnt++;

  print "===  Update #$dud_cnt  ===\n";

  if($dud->{sign}) {
    print "  = Signed: $dud->{sign} =\n";
  }

  if($dud->{global_files}) {
    for (glob("$dud->{global_files}/*")) {
      if(-f $_) {
        s#^.*/##;
        push @{$log->{docs}}, $_;
      }
    }
  }

  for (glob("$dud->{file}/*")) {
    if(-f $_) {
      s#^.*/##;
      push @{$log->{docs}}, $_;
    }
  }

  if($log->{docs}) {
    print "  [Documentation]\n";
    print format_array($log->{docs}, 4), "\n";
  }

  for my $part (glob("$dud->{file}/linux/suse/*")) {
    my $p = $part;
    $p =~ s#^.*/##;
    next if $p eq "";
    if(-l $part && -d $part) {
      my $l = abs_path $part;
      $l =~ s#^.*/##;
      next if $l eq "";
      push @{$section->{$l}{links}}, $p;
    }
    elsif(-d $part) {
      $section->{$p}{log} = show_single_dir $part;
    }
  }

  # join sections with identical output

  my %i;
  for (sort keys %$section) {
    if(defined $i{$section->{$_}{log}}) {
      my $x = $i{$section->{$_}{log}};
      push @{$section->{$x}{links}}, $_;
      push @{$section->{$x}{links}}, @{$section->{$_}{links}} if defined $section->{$_}{links};
    }
    else {
      $i{$section->{$_}{log}} = $_;
    }
  }

  for (values %i) {
    my $s = $section->{$_};
    push @{$s->{links}}, $_;

    print "  [";
    print nice_arch_list($s->{links});
    print "]\n";

    print $s->{log};
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $string = nice_arch_list(\@list)
#
# Put a list of <arch>-<dist> values into something more compact and better
# readable.
#
sub nice_arch_list
{
  my $l = shift;
  local $_;
  my $all_ar = join ", ", sort @all_archs;

  # split by known products

  my %p1;
  for (@$l) {
    if(/^(\S+)-((sle([sd]?)|fedora|rhel|sll)(\d+)|(leap|kubic|casp|caasp|suse-microos)?(\d+\.\d+)|tw)$/) {
      # print STDERR "+++ 1=$1 2=$2 3=$3 4=$4 5=$5 6=$6 7=$7 +++\n";
      if($3 eq 'sles') {
        push @{$p1{"SUSE Linux Enterprise Server $5"}}, $1;
      }
      elsif($3 eq 'sled') {
        push @{$p1{"SUSE Linux Enterprise Desktop $5"}}, $1;
      }
      elsif($3 eq 'sle') {
        push @{$p1{"SUSE Linux Enterprise $5"}}, $1;
      }
      elsif($6 eq 'leap') {
        push @{$p1{"openSUSE Leap $7"}}, $1;
      }
      elsif($6 eq 'kubic') {
        push @{$p1{"openSUSE Kubic $7"}}, $1;
      }
      elsif($6 eq 'casp') {
        push @{$p1{"SUSE Containers as a Service Platform $7"}}, $1;
      }
      elsif($6 eq 'caasp') {
        push @{$p1{"SUSE Containers as a Service Platform $7"}}, $1;
      }
      elsif($6 eq 'suse-microos') {
        push @{$p1{"SUSE Linux Enterprise Micro $7"}}, $1;
      }
      elsif($2 eq "tw") {
        push @{$p1{"openSUSE Tumbleweed"}}, $1;
      }
      elsif($3 eq 'fedora') {
        push @{$p1{"Fedora $5"}}, $1;
      }
      elsif($3 eq 'rhel') {
        push @{$p1{"Red Hat Enterprise Linux $5"}}, $1;
      }
      elsif($3 eq 'sll') {
        push @{$p1{"SUSE Liberty Linux $5"}}, $1;
      }
      else {
        push @{$p1{"openSUSE $2"}}, $1;
      }
    }
    else {
      push @{$p1{$_}}, "";
    }
  }

  # print arch list

  my %p2;
  for (keys %p1) {
    $p2{$_} = join ", ", sort @{$p1{$_}};
  }

  # join sles + sled to sle

  for (sort keys %p2) {
    my $s = $_;
    if($s =~ s/ Desktop / Server / && $p2{$s} eq $p2{$_}) {
      my $e = $_;
      $e =~ s/ Desktop / /;
      $p2{$e} = $p2{$_};
      delete $p2{$_};
      delete $p2{$s};
    }
  }

  # print final string

  my @l;
  for (sort keys %p2) {
    my $al = $p2{$_};
    $al = "" if $al eq $all_ar;
    push @l, $_ . ($al ? " ($al)" : "");
  }

  return join ", ", @l;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $string = show_single_dir($dir)
#
# Return summary of single driver update (what's
# in /linux/suse/<arch>-<dist>/).
#
sub show_single_dir
{
  my $dir = shift;

  my (@i, %i);
  my $id;

  my $installer_names = { agama => "Agama", anaconda => "Anaconda", yast => "YaST" };

  # ----------------------------
  # read config file

  my $installer = "yast";

  my %sect;
  open my $f, "$dir/dud.config";
  while(<$f>) {
    next if /^\s*#/;
    if(/^\s*(\S+)\s*[:=]\s*(.*?)\s*$/) {
      my $key = $1;
      my $val = $2;
      # print "$key -- >$val<\n";
      if("\L$key" eq 'updateid') {
        $id = $val;
        $sect{id} = "      $val\n";
      }
      elsif("\L$key" eq 'updateinstaller') {
        $installer = installer_from_config $val;
      }
      elsif("\L$key" eq 'updatename') {
        $sect{name} .= "      $val\n";
      }
      elsif("\L$key" eq 'boot') {
        $sect{boot} .= "      $val\n";
      }
      elsif("\L$key" eq 'exec') {
        my $ok = 0;
        if($val =~ m#/update/\*/install/(mkdud\..*\.sh)$#) {
          open my $f, "$dir/install/$1";
          chomp(my @f = (<$f>));
          close $f;
          for my $x (@f) {
            if($x =~ /^# run these commands/ ... $x =~ /^\s*#/) {
              $ok = 1;
              $sect{exec} .= "      $x\n" if $x !~ /^\s*#|^\s*$/;
            }
          }
        }
        elsif($val =~ m#/update/\*/install/(mkdud\..*\.if)\s+(\S+)$#) {
          $ok = 1;
          $sect{condition} .= "      $2\n";
        }
        if(!$ok) {
          $sect{exec} .= "      $val\n";
        }
      }
      else {
        $sect{config} .= "      $key = $val\n";
      }
    }
  }
  close $f;

  if(open my $fd, "$dir/initrd/var/lib/dracut/hooks/pre-pivot/99-zz-mkdud.sh") {
    while(<$fd>) {
      chomp;
      next if /^#/;
      next if /^\s*$/;
      $sect{exec} .= "      $_\n";
    }
    close $fd;
  }

  $sect{installer} = "      $installer_names->{$installer}\n";

  # ----------------------------
  # update.* scripts

  for (glob("$dir/install/*update.*")) {
    s#^.*/##;
    s/^\d+/0/;
    $i{$_}++;
  }

  for (qw ( update.pre update.post update.post2 )) {
    next unless defined $i{$_};
    if($i{"0_$_"} > 1) {
      push @i, "$i{\"0_$_\"} x $_";
    }
    else {
      push @i, $_;
    }
  }

  $sect{scripts} = join ", ", @i;

  # ----------------------------
  # modules

  for (glob("$dir/modules/*${kext_glob}")) {
    my $f = $_;
    s#^.*/##;
    my $n = $_;
    my $ar = get_file_arch $f;

    if(defined $ar) {
      @i = split " ", `modinfo -F vermagic $f 2>/dev/null`;
      my $v = $i[0];
      my $mv = `modinfo -F version $f 2>/dev/null`;
      chomp $mv;
      $mv = ", $mv" if $mv ne "";
      $sect{modules} .= "      $n ($v.$ar$mv)\n";
    }
    else {
      $sect{modules} .= "      $n\n";
    }
  }

  $sect{modules} .= "      module.order\n" if -f "$dir/modules/module.order";
  $sect{modules} .= "      module.config\n" if -f "$dir/modules/module.config";

  # ----------------------------
  # packages

  for (glob("$dir/install/*.rpm")) {
    my $f = $_;
    s#^.*/##;
    my $n = $_;

    $_ = `file -b -L $f 2>/dev/null`;
    if(/^RPM/) {
      my $r = `rpm --nosignature -qp --qf '%{NAME}-%{VERSION}-%{RELEASE}.%{ARCH}' $f 2>$tmp_err`;
      if($r ne "") {
        my $ar = $f =~ /\.([^.]+)$/ ? $1 : undef;
        $ar = "i386" if $ar =~ /^i.86$/;

        my $d = `rpm --nosignature -qp --qf '%{BUILDTIME}' $f 2>$tmp_err`;
        $d = gmtime $d if $d;

        $d = "$r, $d" if $f !~ /(^|\/)$r\.rpm$/;
        $sect{rpms} .= "      $n  ($d)\n";
      }
      else {
        $sect{rpms} .= "      $n\n";
      }
    }
    else {
      $sect{rpms} .= "      $n\n";
    }
  }

  if($sect{rpms}) {
    my $prio;
    my $type;

    for my $fn (glob("$dir/install/*update.pre")) {
      my $f;
      open $f, $fn;
      my @f = (<$f>);
      close $f;
      next unless grep { /^# script generated by mkdud / } @f;
      my @p;
      if(@p = grep { /^\$prio = / } @f and $p[0] =~ / = (\d+)/) {
        $prio = $1;
        my @t;
        if(@t = grep { /^system "(ln|mv) / } @f and $t[0] =~ /(ln|mv)/) {
          $type = $1;
        }
        last;
      }
    }

    my %i;

    if(defined $prio) {
      $i{repo} = 1;
      $i{rpm} = 1 if $type eq "ln";
    }
    else {
      $i{rpm} = 1;
    }

    $i{instsys} = 1 if -f "$dir/inst-sys/.update.$id";

    delete $i{rpm} if $installer eq "agama";
    $i{repo} = 1 if $installer eq "agama";

    my $l = "- install methods: " . join(", ", sort keys %i);
    $l .= " (repo priority $prio)" if defined $prio;

    $sect{rpms} = "      $l\n" . $sect{rpms};
  }


  # ----------------------------
  # y2update

  if(-d "$dir/y2update") {
    my $max_files = 10;

    chomp(my @f = `cd $dir/y2update; find . -type f`);
    @f = map { s#^\./##; $_ } sort @f;
    if(@f > $max_files + 1) {
      $sect{y2update} .= "      $_\n" for (@f[0 .. $max_files - 1]);
      my $x = @f - $max_files;
      $sect{y2update} .= "      ... ($x more files)\n";
    }
    else {
      $sect{y2update} .= "      $_\n" for (@f);
    }
  }

  # ----------------------------
  # instsys

  if(-d "$dir/inst-sys") {
    my $max_files = 10;

    chomp(my @f = `cd $dir/inst-sys; find . -type f`);
    @f = map { s#^\.##; $_ } sort @f;
    @f = grep { $_ ne "/.update.$id" } @f;
    @f = grep { ! m#^/usr/lib/rpm/gnupg/keys/[^/]+.asc$# } @f;

    if(open my $fd, "$dir/inst-sys/.update.$id") {
      my @l = <$fd>;
      close $fd;
      chomp @l;
      $sect{instsys} .= "      - $_.rpm\n" for @l;
    }

    if(@f > $max_files + 1) {
      $sect{instsys} .= "      $_\n" for (@f[0 .. $max_files - 1]);
      my $x = @f - $max_files;
      $sect{instsys} .= "      ... ($x more files)\n";
    }
    else {
      $sect{instsys} .= "      $_\n" for (@f);
    }
  }

  # ----------------------------
  # initrd

  if(-d "$dir/initrd") {
    my $max_files = 10;

    chomp(my @f = `cd $dir/initrd; find . -type f`);
    @f = map { s#^\.##; $_ } sort @f;
    @f = grep { $_ ne "/.update.initrd.$id" } @f;
    @f = grep { ! m#^/usr/lib/rpm/gnupg/keys/[^/]+.asc$# } @f;

    if(open my $fd, "$dir/initrd/.update.initrd.$id") {
      my @l = <$fd>;
      close $fd;
      chomp @l;
      $sect{initrd} .= "      - $_.rpm\n" for @l;
    }

    if(@f > $max_files + 1) {
      $sect{initrd} .= "      $_\n" for (@f[0 .. $max_files - 1]);
      my $x = @f - $max_files;
      $sect{initrd} .= "      ... ($x more files)\n";
    }
    else {
      $sect{initrd} .= "      $_\n" for (@f);
    }
  }

  # ----------------------------
  # iso

  if(-d "$dir/iso") {
    my $max_files = 10;

    chomp(my @f = `cd $dir/iso; find . -type f`);
    @f = map { s#^\.##; $_ } sort @f;
    @f = grep { $_ ne "/.update.iso.$id" } @f;

    if(open my $fd, "$dir/iso/.update.iso.$id") {
      my @l = <$fd>;
      close $fd;
      chomp @l;
      $sect{iso} .= "      - $_.rpm\n" for @l;
    }

    if(@f > $max_files + 1) {
      $sect{iso} .= "      $_\n" for (@f[0 .. $max_files - 1]);
      my $x = @f - $max_files;
      $sect{iso} .= "      ... ($x more files)\n";
    }
    else {
      $sect{iso} .= "      $_\n" for (@f);
    }
  }

  # ----------------------------
  # public rpm keys

  for (glob("$dir/inst-sys/usr/lib/rpm/gnupg/keys/* $dir/initrd/usr/lib/rpm/gnupg/keys/*")) {
    s#^.*/##;
    next unless /\.asc$/;
    $sect{pubkeys} .= "      $_\n";
    $sect{pubkeys} .= "      ($pubkey_info->{$_})\n" if $pubkey_info->{$_};
  }

  # ----------------------------
  # other files

  for (glob("$dir/install/*")) {
    s#^.*/##;
    next if /\.rpm$/;
    next if /update\.(pre|post|post2)$/;
    next if /^mkdud\./;
    $sect{other} .= "      $_\n";
  }

  # ----------------------------
  # howto

  my @is_ok = ( "✔", "✔", "✔", "✔", "✔" );

  $is_ok[0] = $is_ok[3] = "✘" if $sect{iso} || $sect{initrd} || $sect{boot} || ($sect{config} && $installer ne "yast");
  $is_ok[1] = $is_ok[2] = "✘" if $sect{iso} || $sect{initrd} || $sect{boot} || $installer ne "yast";

  $is_ok[0] = $is_ok[1] = $is_ok[2] = $is_ok[3] = "✘" if $installer eq "anaconda";

  my $opt = "dud";
  $opt = "inst.dud" if $installer eq "agama";

  $sect{howto} .= "      [$is_ok[0]] during installation: using boot option $opt=URL_TO_DUD_FILE\n";
  $sect{howto} .= "      [$is_ok[1]] during installation: unpacked on local file system with label 'OEMDRV'\n";
  $sect{howto} .= "      [$is_ok[2]] during installation: renamed as 'driverupdate' in installation repository\n";
  $sect{howto} .= "      [$is_ok[3]] rebuilding installation media using 'mkmedia --initrd DUD_FILE ...'\n";
  $sect{howto} .= "      [$is_ok[4]] rebuilding installation media using 'mkmedia --apply-dud DUD_FILE ...'\n";

  # ----------------------------
  # generate summary

  my $log;

  if($sect{condition}) {
    $log .= "    Conditions:\n$sect{condition}";
  }

  if($sect{name}) {
    $log .= "    Name:\n$sect{name}";
  }

  if($sect{id}) {
    $log .= "    ID:\n$sect{id}";
  }

  if($sect{installer}) {
    $log .= "    Installer:\n$sect{installer}";
  }

  if($sect{rpms}) {
    $log .= "    Packages:\n$sect{rpms}";
  }

  if($sect{modules}) {
    $log .= "    Modules:\n$sect{modules}";
  }

  if($sect{scripts}) {
    $log .= "    Scripts:\n      $sect{scripts}\n";
  }

  if($sect{y2update}) {
    $log .= "    YaST Update:\n$sect{y2update}";
  }

  if($sect{pubkeys}) {
    $log .= "    RPM Public Keys:\n$sect{pubkeys}";
  }

  if($sect{instsys}) {
    $log .= "    Installation System:\n$sect{instsys}";
  }

  if($sect{initrd}) {
    $log .= "    Initrd:\n$sect{initrd}";
  }

  if($sect{iso}) {
    $log .= "    ISO:\n$sect{iso}";
  }

  if($sect{other}) {
    $log .= "    Other Files:\n$sect{other}";
  }

  if($sect{exec}) {
    $log .= "    Run Commands:\n$sect{exec}";
  }

  if($sect{config}) {
    $log .= "    Config Entries:\n$sect{config}";
  }

  if($sect{boot}) {
    $log .= "    Boot Options:\n$sect{boot}";
  }

  if($sect{howto}) {
    $log .= "    How to apply this DUD:\n$sect{howto}";
  }

  return $log;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $string = get_service_pack($dist, $sp)
#
# Return shell script that checks for service pack $sp on $dist.
#
sub get_service_pack
{
  my $dist = shift;
  my $sp = shift;

  my ($dist_name, $dist_ver, $dist_ver_sp, $dist_version_var);

  if($dist =~ /^(sle.*?)(\d+)$/) {
    $dist_name = $1;
    $dist_ver_sp = $dist_ver = $2;
    $dist_version_var = 'VERSION';
    $dist_ver_sp .= "-SP$sp" if $sp;
  }
  elsif($dist eq 'tw') {
    $dist_name = "";
    $dist_ver = $dist;
    $dist_version_var = 'VERSION_ID';
    if($sp) {
      $dist_ver_sp = $sp;
    }
    else {
      return "";
    }
  }
  elsif($dist =~ /^(leap|kubic|casp|caasp)(\d.*)$/) {
    $dist_name = "";
    $dist_ver_sp = $dist_ver = $2;
    $dist_version_var = 'VERSION_ID';
    $dist_ver_sp .= ".$sp" if $sp;
  }
  else {
    die "error: can't generate service pack check for dist \"$dist\"\n";
  }

  # do we have to go via linuxrc version...?

  print "$dist_name $dist_ver\n";

  if($dist_name =~ /^sle/ && $servicepack->{$dist_ver}) {
    my $linuxrc = $servicepack->{$dist_ver}{$sp};
    return "" if !$linuxrc;

    my $c = <<'= = = = = = = =';
#! /bin/bash

# script generated by mkdud <version>

while IFS="$IFS[-" read pack ver xxx ; do
  [ "$pack" = linuxrc -a "$ver" = <linuxrc> ] && exit 0
done < /.packages.initrd

exit 1
= = = = = = = =

    $c =~ s#<version>#$VERSION#;
    $c =~ s#<linuxrc>#$linuxrc#;

    return $c;
  }

  # ... if not, check os-release

  my $c = <<'= = = = = = = =';
#! /bin/bash

# script generated by mkdud <version>

[ -f /etc/os-release ] || exit 1
. /etc/os-release
[ "$<dist_version_var>" = "<dist_ver_sp>" ]
= = = = = = = =

  $dist_ver .= ".$sp" if $sp;

  $c =~ s#<version>#$VERSION#;
  $c =~ s#<dist_version_var>#$dist_version_var#;
  $c =~ s#<dist_ver_sp>#$dist_ver_sp#;

  return $c;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub set_format
{
  return if !defined $opt_format;

  if($opt_format =~ /^(cpio|tar|iso)(\.(gz|gzip|xz))?$/) {
    $format_archive = $1;
    $format_compr = $3;
    $format_compr = 'gz' if $format_compr eq 'gzip';

    # print "format = $format_archive.$format_compr\n";
  }
  elsif($opt_format eq 'rpm') {
    $format_archive = 'tar';
    $format_rpm = 1;
  }
  else {
    die "$opt_format: unsupported format spec\n";
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub import_sign_key
{
  return if !$opt_sign;

  if($opt_sign_key_id) {
    $sign_key_ok = 1;
    $sign_key_id = $opt_sign_key_id;
    $sign_key_dir = "$ENV{HOME}/.gnupg";
    die "$sign_key_dir: no such gpg directory\n" unless -d $sign_key_dir;

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

    return;
  }

  die "no sign key specified\n" if !$opt_sign_key;

  my $key = $opt_sign_key;
  $key =~ s/^~/$ENV{HOME}/;
  die "$key: no such key file\n" unless -f $key;

  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 && $date) {
    $sign_key_ok = 1;
    $sign_key_id = $keyid;

    system "gpg --homedir=$sign_key_dir --import $key >/dev/null 2>&1";

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


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub get_sign_key_name
{
  my $key = $_[0];

  my $keyid;
  my $date;
  my $priv;
  my $pub;
  my $uid;
  my $expire;

  local $_;

  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]+)/;
      $uid = $1 if !$uid && /^:user ID packet: "(.+)"/;
      $expire = $1 if !$expire && /pub.*( \[expires:[^\]]*\])/;
      $date = $1 if !$date && $keyid && /created\s+(\d+)/;
    }
    close $p;
  }

  if($pub && $date) {
    my $x = sprintf "gpg-pubkey-%08x-%08x.asc", hex($keyid) & 0xffffffff, $date;
    $pubkey_info->{$x} = "$uid$expire";
    # print ">$x: $uid$expire<\n";
    return $x;
  }
  else {
    die "$key: signing key is not a public key\n";
  }

  return undef;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub sign_file
{
  my $file = $_[0];

  return if !$sign_key_ok;

  if($opt_sign_direct) {
    system "gpg --homedir=$sign_key_dir --yes --sign $file";
    rename "$file.gpg", $file;
  }
  else {
    system "gpg --homedir=$sign_key_dir --batch --yes --armor --detach-sign $file";
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub get_obs_key
{
  my $url = $_[0];
  my $rpm = $_[1];

  my ($server, $prj, $uri);

  if($url =~ m#obs://([^/]+)/([^/]+)/#) {
    $server = $1;
    $prj = $2;

    $uri = $obs->{server}{$server};

    if(!$uri) {
      my $x = $server;
      $x =~ s/^[^\.]*\.//;
      $uri = $obs->{server_short}{$x};
    }
  }

  if(!$server) {
    print STDERR "$rpm: obs info missing, can't get sign key\n";

    return undef;
  }

  if(!$uri) {
    print STDERR "$rpm: no config for obs server \"$server\", can't get sign key\n";

    return undef;
  }

  # print ">$uri $prj<\n";

  my $k = `osc -A '$uri' signkey $prj 2>/dev/null`;

  # stupid osc writes also other stuff to stdout...
  $k =~ s/^.*(-----BEGIN PGP PUBLIC KEY BLOCK-----)/$1/s;

  if($k =~ /^-----BEGIN PGP PUBLIC KEY BLOCK-----/) {
    my $tmp_file = $tmp->file();
    my $f;
    open $f, ">$tmp_file";
    print $f $k;
    close $f;

    return $tmp_file;
  }

  print STDERR "$rpm: no sign key found\n";

  return undef;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub vercmp {
  my ($s1, $s2) = @_; 
  $s1 =~ s/(\d+)/sprintf("%010d", $1)/ge;
  $s2 =~ s/(\d+)/sprintf("%010d", $1)/ge;
  return $s1 cmp $s2;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $new_file = repack_as_rpm($file)
#
# Re-pack archive $file as rpm. Returns the rpm name.
#
sub repack_as_rpm
{
  my $tmp_dir = $tmp->dir('rpm');
  my $file = $_[0];

  mkdir "$tmp_dir/SOURCES", 0755;
  link $file, "$tmp_dir/SOURCES/dud.tar" or die "$file: $!\n";

  my $c = <<'= = = = = = = =';
Name:           dud
Summary:        Driver Update
License:        No License Info
Group:          Other
Version:        1.0
Release:        0
Source:         dud.tar
BuildRoot:      %{_tmppath}/dud-build
BuildArch:      noarch

%description
Driver Update

%prep
%setup -c dud -n dud
%build
%install
  cp -a * %{buildroot}

%files
%defattr(-,root,root)
/
= = = = = = = =

  open my $f, ">$tmp_dir/SOURCES/dud.spec";
  print $f $c;
  close $f;

  system "rpmbuild -D '%_topdir $tmp_dir' -bb $tmp_dir/SOURCES/dud.spec >$tmp_dir/build.log 2>&1";

  my $rpm_name = "$tmp_dir/RPMS/noarch/dud-1.0-0.noarch.rpm";

  die "failed to create rpm\n" unless -f $rpm_name;

  return $rpm_name;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Sign an rpm.
#
sub sign_rpm
{
  my $file = $_[0];

  return if !$sign_key_ok;

  # note: the 'setsid' is needed because rpmsign will try to read the passphrase from /dev/tty
  system "setsid rpmsign --addsign -D '%_gpg_path $sign_key_dir' -D '%_gpg_name $sign_key_id' $file </dev/null >$sign_key_dir/rpmsign.log 2>&1";
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Create adddir script at given location.
#
# create_adddir(filename)
#
# The script lives here:
#   https://github.com/openSUSE/installation-images/blob/master/data/initrd/adddir
#
sub create_adddir
{
  my $file = $_[0];

  (my $c = <<"  ===") =~ s/^ +//mg;
    H4sICIhyx1oCA2FkZGRpcgCdVclu2zAQvfMrJrIR2AFkxbm0deAC6amHoOihBQo0QcyIo5iIRBoi
    ndRI+u+doShZzuI2NWCby5s3Cx+HgwPIrrXJrqVbCjEQAzCIys0gX1ZW0a+9N5CvoDRQ3XqsVlDd
    QY1SldrcQl2RiRDF2uReWxM2rpSu4UEAlDaXJWhBw8LWoEEbWIwgV5AMpwkcHgISPUyO4AjGCzgF
    ZQkK8JP2dQJzmEBqu/EELtkkt8Zrs8aITJHJMsYQNj3vZpcdPxMQWlmD4nc/WKmexurqHJTz4CnN
    EDctzDlYtneehicJLw8iMzEwwZDthgRIdnPtqsGAXoK64Mi5DLTexC7hoIm+Xbk8Bb9EE/DskFwB
    UWHubb3pVpswoN2FaA7ShXiyJvVdn3GdKrTjooWc74MAl2a+iEro6Jo6TH6ET7KAx0fAX9rDSc+w
    UVKaKqyxoK/JkWbb8dZrMiQnCZx8zBTeZWZdljs0LMt9hj2wY5HOk0Wn1whd9EEst4eAnB3Ppr9Z
    bBllTjlE88YoTPp2rXySuPXcfV1BWmzj6+3QHYpZPt8ttHjBRZTFUziWDsXf/NHdTR0kzd0LYj2F
    1b2iS/ciZwygR93Iz1iT7pdgoUt8kwa3un9VcpxUXfxP4l0hG7/F/vv21O8bi7YTD/VL8vD6qcXI
    wl/Xl7pes+00IfADuBh13SKE3pYwgYtxP/DmKL47eYMziL2JbIKECM7/SQc7U8rByG0qFq8bA95h
    vfFLbW6ITnrQjntYa+5tyzDZUnyxnhy5lcwxgPn4jaxoImskQl02YG4FU0HZ0uvyCak/ItwjOC9r
    P4NK3iJI6uurDdiCUyFntnQM4aeIiXPpkHc2gXi9UtKjIjJaqhyWd+RxdO3ywfT4/bsPx9PxRAi6
    XFcr6ZdvaVaia93/+Pi1LZ0g2drV4SEl5Q9b5yIcrvh69u3zvFucDXkuxM7ljhqhM49633L8AczS
    6oiiBwAA
  ===

  unlink $file;

  if(open my $f, "| base64 -d | gunzip -c > $file") {
    print $f $c;
    close $f;
  }

  chmod 0755, $file;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Get installer name for dist, taking mkdud options into account.
#
# installer_from_dist(dist)
#
sub installer_from_dist
{
  my $dist = $_[0];

  return "\L$opt_installer" if $opt_installer =~ /^(agama|anaconda|yast)$/i;

  my $installer = "yast";

  $installer = "agama" if $dist =~ /^(sle[sd]?|leap)(\d+)/ && $2 >= 16;

  $installer = "anaconda" if $dist =~ /^(fedora|rhel|sll)\d+(\.\d+)?$/;

  return $installer;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Get installer name from dud.config setting.
#
# installer_from_config(config_val)
#
sub installer_from_config
{
  my $val = $_[0];

  my $installer = "yast";

  $installer = "\L$val" if $val =~ /^(agama|anaconda|yast)$/i;

  return $installer;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Read scripts from RPM archive.
#
# Return hash with scripts;
#
# get_rpm_scripts(file)
#
sub get_rpm_scripts
{
  my $file = $_[0];
  my $scripts = {};

  for my $script (`rpm --nosignature -qp --qf '%|PREIN?{PREIN\n}:{}|%|POSTIN?{POSTIN\n}:{}|%|PREUN?{PREUN\n}:{}|%|POSTUN?{POSTUN\n}:{}|' $file 2>/dev/null`) {
    chomp $script;
    $script = "\L$script";
    $scripts->{$script} = `rpm --nosignature -qp --qf '%{\U$script\E}' $file 2>/dev/null`;
    # join continuation lines
    $scripts->{$script} =~ s/\\\n//g;
  }

  return $scripts;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Parse postin script for update-alternative calls.
#
# Return hash with symlinks or undef if update-alternative was not used.
#
# parse_alternatives(script)
#
sub parse_alternatives
{
  my $script= $_[0];

  my $update_links;

  for (split /\n/, $script) {
    next unless /update-alternatives\s/;
    while(/--(?:install|slave)\s+(\S+)\s+(\S+)\s+(\S+)/g) {
      # link: general name (full path)
      # name: symlink name in /etc/alternatives/
      # path: full path /etc/alternatives/$name links to
      my $link = $1;
      my $name = $2;
      my $path = $3;
      $link =~ s/^(["'])(.*)\1$/$2/;
      $name =~ s/^(["'])(.*)\1$/$2/;
      $path =~ s/^(["'])(.*)\1$/$2/;
      $update_links->{$link} = { name => $name, path => $path };
    }
  }

  return $update_links;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
sub apply_alternatives
{
  my $dir = $_[0];
  my $alternatives = $_[1];
  my $direct_links = $_[2];

  for my $alt (sort keys %$alternatives) {
    my $etc_alt = "/etc/alternatives";
    File::Path::mkpath("$dir/$etc_alt") if !$direct_links;
    my $alt_dir = $alt;
    $alt_dir =~ s#[^/]*$##;
    my $d = "$dir/$alt_dir";
    File::Path::mkpath($d);
    if(-d $d) {
      unlink "$dir/$alt";
      if($direct_links) {
        symlink $alternatives->{$alt}{path}, "$dir/$alt";
      }
      else {
        symlink "$etc_alt/$alternatives->{$alt}{name}", "$dir/$alt";
        unlink "$dir/$etc_alt/$alternatives->{$alt}{name}";
        symlink $alternatives->{$alt}{path}, "$dir/$etc_alt/$alternatives->{$alt}{name}";
      }
    }
    else {
      die "$d: failed to create directory\n";
    }
  }
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# Check for presence of 'osc' command.
#
sub check_osc
{
  my $prog;

  chomp($prog = `bash -c 'type -p osc'`);

  die "osc is needed, please install package 'osc'.\n" if $prog eq "";
}
