#! /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.18";

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

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

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

# 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 $sign_key_dir;
my $sign_key_ok;

# linuxrc versions in service packs
my $servicepack;
$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}{1} = "3.3.59";
$servicepack->{11}{2} = "3.3.81";
$servicepack->{11}{3} = "3.3.91";

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' => \$opt_force,
  '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,
  'force'            => \$opt_force,
  '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'};

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;
    @d{map { /^sle([sd]?)(\d+)/i ? $1 eq "" ? ("sles$2", "sled$2") : "sle\L$1$2" : "\L$_" } @opt_dist} = ();
    @dists = sort keys %d;
  }

  # 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               Either an openSUSE version (e.g. 13.2) or SLE version
                                (e.g. sles12).
                                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.
                                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 = 1, 2, ...), a
                                script that checks for service pack N is _generated_ and added.
                                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                   Allow driver update to contain files that might break the
                                installation. mkdud will normally remove those files and
                                print a warning. Use this option to override.
      --format FORMAT           Specify archive format for DUD. FORMAT=(cpio|tar|iso)[.(gz|xz)].
                                Default FORMAT is cpio.gz (gzip compressed cpio archive).
                                Note: don't change the default. See README.
      --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').

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 or unpacked directory. All driver
    updates are joined.

  - RPMs. The packages are used according to the value of the --install option.

  - kernel modules.

  - 'module.order' 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.

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
{
  local $_;

  for (`objdump -f $_[0] 2>/dev/null`) {
    if(/^architecture:\s*(\S+)/) {
      my $ar = $1;
      $ar =~ s/^.*:|,$//g;
      $ar = "i386" if $ar =~ /^i.86$/;
      $ar =~ tr/-/_/;
      return $ar if $ar ne "";
    }
  }

  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}-%{VERSION}-%{RELEASE}.%{ARCH}' $_[0] 2>$tmp_err`;
    if($f eq "") {
      print STDERR "failed to read rpm: $_[0]\n";
      open my $x, $tmp_err;
      print STDERR $_ while (<$x>);
      close $x;
      exit 1;
    }

    $ft->{canonical_name} = $f;

    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;

    push @files, $ft;

    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)$#) {
    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];
    my $l = <$f>;
    close $f;
    if($l =~ /^#!/) {
      push @files, { type => 'bin', file => $_[0] } if -x $_[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))?$/) {
      my $cmd = "cpio --quiet -dmiu";
      $cmd = "tar -xpf -" if $1 eq "tar";
      my $compr = 'cat';
      $compr = 'gzip -dc' if $3 eq 'gz';
      $compr = 'xz -dc' if $3 eq 'xz';
      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;

  # 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 'module.order') {
        $dud_ok = 1;
        mkdir "$base/modules", 0755;
        system "cat '$_->{file}' >>$base/modules/module.order";
      }

      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::make_path($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(!$opt_force) {
      if(lstat "$base/inst-sys/sbin/yast") {
        print STDERR
          "Warning: prevented driver update from replacing /sbin/yast.\n" .
          "If you really need to do this, use --force.\n";
        unlink "$base/inst-sys/sbin/yast";
      }

      if(-e "$base/inst-sys/usr/src/packages") {
        print STDERR
          "Warning: prevented driver update from including /usr/src/packages.\n" .
          "If you really need to do this, use --force.\n";
        system "rm -rf $base/inst-sys/usr/src/packages";
      }
    }

    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;

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

    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;
    unshift @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($opt_sign) {
    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+)|\d+\.\d+)$/) {
      if($3 eq 's') {
        push @{$p1{"SLES $4"}}, $1;
      }
      elsif($3 eq 'd') {
        push @{$p1{"SLED $4"}}, $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";

  # ----------------------------
  # 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")) {
      open my $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;

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

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

  if($dist !~ /^sle.*?(\d+)$/ || ($linuxrc = $servicepack->{$1}{$sp}) eq "") {
    return "";
  }

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


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

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

