#! /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 = "1.44";

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

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;

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_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_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;

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

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

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,
  '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,
  'obs-keys'         => \$opt_obs_keys,
  'force'            => sub { $opt_fix_yast = $opt_fix_usr_src = $opt_fix_dist = $opt_fix_adddir = 0 },
  'fix-yast!'        => \$opt_fix_yast,
  'fix-usr-src!'     => \$opt_fix_usr_src,
  'fix-dist!'        => \$opt_fix_dist,
  'fix-adddir!'      => \$opt_fix_adddir,
  '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,
  '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 unless $opt_show xor $opt_create;

usage 2 if $opt_show && @ARGV;

@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'};

if($opt_obs_keys) {
  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);

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

  $need_dist ||= @opt_config || @opt_exec || @opt_name;

  if($need_dist) {
    die "Error: distribution arg is required; use --dist.\n" if !@opt_dist;
    my %d;
    @dists = @opt_dist;
    map { tr/-//d } @dists;
    map { s/^(tumbleweed|tw).*/tw/g } @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 "" ? ("sles$2", "sled$2") : "sle\L$1$2" : "\L$_" } @dists} = ();
    @dists = sort keys %d;

    for (@dists) {
      if(!/^((leap|kubic|casp|caasp)?\d+\.\d+|tw|sle[sd]\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 short summary. DUD may be a file
                                or directory or an ISO image.

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).
                                Option can be repeated to specify several achitectures.
                                Note: if you don't set the architecture and mkdud can't
                                find a hint in SOURCES either, an update for all supported
                                architectures is created.
  -d, --dist DIST               Specify the product the DUD is for. Possible values include:
                                13.2, sle12, leap42.3, kubic1.0, caasp1.0, tw standing for
                                openSUSE 13.2, SLE12, Leap 42.3, Kubic 1.0, CaaSP 1.0,
                                Tumbleweed, respectively.
                                Note that 'sle12' is a short hand for specifying both
                                'sles12' and 'sled12'.
                                Note also that there are no separate names for service packs.
                                So 'sles12-sp1' is the same as 'sles12'. But see '--condition'
                                below for a way to target specific service packs.
                                See distribution name notes below for more details.
                                Option can be repeated to specify several distribution targets.
      --condition SCRIPT        Run SCRIPT and apply DUD only if SCRIPT has exit status 0.
                                If SCRIPT has the special name ServicePackN (N = 0, 1, 2, ...),
                                a script that checks for service pack N is _generated_ and added.
                                N = 0 refers to the main product (without service pack).
                                Note that for Tumbleweed you can pass the snapshot version as N
                                to limit the DUD to a specific snapshot.
                                Option can be repeated to specify several conditions.
  -p, --prio NUM                Set repository priority to NUM; lower NUM means higher priority
                                (default: 50).
  -n, --name NAME               Set driver update name. If you update packages or modules
                                a default name is generated based on the package and module
                                versions.
                                Option can be repeated to specify a multi-line name.
  -x, --exec COMMAND            Run command just after the driver update has been loaded.
                                Option can be repeated to specify several commands.
                                Note: the commands are run just before kernel modules
                                are updated.
  -i, --install METHODS         Package install method. METHODS is a comma-separated list
                                of: instsys, repo, rpm (default: 'instsys,repo,rpm').
                                - instsys: unpack packages in installation system
                                - repo: create repo with all packages and register with
                                    yast before starting installation; repo will be removed
                                    after the installation
                                - rpm: install packages at the end of the installation
                                    using rpm (that is, not via repo & zypper)
      --config KEY=VALUE        Set linuxrc config option KEY to VALUE. The options are changed
                                just after the driver update has been loaded.
                                Option can be repeated to set several options.
      --no-docs                 Don't include package documentation in unpacked instsys tree
                                (to save space). This is the default setting.
      --keep-docs               Include package documentation in unpacked instsys tree.
      --force                   Obsolete. Use one or more of --no-fix-XXX instead.
      --no-fix-yast             Allow driver update to replace /sbin/yast. See 'Consistency checks'.
      --no-fix-dist             Allow to specify an arbitrary distribution name with --dist. See
                                'Consistency checks'.
      --no-fix-usr-src          Allow driver update to include /usr/src/packages. See 'Consistency
                                checks'.
      --no-fix-adddir           Do not include an updated adddir script. See 'Consistency checks'.
      --format FORMAT           Specify archive format for DUD.
                                FORMAT=((cpio|tar|iso)[.(gz|xz)])|rpm.
                                Default FORMAT is cpio.gz (gzip compressed cpio archive).
                                Note: please check README before changing the default.
      --prefix NUM              First directory prefix of driver update. See README.
      --sign                    Sign the driver update.
      --detached-sign           Sign the driver update. This creates a detached signature.
      --sign-key KEY_FILE       Use this key for signing. Alternatively, use the
                                'sign-key' entry in ~/.mkdudrc.
      --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.
                                See 'Adding RPMs notes' below.


Configuration file:

  \$HOME/.mkdudrc

    sign-key=KEY_FILE
      File name of the private signing key. The same as the 'sign-key' option.

To create a driver update you need SOURCES. SOURCES can contain:

  - existing driver updates; either as archive, rpm, or unpacked directory. All driver
    updates are joined.

  - RPMs. Packages not containing a driver update are used according to the value
    of the --install option.

  - PGP pubic key files (ASCII). The files are added to the rpm key database for verifying
    RPMs during the installation process. See 'Adding RPMs notes' below.

  - kernel modules.

  - 'module.order' and 'module.config' files. See driver update documentation.

  - 'update.pre', 'update.post', 'update.post2' scripts.
    See driver update documentation.
    Note that you can specify several 'update.post', etc. scripts. They are all run.

  - *.ycp, *.ybc, or *.rb files. Files are copied to the correct places automatically
    if they contain a usable 'File' comment.

  - 'y2update' directories.

  - program files (binaries, libraries, executable scripts). They are put into the
    'install' dir. You can run them if needed using the --exec option.

  - plain text files. They are considered documentation.

  - directories that are neither DUDs nor YaST updates. Everything below the directory
    is added to the installation system.

  - ISO images. The images are unpacked and scanned for driver updates.

Adding RPMs notes:

  If you add RPMs to SOURCES, these RPMs can be used to update the installation system
  and/or to be installed in the target system. See --install option on how to choose.

  The best way is to go for 'repo' to install them in the target system as then they
  are handled by the package manager and package dependencies are automatically resolved.

  But there are two catches:

    (1) The package must be actually required by the selected pattern or it must be
    manually selected in the package manager; else it won't be installed at all.

    This is normally no problem if you update a package (as it's likely required, else
    you wouldn't want to update it) but if you try to install some uncommon package
    it might not be selected.

    (2) If the package is signed, the signature will be checked by the package manager
    and you'll end up with a warning if the necessary public key is not available. This
    particularly happens with packages from the openSUSE build service which were built
    in some user's project.

    For this, mkdud will package any pgp public key files you add to SOURCES in a way so
    that they are used by the package manager during installation. They will *not* be
    installed in the target system.

    To ease this even more, the --obs-keys option causes mkdud to retrieve the necessary
    keys for each RPM in SOURCES from the build service using the 'osc' tool. Note that
    you must configure the build service access via .oscrc in your HOME directory for this
    to work.

Distribution (product) name notes:

  The --dist option accepts these values (case-insensitive; X, Y: decimal numbers):

    - X.Y (e.g. 13.2) = openSUSE X.Y
    - leapX.Y (e.g. leap42.3) = openSUSE Leap X.Y
    - kubicX.Y (e.g. kubic1.0) = openSUSE Kubic X.Y
    - tw = openSUSE Tumbleweed
    - sleX (e.g. sle12) = SUSE Linux Enterprise (Server + Desktop) X
    - slesX (e.g. sles12) = SUSE Linux Enterprise Server X
    - sledX (e.g. sled12) = SUSE Linux Enterprise Desktop X - but see notes below
    - caaspX.Y (e.g. caasp1.0) = SUSE Container as a Service Platform X.Y - see notes below

  Note that there aren't any products anymore that actually use 'sledX',
  even SLED uses the 'sles' DUDs meanwhile. For this, please just use 'sleX'
  when creating a DUD for a SUSE Linux Enterprise product.

  Note also that CaaSP releases are based on SLE releases. Since driver
  updates are usually released for a specific SLE version mkdud provides a
  mapping CaaSP release <-> SLE release when building with --dist caspX.Y.
  Currently:

    - caasp1.0 = sles12 (-sp2)
    - caasp2.0 = sles12 (-sp3)
    - caasp3.0 = sles12 (-sp3)
    - caasp4.0 = sles15

  Driver updates built only for SLE12 will implicitly also work with
  CaaSP3.0; those built only for SLE15 will also work with CaaSP4.0. But
  this is not true for CaaSP1.0 and CaaSP2.0.

  Normally, a DUD will apply to the main release as well as to all the service packs.
  To target a specific service pack, use the --condition option. For example:
  '--dist sle12 --condition ServicePack3' to get a DUD exclusively for sle12-sp3.

  This works also to target a specifc Tumbleweed release:
  '--dist tw --condition 20161231'.

  You can specify several '--condition' options. They must all be fulfilled (logical AND).

Consistency checks

  It is possible to create driver updates that will predictably not work
  because they interfere with the setup of the installation environment.

  mkdud will warn you and try to work around these cases. But there still
  may be valid use-cases so the --fix-XXX / --no-fix-XXX group of options
  lets you enable (default) or disable these workarounds.

  --[no-]fix-yast
    In older SUSE versions /sbin/yast was a different script in the
    installation environment than the one from the yast package. Updating
    the special variant with the regular script will make the installation
    impossible. mkdud recognizes this and removes /sbin/yast from driver
    updates.

  --[no-]fix-dist
    The --dist option normally allows you to specify only distributions
    mkdud knows about. With this option you may put anything there - in case
    you know better.

  --[no-]fix-usr-src
    The installation system must not contain a /usr/src/packages directory.
    mkdud normally removes it (and its content) from driver updates.

  --[no-]fix-adddir
    The /sbin/adddir script is used in the installation system to actually
    apply that part of a driver update that replaces files in the
    installation system. In older SUSE versions this script was not able to
    update some programs from the 'coreutils' package. mkdud implicitly
    includes an update for this script if it detects a need for it.

References:

Driver update documentation is available here:

  http://ftp.suse.com/pub/people/hvogel/Update-Media-HOWTO/index.html

  http://en.opensuse.org/SDB:Linuxrc#p_driverupdate

Examples:

  # show content of foo.dud
  mkdud --show foo.dud

  # create update for hello.rpm
  mkdud --create foo.dud --dist 13.2 hello.rpm

  # create kernel update
  mkdud --create foo.dud --dist 13.2 kernel-*.rpm

  # create kernel update and replace tg3 module
  mkdud --create foo.dud --dist 13.2 kernel-*.rpm tg3.ko

  # create kernel update, replace tg3 module, add some docs and give the dud a nice name
  mkdud --create foo.dud --dist 13.2 --name 'for granny' kernel-*.rpm tg3.ko README

  # update some YaST stuff
  mkdud --create foo.dud --dist 13.2 BootCommon.y*

  # add directory tree below 'newstuff/' to installation system
  mkdud --create foo.dud --dist 13.2 newstuff

  # extract driver updates from ISO (you need root permissions for that)
  mkdud --create foo.dud xxx.iso

  # create update for hello.rpm and join with foo1.dud and foo2.dud
  mkdud --create foo.dud --dist sle12 foo1.dud foo2.dud hello.rpm

= = = = = = = =

  exit shift;
}


# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
# $arch = get_file_arch($file)
#
# Return architecture for $file or undef if it couldn't be determined.
#
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',
  };

  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;

  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 => '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;
    }

    # 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(/^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;
    }
    elsif($_[0] =~ m#\.ko$#) {
      $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(/ (cpio|tar) archive/) {
    $dud = $1;
  }
  elsif(/^(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(/ 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($_[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 => '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(-f $_[0] && $_[0] =~ m#(^|/)(update\.(pre|post|post2)|module\.(order|config))$#) {
    push @files, { type => $2, file => $_[0] };

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

    return;
  }
  elsif(-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;
    }
  }
  else {
    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;

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

    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;

    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;
        system "cp -a '$_->{file}'/* $base/inst-sys";
      }

      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 "$_->{canonical_name}\n";
          close $f;
          system "rpm2cpio $_->{file} | ( cd $base/inst-sys ; 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,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($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($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($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(@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(@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}) {
      $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 -l $repo /etc/zypp/repos.d/*` ; do
  [ -f "$i" ] && rm "$i"
done
= = = = = = = =

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

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

    if($has_pubkeys) {
      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 = "DriverUpdate" 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 = "genisoimage -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 $tmp_archive $file_name";

  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])(\d+)|(leap|kubic|casp|caasp)?(\d+\.\d+)|tw)$/) {
      if($3 eq 's') {
        push @{$p1{"SLES $4"}}, $1;
      }
      elsif($3 eq 'd') {
        push @{$p1{"SLED $4"}}, $1;
      }
      elsif($5 eq 'leap') {
        push @{$p1{"openSUSE Leap $6"}}, $1;
      }
      elsif($5 eq 'kubic') {
        push @{$p1{"openSUSE Kubic $6"}}, $1;
      }
      elsif($5 eq 'casp') {
        push @{$p1{"CASP $6"}}, $1;
      }
      elsif($5 eq 'caasp') {
        push @{$p1{"CaaSP $6"}}, $1;
      }
      elsif($2 eq "tw") {
        push @{$p1{"openSUSE Tumbleweed"}}, $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/^SLED /SLES / && $p2{$s} eq $p2{$_}) {
      my $e = $_;
      $e =~ s/^SLED /SLE /;
      $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;

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

  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 'updatename') {
        $sect{name} .= "      $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;

  # ----------------------------
  # 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/*.ko")) {
    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";

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

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


  # ----------------------------
  # 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(-e "$dir/inst-sys/sbin/yast") {
      $sect{instsys} .= "      ***  Warning: replaces /sbin/yast.  ***\n";
    }

    if(-e "$dir/inst-sys/usr/src/packages") {
      $sect{instsys} .= "      ***  Warning: includes /usr/src/packages.  ***\n";
    }

    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);
    }
  }

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

  for (glob("$dir/inst-sys/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";
  }

  # ----------------------------
  # 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{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{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}";
  }

  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;

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