#!/bin/sh
#! -*- perl -*-
eval 'PATH=/opt/ozsw/bin:$PATH:/usr/perl5/bin exec perl -x -w $0 ${1+"$@"}'
  if 0;

# pkgutil - manages packages on Sun/Oracle Solaris systems
# Copyright (C) 2008-2014 Peter Bonivart

# $Id: pkgutil 463 2014-10-16 19:28:57Z bonivart $

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
#
# The author, Peter Bonivart, can be contacted by email at bonivart@opencsw.org

use strict;
use Cwd;
use Getopt::Long;
Getopt::Long::Configure qw( no_ignore_case bundling );
use Symbol qw( gensym );

# Unbuffered output
$| = 1;

# Signal handlers
$SIG{HUP} = 'IGNORE';
$SIG{TERM} = $SIG{INT} = $SIG{QUIT} = \&signal_handler;

# Untainting
delete @ENV{qw(LD_LIBRARY_PATH IFS CDPATH ENV BASH_ENV)};    # Make %ENV safer
$ENV{'PATH'} = '/usr/sbin:/bin:/usr/bin:/opt/ozsw/bin';
$ENV{'LANG'} = 'C';

# Ensure we always work from a directory that won't
# be yanked out from under pkgadd/pkgrm.
chdir('/');

my $debug        = 0;
my $pkgutilver   = "2.6.7";
my $use_md5      = 0;
my $use_gpg      = 0;
my $nonozsw      = 0;
my $pkgliststyle = 2;
my $maxpkglist   = 100000;
my @mirror;
my $defaultmirror = ("http://operz.coralcmd.net/pub/osol/ozsw");
my $workdir       = "/var/opt/ozsw/pkgutil";
my $pkgdir;
my $admin;
my $logfile;
my (
    $line, $tmp,  $wget, $yes,     $force, $nomod,
    $gzip, $name, $ver,  $pkgname, $filename
);
my ( $size,    $deps,   $cat, $tmppkg,    $tmppkg2,   $trace,   $hash );
my ( @exclude, %config, %pkg, %commonpkg, %pkgmirror, %catinfo, %state );

# Show available packages in catalog (-a/A)
#   mode  - 0 = display available packages (-a)
#           1 = compare available packages to those installed (-A)
#   parse - 0 = normal output, 1 = machine parsable output
sub available {
    my ( $mode, $parse ) = @_;
    my $tmp;
    my $local_ver;

    unless ($parse) {    # No header when --parse is being used
        if ( $mode == 0 ) {
            print
"common               package              catalog                        size\n";
        }
        else {
            print "package\t\t\t  catalog\t\t    installed\n";
        }
    }

    my $hit = 0;
    foreach my $common ( sort keys %commonpkg ) {
        my $pkgname = $commonpkg{$common};
        foreach my $remote_ver ( sort keys %{ $pkg{$pkgname} } ) {
            my ($size) = ( split /\s+/, $pkg{$pkgname}{$remote_ver} )[5];

            if ( $mode == 0 ) {
                if ($parse) {
                    $tmp = "$common\t$pkgname\t$remote_ver\t$size\n";
                }
                else {
                    $tmp = sprintf( "%-20s %-20s %-25s %9s\n",
                        $common, $pkgname, $remote_ver, format_byte($size) );
                }
            }
            else {
                $local_ver = check_pkg( $pkgname, 0 );
                $local_ver = "SAME"          if ( $local_ver eq $remote_ver );
                $local_ver = "not installed" if ( $local_ver eq "notinst" );
                if ($parse) {
                    $tmp = "$pkgname\t$remote_ver\t$local_ver\n";
                }
                else {
                    $tmp = sprintf( "%-25s %-25s %-25s\n",
                        $pkgname, $remote_ver, $local_ver );
                }
            }

            if ( scalar(@ARGV) > 0 ) {
                foreach (@ARGV) {
                    if (   "$pkgname-$remote_ver" =~ /$_/
                        || "$common-$remote_ver" =~ /$_/ )
                    {
                        print $tmp;
                        $hit = 1;
                    }
                }
            }
            else {
                print $tmp;
                $hit = 1;
            }
        }
    }

    # Do fuzzy matching if nothing found
    unless ( $hit || $parse || $mode ) {
        print
"\nNo exact matches found, doing fuzzy matching for first argument ($ARGV[0]) ...\n";
        eval { require String::Approx; };    # Check for String::Approx
        if ($@) {
            myexit(
"String::Approx required for fuzzy matching not found, install with pkgutil -i OZSWpmstringapprox to enable.",
                "", 0
            );
        }
        else {
            my @match = String::Approx::amatch( $ARGV[0], %commonpkg );
            if ( scalar(@match) ) {
                print "Suggestions: ";
                foreach (@match) {
                    print "$_ ";
                }
                print "\n";
            }
        }
    }

    myexit( "", "", 0 );
}

# display catalog info
sub catalog_info {

    foreach ( sort keys %catinfo ) {
        print "\n";
        print "URL\t\t" . $catinfo{$_}{url} . "\n";
        print "Release\t\t" . $catinfo{$_}{RELEASE} . "\n";
        print "Creation time\t" . $catinfo{$_}{CREATIONDATE} . "\n";
        print "Number of pkgs\t" . $catinfo{$_}{num_of_pkgs} . "\n";
        print "File\t\t" . $catinfo{$_}{filename} . "\n";
        print "GPG\t\t" . $catinfo{$_}{gpg} . "\n" if $use_gpg;
    }
    myexit( "", "", 0 );
}

# check binary to avoid trying to use broken wget for example
#   bin - binary to check
sub check_binary {
    my ($bin) = @_;

    print STDERR "DEBUG:Checking binary: $bin ... " if $debug;
    my $status = system("/bin/sh -c '$bin --version' >/dev/null 2>/dev/null");
    print "" . ( ($status) ? "fail" : "OK" ) . "\n" if $debug;
    return 1 if $status;
}

# Check catalog
#   always - always update catalog
#   parse  - pass through to silence gpg checks
sub check_catalog {
    my ( $always, $parse ) = @_;

    foreach my $url (@mirror) {
        my $filename = mangle_url($url);
        my $age      = 0;
        $age = -M $filename if ( -r $filename );
        print STDERR "DEBUG:Catalog $url age in days: $age\n" if $debug;
        fetch_catalog( $url, $filename )
          if (
            ( $age > $config{catalog_update} && $config{catalog_update} != -1 )
            || !-e $filename
            || $always );
        gpg( $filename, $parse ) if $use_gpg;
        my $entries = read_catalog( $url, $filename );    # Once per file
        print "==> $entries package"
          . ( $entries > 1 ? "s" : "" )
          . " loaded from $filename\n"
          if (
            ( $age > $config{catalog_update} && $config{catalog_update} != -1 )
            || !-e $filename
            || $always
            || $debug );
    }
}

# Check if package is already installed and if so, which version
#   pkg  - package to check for
#   mode - 0 = return full string, 1 = return only rev part
sub check_pkg {
    my ( $pkg, $mode ) = @_;
    my ( $tmp, $exit_code, $retvalue );
    my $root_path = $config{root_path} ? "-R $config{root_path}" : "";
    my $test_path = $config{root_path} ? $config{root_path}      : "";

    if ( !-d "$test_path/var/sadm/pkg/$pkg" ) {
        $retvalue = "notinst";
        print STDERR "DEBUG:$pkg not installed\n" if $debug;
    }
    else {
        $tmp = `pkgparam $root_path $pkg VERSION 2> /dev/null`;
        chomp $tmp;
        if ( $mode == 0 ) {
            $retvalue = $tmp;
        }
        else {
            ($retvalue) = ( $tmp =~ /REV=(.+)$/ );
        }
        print STDERR "DEBUG:$pkg installed, version $retvalue\n" if $debug;
    }
    return $retvalue;
}

# Clean up obsolete packages (--cleanup)
sub cleanup {
    my @legacy = list_obsolete();
    print STDERR "DEBUG:"
      . scalar(@legacy)
      . " legacy: "
      . join( " ", @legacy ) . "\n"
      if $debug;

    my @deps = list_deps();
    print STDERR "DEBUG:"
      . scalar(@deps)
      . " deps: "
      . join( " ", @deps ) . "\n"
      if $debug;

    my @obsolete;
    foreach my $pkg (@legacy) {
        push( @obsolete, $pkg ) unless ( grep { /^\Q$pkg\E$/ } @deps );
    }
    my $pkgsep = ( $pkgliststyle ? "\n\t" : " " );
    print scalar(@obsolete)
      . " OBSOLETE package"
      . ( scalar(@obsolete) > 1 ? "s" : "" );
    if ( scalar(@obsolete) ) {
        print ":$pkgsep" . join( "$pkgsep", @obsolete ) . "\n";
        rem_pkgs(@obsolete);
    }
    else {
        print "s.\n\n";
    }
    myexit( "", "", 0 );
}

# Compare current to available packages (-c/C)
#   mode  - 0 = print full list, 1 = return list of old packages
#           2 = return full list of old packages, 3 = same as 0 but only diffs
#           4 = compare a single package (much faster, used with Puppet)
#   parse - 0 = normal output, 1 = machine parsable output
sub compare {
    my ( $mode, $parse ) = @_;
    my ( @cswpkgs, @retlist );
    my ( $local_ver, $remote_ver, $tmp );
    my $root_path = $config{root_path} ? "-R $config{root_path}" : "";

    unless ($parse) {    # No header when --parse is being used
        print "package\t\t\t  installed\t\t    catalog\n"
          if ( $mode == 0 || $mode == 3 );
    }
    if ( $mode == 4 ) {
        $cswpkgs[0] = "a " . parse_catalog( $ARGV[0], 2 ) . " a";
        myexit( "Not in catalog", "", 0 )
          if ( $cswpkgs[0] eq "a not in catalog a" );
    }
    else {
        if ($nonozsw) {
            @cswpkgs = `pkginfo $root_path`;
        }
        else {
            @cswpkgs = `pkginfo $root_path | grep OZSW`;
        }
    }
    foreach my $pkg (@cswpkgs) {
        ($pkg) = ( $pkg =~ /\s+?(\S+?)\s/ );
        $local_ver = check_pkg( $pkg, 0 );
        $remote_ver = parse_catalog( $pkg, 1 );
        $remote_ver = "SAME" if ( $local_ver eq $remote_ver );
        if ( $mode == 1 ) {
            push( @retlist, $pkg )
              if ( $remote_ver ne "SAME" && $remote_ver ne "not in catalog" );
        }
        else {
            if ($parse) {
                $tmp = "$pkg\t$local_ver\t$remote_ver\n";
            }
            else {
                $tmp = sprintf( "%-25s %-25s %-25s\n",
                    $pkg, $local_ver, $remote_ver );
            }
            if ( $mode == 2 ) {
                push( @retlist, $tmp )
                  if ( $remote_ver ne "SAME"
                    && $remote_ver ne "not in catalog" );
            }
            else {
                if ( scalar(@ARGV) > 0 && $mode != 4 ) {
                    foreach (@ARGV) {
                        if ( $pkg =~ /$_/ ) {
                            print $tmp
                              unless ( $mode == 3 && $remote_ver eq "SAME" );
                        }
                    }
                }
                else {
                    print $tmp unless ( $mode == 3 && $remote_ver eq "SAME" );
                }
            }
        }
    }
    if ( $mode == 1 || $mode == 2 ) {
        return @retlist;
    }
    else {
        myexit( "", "", 0 );
    }
}

# Deduplicate a list
#   list - list to deduplicate
sub dedup {
    my @list = @_;
    my %deduped;
    my @tmplist;

    for my $pkg (@list) {
        unless ( exists $deduped{$pkg} ) {
            push( @tmplist, $pkg );
            $deduped{$pkg} = 1;
        }
    }
    return @tmplist;
}

# Display package dependencies as a tree (--deptree)
#   depth - number of levels to show
sub deptree {
    my ($depth) = @_;
    my ( @pkgarray, @rest, @j );
    my $level = 0;
    my $true;

    foreach (@ARGV) {
        undef @pkgarray;
        $pkgarray[0] = $_;

        foreach (@pkgarray) {
            my $pkg  = parse_catalog( $_, 2 );    # Get package name
            my $deps = parse_catalog( $_, 6 );    # Get dependencies

            unless ( $pkg eq "OZSWcommon"
                && $config{deptree_filter_common} eq "true"
                || $level > $depth )
            {
                for ( my $i = 0 ; $i < $level ; $i++ ) {
                    print "- ";
                }
                print "$pkg\n";
            }

            if ( $deps ne "none" ) {
                my @tmplist = split( /\|/, $deps );
                unshift( @rest, @tmplist );    # Add deps to rest list
                $level++;                      # Increment level
                $j[$level] = scalar(@tmplist); # Nr of deps on this level
            }

            push( @pkgarray, shift(@rest) ) if ( scalar(@rest) > 0 );

            $true = 1;
            while ( $true && $level > 0 )
            {    # How many levels do we need to back up?
                if ( $deps eq "none" && $j[$level] == 0 ) {
                    $level--;
                }
                else {
                    $true = 0;
                }
            }

            $j[$level]--;    # Lower nr of remaining deps on this level
        }
    }
    myexit( "", "", 0 );
}

# Describe available packages (--describe)
#   parse - 0 = normal output, 1 = machine parsable output
sub describe {
    my ($parse) = @_;
    my $desc_file = mangle_url( $mirror[0] );
    $desc_file =~ s|/catalog\.|/descriptions\.|;

    myexit( "No descriptions file available. Try pkgutil -U.", "", 1 )
      unless ( -r $desc_file && -s _ );
    my $DESC = gensym();
    open( $DESC, "<$desc_file" );
    while ( my $line = <$DESC> ) {
        chomp $line;
        $line =~ s/ - /\t/ if $parse;
        if ( scalar(@ARGV) > 0 ) {
            foreach (@ARGV) {
                print "$line\n" if ( $line =~ /$_/ );
            }
        }
        else {
            print "$line\n";
        }
    }
    close $DESC;
    myexit( "", "", 0 );
}

# Email notifications when updates are available (-e)
#   email - address to send notications to
sub email {
    my ($email) = @_;
    my $file;
    my $TMP = gensym();

    eval { require File::Temp; };    # Check for File::Temp
    $file = "/tmp/pkgutil.$$.$^T"
      if ($@);    # Use less safe method if File::Temp not available

    myexit( "Mailx not found on system!", "", 1 ) unless ( -x "/bin/mailx" );
    my @update_list = compare(2);
    if ( scalar(@update_list) > 0 ) {
        if ( defined $file ) {
            open( $TMP, ">$file" ) or myexit( "Can't open $file", "$!", 1 );
        }
        else {
            ( $TMP, $file ) = File::Temp::tempfile( DIR => "/tmp" );
        }
        foreach (@update_list) {
            print $TMP $_;
        }
        chomp( my $host = `uname -n` );
        my $subject = "$host: OZSW updates available";
        my @args    = ("/bin/mailx -s \"$subject\" $email < $file");
        system(@args);
        unlink $file;
        close $TMP;
    }
    myexit( "", "", 0 );
}

# Extract downloaded packages (--extract)
#   pkglist - list of packages to extract (from install sub)
sub extract_pkg {
    my (@pkglist) = @_;

    foreach (@pkglist) {    # pkgtrans them one by one
        my $file = parse_catalog( $_, 3 );
        print "Extracting $_ to $pkgdir/" . parse_catalog( $_, 2 ) . " ...\n";
        my $status = system(
"$gzip -c -f -d $pkgdir/$file | pkgtrans /dev/fd/0 $pkgdir all 2> /dev/null"
        );
        myexit( "Could not extract $file (directory may already exist)", "", 1 )
          if $status;
    }
}

# Fetch catalog
#   url - url to download from
#   filename - filename to use for local catalog
sub fetch_catalog {
    my ( $url, $filename ) = @_;
    my $wgetopts = "";
    my $status;

    $wgetopts .= " $config{wgetopts}" if $config{wgetopts};
    $wgetopts .= " --execute cache=off"
      if ( $config{catalog_not_cached} eq "true" );
    print "=> Fetching new catalog and descriptions ($url) if available ...\n";
    myexit( "Option -n selected, stopping here.", "", 0 ) if $nomod;

    my $desc_file = $filename;
    $desc_file =~ s|/catalog\.?|/descriptions\.|;
    unlink "${filename}.tmp", "${desc_file}.tmp";
    if ( $url =~ /^file:/ ) {    # file method used, use cp
        my ($tmpcatsrc) = ( $url =~ /^file:\/(.+)$/ );
        $status = system( "/bin/cp", "$tmpcatsrc/catalog", "${filename}.tmp" );
        if ($status) {
            myexit( "\nFetching of catalog failed.", "", 1 );
        }
        else {
            rename "${filename}.tmp", $filename;
        }
        $status =
          system( "/bin/cp", "$tmpcatsrc/descriptions", "${desc_file}.tmp" );
        if ($status) {
            print
"Warning: the descriptions file could not be fetched. The --describe option will not be available.\n"
              if $status;
        }
        else {
            rename "${desc_file}.tmp", $desc_file;
        }
    }
    else {    # http/ftp used, use wget
        locate_wget() unless ( -x $wget );    # If wget disappeared locate alt.
        logoutput();
        $status = system( "$wget", split( ' ', $wgetopts ),
            "-O", "${filename}.tmp", "$url/catalog" );
        if ($status) {
            logfail();
            myexit( "\nFetching of catalog failed.", "", 1 );
        }
        else {
            logend();
            rename "${filename}.tmp", $filename;
        }
        logoutput();
        $status = system( "$wget", split( ' ', $wgetopts ),
            "-O", "${desc_file}.tmp", "$url/descriptions" );
        if ($status) {
            logfail();
            print
"Warning: the descriptions file could not be fetched. The --describe option will not be available"
              if $status;
        }
        else {
            logend();
            rename "${desc_file}.tmp", $desc_file;
        }
    }
    system( "/bin/touch", $filename ) if ( -w $filename );
}

# Get wanted package and its dependencies
#   pkglist - packages to fetch
#   mode - 4 = stream silent
sub fetch_pkgs {
    my ( $mode, @pkglist ) = @_;
    my $wgetopts = "";
    my ( $file, $status );
    my $FH = ( $mode == 4 ? *STDERR : *STDOUT );

    if ( scalar(@pkglist) > 1 && !$yes ) {
        print scalar(@pkglist),
          " packages to fetch. Do you want to continue? ([y],n,auto) ";
        chomp( my $prompt = <STDIN> );
        if ( $prompt =~ /^[nN]/ ) {
            myexit( "", "", 0 );
        }
        elsif ( $prompt =~ /^a(uto)*/i ) {
            print "Turning on automatic mode as if --yes was passed.\n";
            $yes = 1;
        }
    }
    $wgetopts .= " $config{wgetopts}" if $config{wgetopts};
    my $i = 1;
    foreach my $pkg (@pkglist) {
        $file = parse_catalog( $pkg, 3 );
        if ( -r "$pkgdir/$file" && -s _ != parse_catalog( $pkg, 5 ) )
        {    # Broken/empty files may be present
            print $FH "Removing non-matching local file for $pkg.\n";
            unlink "$pkgdir/$file";
        }
        if ( -r "$pkgdir/$file" ) {
            $i++;
            print $FH "A local copy of $pkg exists and is of matching size.\n";
        }
        else {
            print $FH "=> Fetching $pkg ("
              . $i++ . "/"
              . scalar(@pkglist)
              . ") ...\n";
            locate_wget() unless ( -x $wget ); # If wget disappeared locate alt.
            logoutput();
            my $tried;
            foreach ( @{ $pkgmirror{$file} } ) {
                $tried = 1;
                unlink "$pkgdir/${file}.tmp";
                if ( $_ =~ /^file:/ ) {
                    my ($tmpcatsrc) = ( $_ =~ /^file:\/(.+)$/ );
                    run_hooks( "prefetch", "$tmpcatsrc/$file", $pkg );
                    $status = system( "/bin/cp", "$tmpcatsrc/$file",
                        "$pkgdir/${file}.tmp" );
                }
                else {
                    run_hooks( "prefetch", "$_/$file", $pkg );

                    $status = system( "$wget", split( ' ', $wgetopts ),
                        "-O", "$pkgdir/${file}.tmp", "$_/$file" );
                }
                unless ($status) {
                    run_hooks( "postfetch", "$_/$file", $pkg );
                    last;
                }
            }
            if ( $status || !$tried ) {
                logfail();
                myexit(
"\nFetching of $pkg failed. Try updating your catalog with pkgutil -U.",
                    "", 1
                );
            }
            else {
                logend();
                rename "$pkgdir/${file}.tmp", "$pkgdir/$file";
            }
        }
        md5( $pkg, "$pkgdir/$file", $mode ) if $use_md5;
    }
}

# Find files in package (-F)
sub find_file {
    my $contents = "/var/sadm/install/contents";

    # Flush the pkgserv cache
    if ( -x "/usr/bin/pkgadm" ) {
        system("/usr/bin/pkgadm sync > dev/null 2>&1");
    }

    foreach (@ARGV) {
        my $FILE = gensym();
        open( $FILE, "<$contents" )
          or myexit( "Can't open $contents", "$!", 1 );
        while ( my $line = <$FILE> ) {
            chomp $line;
            my ( $file, $pkg ) = ( $line =~ /^(.+?)\s.+\s(.+)$/ );
            ($file) = ( $file =~ /^(.+)=/ ) if ( $file =~ /=/ );
            print "$file\t$pkg\n" if ( $file =~ /\Q$_\E/ );
        }
        close $FILE;
    }
    myexit( "", "", 0 );
}

# Format size in bytes to more human friendly format with suffixes
#   size - size in bytes
sub format_byte {
    my ($size) = @_;
    my $suffix = "B";

    if ( $size > 1024 ) {
        $size /= 1024;
        $suffix = "KB";
    }
    if ( $size > 1024 ) {
        $size /= 1024;
        $suffix = "MB";
    }
    if ( $size > 1024 ) {
        $size /= 1024;
        $suffix = "GB";
    }
    return sprintf( "%1.1f %s", $size, $suffix );
}

# Get which distribution a package is in
#   package - package to check
sub get_dist {
    my ($package) = @_;

    my $file = parse_catalog( $_, 3 );
    my @catalog = ( @{ $pkgmirror{$file} } );

    #my ($dist) = ($catalog[0] =~ /^.*\/(\S+)\/[i386|sparc]/); # show last part
    my ($dist) =
      ( $catalog[0] =~ /^.*\/(\S+\/\S+)\/[i386|sparc]/ );  # show two last parts
    print STDERR "DEBUG:dist:$dist\n" if $debug;

    return $dist;
}

# Check signature on catalog
#   catalog - filename of catalog to gpg check
#   parse   - silence output if set
sub gpg {
    my ( $catalog, $parse ) = @_;
    my $line;
    my $gpg_signed_catalog = 0;

    if ( !-x "/opt/ozsw/bin/gpg" ) {
        myexit(
"GPG not found! Install OZSWgnupg (pkgutil -i OZSWgnupg) or disable use_gpg in pkgutil.conf.",
            "", 1
        );
    }
    my $CATALOG = gensym();
    open( $CATALOG, "<$catalog" ) or myexit( "Can't open $catalog", "$!", 1 );
    while ( $line = <$CATALOG> ) {
        $gpg_signed_catalog = 1 if ( $line =~ /-BEGIN PGP SIGNED MESSAGE-/ );
        last;
    }
    close $CATALOG;
    print STDERR "DEBUG:GPG catalog: $gpg_signed_catalog\n" if $debug;
    if ($gpg_signed_catalog) {
        print "Checking integrity of $catalog with gpg.\n" unless $parse;
        my $gpghomedir = "";
        if ( $config{gpg_homedir} ) {
            $gpghomedir = "--homedir $config{gpg_homedir}";
        }
        else {
            if ( check_pkg( "OZSWpki", 0 ) eq "notinst" ) {
                $gpghomedir = "";
            }
            else {
                $gpghomedir = "--homedir /var/opt/ozsw/pki";
            }
        }
        my $status = system( "/opt/ozsw/bin/gpg", split( ' ', $gpghomedir ),
            "--verify", "$catalog" );
        print STDERR "DEBUG:GPG signature: "
          . ( ($status) ? "Bad" : "Good" ) . "\n"
          if $debug;
        myexit( "Bad signature detected in catalog!", "", 1 ) if $status;
        rename( $catalog, $catalog . ".asc" )
          or myexit( "Could not rename catalog!", "", 1 );
        $status =
          system("/opt/ozsw/bin/gpg $gpghomedir $catalog.asc 2> /dev/null");
        myexit( "Catalog signature is not correct!", "", 1 ) if $status;
        rename( $catalog . ".asc", $catalog )
          or myexit( "Could not rename catalog!", "", 1 );
        print STDERR "DEBUG:Status GPG: $status\n" if $debug;

        # Save catalog signer
        my $gpgoutput = `/opt/ozsw/bin/gpg $gpghomedir --verify $catalog 2>&1`;
        ( $catinfo{$catalog}{gpg} ) =
          ( $gpgoutput =~ /^gpg: .+ signature from (.+)$/m );
    }
    else {
        myexit(
"Catalog $catalog is not signed! Check your mirror settings or disable use_gpg in pkgutil.conf.",
            "", 1
        );
    }
}

# Initialize (note that this section is before options are parsed so
#             -D does not work here. Set debug to 1 at top of script)
#   conf - custom configuration
#   param - configuration overrides
sub init {
    my ( $conf, @param ) = @_;
    my @conf_file;
    if ($conf) {
        @conf_file = ($conf);
    }
    else {
        @conf_file =
          ( "/opt/ozsw/etc/pkgutil.conf", "/etc/opt/ozsw/pkgutil.conf" );
    }
    %config = (
        "mirror"                  => [],
        "pkgaddopts"              => "",
        "pkgrmopts"               => "",
        "wgetopts"                => "",
        "use_gpg"                 => "",
        "use_md5"                 => "",
        "pkgliststyle"            => "2",
        "maxpkglist"              => "",
        "nonozsw"                  => "",
        "stop_on_hook_soft_error" => "",
        "exclude_pattern"         => "",
        "gpg_homedir"             => "",
        "root_path"               => "",
        "deptree_filter_common"   => "",
        "show_current"            => "true",
        "catalog_not_cached"      => "true",
        "catalog_update"          => "14"
    );

    foreach my $file (@conf_file) {
        if ( -r $file ) {    # If conf file found, parse it
            print STDERR "DEBUG:Conf file: $file\n" if $debug;
            my $CONFIG = gensym();
            open( $CONFIG, "<$file" ) or myexit( "Can't open $file", "$!", 1 );
            while (<$CONFIG>) {
                chomp;       # Remove newline
                s/#.*//;     # Remove comments
                s/^\s+//;    # Remove leading white
                s/\s+$//;    # Remove trailing white
                next unless length;    # Next if nothing left
                my ( $var, $value ) = split( /\s*=\s*/, $_, 2 );
                unless ( exists $config{$var} ) {
                    print "Unrecognized option (in $file): $var\n";
                    next;
                }
                print STDERR "DEBUG:Config:Found $var = $value\n" if $debug;
                my $r = ref( $config{$var} );
                if ( !$r ) {
                    $config{$var} = $value;
                }
                elsif ( $r eq 'ARRAY' ) {
                    if ( grep { /^$value$/ } @{ $config{$var} } ) {
                        print STDERR
                          "Duplicate mirror definition skipped ($value).\n";
                    }
                    else {
                        push( @{ $config{$var} }, $value );
                    }
                }
            }
            close $CONFIG;
        }
    }

    # Configuration overrides using -p
    foreach (@param) {
        if (/mirror/) {
            print STDERR "Do not use -p for mirror, use -t instead!\n";
            next;
        }
        my ( $var, $value ) = split(/:/);
        unless ( exists $config{$var} ) {
            print "Unrecognized option (using --param): $var\n";
            next;
        }
        my $r = ref( $config{$var} );
        if ( !$r ) {
            $config{$var} = $value;
        }
        elsif ( $r eq 'ARRAY' ) {
            push( @{ $config{$var} }, $value );
        }
    }

    if ($debug) {
        foreach ( sort keys %config ) {
            print STDERR "DEBUG:$_: "
              . (
                ( $_ eq "mirror" )
                ? join( " ", @{ $config{$_} } )
                : $config{$_}
              ) . "\n";
        }
    }

    $pkgliststyle = $config{pkgliststyle} if defined $config{pkgliststyle};
    $maxpkglist   = $config{maxpkglist}   if $config{maxpkglist};
    $use_md5 = $use_gpg = $nonozsw = 0;
    $use_md5 = 1 if ( $config{use_md5} eq "true" );
    $use_gpg = 1 if ( $config{use_gpg} eq "true" );
    $nonozsw  = 1 if ( $config{nonozsw}  eq "true" || $config{nonozsw} eq "yes" );
}

# Install local packages
#   pkglist - packages to install
sub inst_loc_pkgs {
    my @pkglist    = @_;
    my $pkgadd     = "pkgadd";
    my $pkgaddopts = "";
    my $pkgrm      = "pkgrm";
    my $pkgrmopts  = "";
    my $pkginfo    = "pkginfo";
    my $pkgtrace   = "";
    my $file;

    $pkgaddopts .= " $config{pkgaddopts}" if $config{pkgaddopts};
    $pkgrmopts  .= " $config{pkgrmopts}"  if $config{pkgrmopts};
    $pkgtrace = "-v" if $trace;
    my $root_path = $config{root_path} ? "-R $config{root_path}" : "";

    foreach (@pkglist) {
        my $pkgforce = "";
        $pkgforce = "-a $admin -n" if ( $yes && -r $admin );

        print "\n=> Installing $_\n";
        $file = $_;
        if ( $_ =~ /\.gz$/ ) {    # Not all packages are compressed
            my $status = locate_gzip();
            ($file) = ( $file =~ /(.+)\.gz$/ );
            if ( !$status ) {
                if ($yes) {
                    `$gzip -c -f -d $_ > $file`;
                }
                else {
                    `$gzip -c -d $_ > $file`;
                }
            }
            else {
                myexit( "\nGzip not found, please install OZSWgzip.", "", 1 );
            }
        }

        # Remove first if the package is already installed
        my $pkgname = `$pkginfo $root_path -d $file`;
        ($pkgname) = ( $pkgname =~ /\s(.+?)\s/ );
        my $tmp = check_pkg( $pkgname, 0 );
        if ( $tmp ne "notinst" ) {
            print "\n=> Removing currently installed $pkgname\n";
            system(
                "$pkgrm",
                split( ' ', $root_path ),
                split( ' ', $pkgforce ),
                split( ' ', $pkgtrace ),
                split( ' ', $pkgrmopts ), "$pkgname"
            );
        }

        # Install
        system(
            "$pkgadd",
            split( ' ', $root_path ),
            split( ' ', $pkgforce ),
            split( ' ', $pkgtrace ),
            split( ' ', $pkgaddopts ),
            "-d", "$file"
        );

        # Check if pkgadd operation exited successfully
        if ( $? != 0 ) {

            # If failed pkgadd op and --yes used, exit immediately
            myexit( "Exiting pkgutil due to pkgadd error", $? >> 8, 1 ) if $yes;

            printf "\npkgadd failed with exit code: %d\n", $? >> 8;
            print
"Exit from pkgutil and fix this issue first (recommended)? ([y],n) ";
            chomp( my $prompt = <STDIN> );
            myexit( "Exiting pkgutil", "", 1 ) if ( $prompt !~ /^[nN]/ );
        }

        if ( $_ =~ /\.gz$/ ) {    # Not all packages are compressed
            unlink "$file" or myexit( "Can't delete $file", "$!", 1 );
        }
    }
    myexit( "", "", 0 );
}

# Install packages
#   batchmode  - whether we're updating some of the packages or doing new
#                installs only (values: upgrade or install)
#   updlistlen - number of packages to update
#   pkglist    - packages to install
sub inst_pkgs {
    my $batchmode  = shift;
    my $updlistlen = shift;
    my @pkglist    = @_;
    my $tmp        = "";
    my $pkgadd     = "pkgadd";
    my $pkgaddopts = "";
    my $pkgrm      = "pkgrm";
    my $pkgrmopts  = "";
    my $pkgtrace   = "";
    my $pkgask     = "";
    my %upg_pkglist;
    my ( $file, $name );

    $pkgaddopts .= " $config{pkgaddopts}" if $config{pkgaddopts};
    $pkgrmopts  .= " $config{pkgrmopts}"  if $config{pkgrmopts};
    $pkgtrace = "-v" if $trace;
    my $root_path = $config{root_path} ? "-R $config{root_path}" : "";

    run_hooks( "prebatch$batchmode", @pkglist );

    # Handle pkgutil as a special case, always complete pkgutil upgrade
    # before proceeding with rest of list
    if ( scalar(@pkglist) > 1 ) {
        if ( my @tmplist = grep { /^OZSWpkgutil-/ } @pkglist ) {
            print "\n=> Updating pkgutil first ...\n";
            @pkglist = grep { !/^OZSWpkgutil-/ } @pkglist;
            $updlistlen--;
            inst_pkgs( 'upgrade', 1, $tmplist[0] );
        }
    }

    # Removing old version of all packages being upgraded
    my $i = 1;
    foreach ( reverse @pkglist ) {
        my $pkgforce = "";
        $pkgforce = "-a $admin -n" if ( $yes && -r $admin );
        my ($pkgname) = ( $_ =~ /^(.+)-/ );
        $tmp = check_pkg( $pkgname, 0 );
        if ( $tmp ne "notinst" ) {

            # A small sanity check
            myexit(
"Batch hook 'install' was run, but we've found a package being upgraded. Bailing out.",
                "", 1
            ) if ( $batchmode eq 'install' );

            $upg_pkglist{$_} = 1;
            run_hooks( "preupgrade", $_ );
            print "\n=> Removing old version of $pkgname ("
              . $i++ . "/"
              . $updlistlen
              . ") ...\n";
            system(
                "$pkgrm",
                split( ' ', $root_path ),
                split( ' ', $pkgforce ),
                split( ' ', $pkgtrace ),
                split( ' ', $pkgrmopts ), "$pkgname"
            );
        }
    }

    # Install all packages being upgraded/installed
    $i = 1;
    foreach (@pkglist) {
        my $pkgforce = "";
        $pkgforce = "-a $admin -n" if ( $yes && -r $admin );
        $file = parse_catalog( $_, 3 );
        $name = parse_catalog( $_, 2 );
        print "\n=> Installing $_ ("
          . $i++ . "/"
          . scalar(@pkglist)
          . ") ...\n";
        if ( $file =~ /\.gz$/ ) {    # Not all packages are compressed
            my $status = locate_gzip();
            if ( !$status ) {
                if ($yes) {
                    `$gzip -f -d $pkgdir/$file`;
                }
                else {
                    `$gzip -d $pkgdir/$file`;
                }
            }
            else {
                myexit( "\nGzip not found, please install OZSWgzip.", "", 1 );
            }
            ($file) = ( $file =~ /(.+)\.gz$/ );
        }

       # We may have already run preupgrade above. Running upgrade _and_ install
       # is invalid
        run_hooks( "preinstall", $_ )
          unless ( defined $upg_pkglist{$_} && $upg_pkglist{$_} == 1 );

        # Support response (pkgask) files
        $pkgask =
          ( -r "$workdir/pkgask/$name" ) ? "-r $workdir/pkgask/$name" : "";
        print STDERR "DEBUG:pkgask: $pkgask\n" if $debug;

        # Install
        system(
            "$pkgadd",
            split( ' ', $root_path ),
            split( ' ', $pkgforce ),
            split( ' ', $pkgtrace ),
            split( ' ', $pkgask ),
            split( ' ', $pkgaddopts ),
            "-d",
            "$pkgdir/$file",
            "$name"
        );

        # Check if pkgadd operation exited successfully
        if ( $? != 0 ) {

            # If failed pkgadd op and --yes used, exit immediately
            myexit( "Exiting pkgutil due to pkgadd error", $? >> 8, 1 ) if $yes;

            printf "\npkgadd failed with exit code: %d\n", $? >> 8;
            print
"Exit from pkgutil and fix this issue first (recommended)? ([y],n) ";
            chomp( my $prompt = <STDIN> );
            myexit( "Exiting pkgutil", "", 1 ) if ( $prompt !~ /^[nN]/ );
        }

        unlink "$pkgdir/$file"
          or myexit( "Can't delete $pkgdir/$file", "$!", 1 );

        if ( defined $upg_pkglist{$_} && $upg_pkglist{$_} == 1 ) {
            run_hooks( "postupgrade", $_ );
        }
        else {
            run_hooks( "postinstall", $_ );
        }
    }
    run_hooks( "postbatch$batchmode", @pkglist );
}

# Install (-i) option
#   mode  - 0 = install, 1 = download only, 2 = remove, 3 = stream,
#           4 = stream silent, 5 = extract
#   deps  - option --nodeps set or not
#   parse - option --parse set or not
sub install {
    my ( $mode, $deps, $parse ) = @_;
    my $retvalue;
    my (
        @tmppkglist, @revpkglist, @newpkglist, @instpkglist,
        @updpkglist, @curpkglist, @dlpkglist
    );
    my $FH = ( $mode == 4 ? *STDERR : *STDOUT );
    my ( %pkgvers, @pkglist, @excludelist )
      ;    # @pkglist is specifically in the correct order
    my $root_path = $config{root_path} ? "-R $config{root_path}" : "";
    my $pkgrmopts = "";
    $pkgrmopts = "$config{pkgrmopts}" if $config{pkgrmopts};

    print $FH "Solving needed dependencies ...\n" unless ( $deps || $parse );

    foreach my $tmppkg (@ARGV) {
        my $tmp  = parse_catalog( $tmppkg, 2 );
        my $vers = parse_catalog( $tmppkg, 1 );
        if ( $tmp ne 'not in catalog' && ( $tmp =~ /^OZSW/ $nonozsw ) ) {
            push( @pkglist, $tmp );
            $pkgvers{$tmp} = $vers;
        }
        else {
            myexit( "Package $tmppkg not in catalog. Exiting.", "", 1 );
        }
    }

    unless ($deps) {    # Skip adding dependencies? (option --nodeps)
        my %finished;
        foreach my $tmppkg2 (@pkglist) {

           # N.B. This array grows and we keep looping until we've satisfied all
           # of the dependencies
            print STDERR "DEBUG:Loop #", scalar(@pkglist),
              " (limit $maxpkglist)\n"
              if $debug;

            my @tmpdeps;

            # Next two lines from Joe Baro reduces iterations
            next if $finished{$tmppkg2};
            $finished{$tmppkg2} = 1;

            if ( $tmppkg2 ne "OZSWcommon" ) {
                $retvalue = parse_catalog( $tmppkg2, 6 );
                if ( $retvalue eq "not in catalog" ) {
                    system( "pkginfo", split( ' ', $root_path ),
                        "-q", "$tmppkg2" );
                    if ( $? == 0 )
                    { # Already installed...forgive its absence from the catalog
                        print STDERR "Warning: $tmppkg2 not in catalog.\n";
                        next;
                    }
                    else {
                        myexit( "Package $tmppkg2 not in catalog. Exiting.",
                            "", 1 );
                    }
                }
                else {
                    @tmpdeps = split( /\|/, $retvalue );
                    print STDERR "DEBUG:depmap: $tmppkg2 -> ", $retvalue, "\n"
                      if $debug;
                }
            }
            else {
                next;
            }
            if ( $tmpdeps[0] ne "none" && $tmpdeps[0] ne "not in catalog" ) {
                foreach my $dep (@tmpdeps) {

# There is no support for dependencies against a particular version, e.g. CSWzope-3.3.1, just CSWzope
                    if ( !exists $pkgvers{$dep} ) {
                        $pkgvers{$dep} = parse_catalog( $dep, 1 );
                    }
                    push( @pkglist, $dep )
                      ;    # Append to list, does not include deduplication
                }
            }
            next;
        }
    }
    print STDERR "DEBUG:pkglist ", join( ",", @pkglist ), "\n" if $debug;

    # Now reverse and deduplicate @pkglist. Need the last instance of each
    # package if reading @pkglist forwards
    my @tmprevpkglist = dedup( reverse @pkglist );
    foreach (@tmprevpkglist) {
        push( @revpkglist, $_ ) if $pkgvers{$_};    # Not if excluded
    }

    if ( $mode == 2 ) {                             # If -r used
        if ( scalar(@revpkglist) > 0 ) {
            my @rmpkgs = ();
            foreach my $p (@revpkglist) {
                push( @rmpkgs, "$p-" . $pkgvers{$p} );
            }
            remove( reverse @rmpkgs );
        }
    }

    print STDERR "DEBUG:revpkglist: " . scalar(@revpkglist) . "\n",
      join( ",", @revpkglist ), "\n"
      if $debug;
    print STDERR "DEBUG:Loop #", scalar(@pkglist), " (limit $maxpkglist)\n"
      if $debug;

    unless ($deps) {
        print $FH "Solving dependency order ...\n" unless $parse;
        my @revpkglist2 = @revpkglist;
        my @revpkglist3;
        foreach (@revpkglist2) {
            if ( scalar(@revpkglist2) > $maxpkglist ) {
                print
"Loop protection limit ($maxpkglist iterations) hit. There's probably a\ncyclic dependency in the catalog.\n";
                print "Do you want to continue anyway? ([y],n) ";
                chomp( my $prompt = <STDIN> );
                myexit( "", "", 0 ) if ( $prompt =~ /^[nN]/ );
                @revpkglist3 = dedup(@revpkglist2);
                last;
            }
            my $dep = parse_catalog( $_, 6 );
            if ( $dep eq "none" || $dep eq "not in catalog" ) {
                push( @revpkglist3, $_ );
            }
            else {
                my $allhit = 1;
                foreach my $i ( split( /\|/, $dep ) ) {
                    my $hit = 0;
                    foreach my $j (@revpkglist3) {
                        if ( $i eq $j ) {
                            $hit = 1;
                            last;
                        }
                    }
                    if ( !$hit ) {    # no hit, add last for re-processing
                        push( @revpkglist2, $_ );
                        $allhit = 0;
                        last;
                    }
                }
                if ($allhit) {        # all deps ok, add to final list
                    push( @revpkglist3, $_ );
                }
            }
        }
        print STDERR "DEBUG:revpkglist3: " . scalar(@revpkglist3) . "\n",
          join( ",", @revpkglist3 ), "\n"
          if $debug;
        @revpkglist = @revpkglist3;
    }

    # Clean list from command line exclude patterns
    foreach my $pattern (@exclude) {
        my @tmppkglist = grep { !/$pattern/ } @revpkglist;
        push( @excludelist, grep { /$pattern/ } @revpkglist );
        @revpkglist = @tmppkglist;
    }

    # Clean list from exclude patterns in pkgutil.conf
    foreach my $pattern ( split( /\s+?/, $config{exclude_pattern} ) ) {
        my @tmppkglist = grep { !/$pattern/ } @revpkglist;
        push( @excludelist, grep { /$pattern/ } @revpkglist );
        @revpkglist = @tmppkglist;
    }

    # Incompatible packages (I-dependencies)
    my @ideppkglist;
    foreach (@revpkglist) {
        my $retvalue = parse_catalog( $_, 8 );
        push( @ideppkglist, split( /\|/, $retvalue ) )
          if ( $retvalue ne "none" );
    }

    # Clean the list from packages not installed
    my @ideppkglist2;
    foreach (@ideppkglist) {

        # Check if pkg is present first
        push( @ideppkglist2, $_ ) if ( check_pkg( $_, 0 ) ne "notinst" );
    }
    @ideppkglist = @ideppkglist2;
    undef @ideppkglist2;

    # Which packages do we really need to download?
    if ( $mode == 0 || $mode == 2 ) {
        foreach (@revpkglist) {
            my $old = check_pkg( $_, 0 );
            my $new = $pkgvers{$_};
            if ( $old eq "notinst" ) {    # New package needed
                push( @newpkglist, "$_-$new" );
            }
            else {                        # Package already installed
                my $cmpval = verscmp( $new, $old );
                if ( $cmpval == 1 || ( $cmpval != 0 && $force ) )
                {                         # Older or --force
                    push( @updpkglist, "$_-$new" );    # Updated package needed
                }
                else {
                    push( @curpkglist, "$_-$old" );    # Package is current
                }
            }
        }
    }
    else {    # If -s or --extract then just copy revpkglist to newpkglist
        foreach (@revpkglist) {
            push( @newpkglist, "$_-$pkgvers{$_}" );
        }
    }

    my $pkgsep = ( $pkgliststyle ? "\n\t" : " " );

    # Only present updated and current packages when doing install/upgrade
    if ( $mode == 0 ) {
        if ( scalar(@curpkglist) > 0 && $config{show_current} eq "true" ) {
            print scalar(@curpkglist) . " CURRENT packages:$pkgsep",
              join( "$pkgsep", sort @curpkglist ), "\n";
        }

        if ( scalar(@updpkglist) > 0 ) {
            print "Install " . scalar(@updpkglist) . " UPDATED packages";
            if ( $pkgliststyle == 2 ) {
                foreach ( sort @updpkglist ) {
                    print "$pkgsep$_ (" . get_dist($_) . ")";
                }
                print "\n";
            }
            else {
                print $FH ":$pkgsep", join( "$pkgsep", sort @updpkglist ), "\n";
            }
        }
    }

    # Different text for install/upgrade vs. the rest
    if ( scalar(@newpkglist) > 0 ) {
        print $FH ( $mode == 0 )
          ? "Install " . scalar(@newpkglist) . " NEW packages:"
          : "Package list:"
          unless $parse;
        if ( $pkgliststyle == 2 || $parse ) {
            foreach ( sort @newpkglist ) {
                if ($parse)
                {    # parse mode to display catalog name, catalog and md5
                    print parse_catalog( $_, 0 ) . "\t"
                      . get_dist($_) . "\t"
                      . parse_catalog( $_, 4 ) . "\n";
                }
                else {
                    print "$pkgsep$_ (" . get_dist($_) . ")";
                }
            }
            print "\n" unless $parse;
        }
        else {
            print $FH "$pkgsep", join( "$pkgsep", sort @newpkglist ), "\n";
        }
    }

    # Only present incompatible packages when doing install/upgrade
    if ( $mode == 0 ) {
        if ( scalar(@ideppkglist) > 0 ) {
            print "Remove "
              . scalar(@ideppkglist)
              . " INCOMPATIBLE packages:$pkgsep",
              join( "$pkgsep", sort @ideppkglist ), "\n";
        }
    }

    # Show excluded packages
    if ( scalar(@excludelist) > 0 ) {
        print $FH scalar(@excludelist) . " EXCLUDED packages:$pkgsep",
          join( "$pkgsep", sort @excludelist ), "\n";
    }

    # What packages do we need to fetch?
    # Make sure they are in correct order
    foreach my $i (@revpkglist) {
        my $hit = 0;
        foreach (@curpkglist) {
            my ($j) = ( $_ =~ /^(.+)-/ );
            if ( $i eq $j ) {
                $hit = 1;
                print STDERR "DEBUG:i: $i j: $j hit: $hit\n" if $debug;
                last;
            }
            print STDERR "DEBUG:i: $i j: $j hit: $hit\n" if $debug;
        }
        my $tmp = "$i-$pkgvers{$i}";
        push( @dlpkglist, $tmp ) unless $hit;
    }
    print STDERR "DEBUG:dlpkglist: " . scalar(@dlpkglist) . "\n",
      join( ",", @dlpkglist ), "\n"
      if $debug;

    # Print info before exiting if there's nothing to do
    myexit( "\nNothing to do.", "", 0 ) if ( scalar(@dlpkglist) == 0 );

    # Print size of download
    if ( scalar(@dlpkglist) > 0 && $parse == 0 ) {
        foreach (@dlpkglist) {
            $size += parse_catalog( $_, 5 );
        }
        print $FH "Total size: " . format_byte($size) . "\n";
    }

    myexit( "",                                   "", 0 ) if $parse;
    myexit( "Option -n selected, stopping here.", "", 0 ) if $nomod;

    if ( scalar(@dlpkglist) > 0 ) {
        fetch_pkgs( $mode, @dlpkglist );
    }

    # Display a list of files when downloading
    if ( $mode == 1 ) {
        print "\nPackages downloaded to $pkgdir:\n";
        foreach (@dlpkglist) {
            my $file = parse_catalog( $_, 3 );
            print "$file";
            $file = "$pkgdir/$file";
            print " (OK)\n"      if ( -s $file );
            print " (empty)\n"   if ( -z $file );
            print " (missing)\n" if ( !-e $file );
        }
    }

    extract_pkg(@dlpkglist) if ( $mode == 5 );    # --extract

    return @revpkglist if ( $mode == 3 || $mode == 4 );    # If -s used

    # Warn if cswclassutils is in the install list and we have a
    # read-only /usr/sadm/install/scripts dir
    if ( grep { /^OZSWcas-/ } @dlpkglist ) {    # CAS pkg in the list?
        if ( ! -w "/usr/sadm/install/scripts" && $mode == 0 ) {	# /usr ro?
            myexit(
                "A class action script is about to be installed but\n"
                  . "/usr/sadm/install/scripts is read-only so it will fail.\n"
                  . "If you're running a sparse zone, please install/update the\n"
                  . "class action scripts from the global zone. If you want to\n"
                  . "proceed with this installation you have to exclude the\n"
                  . "CAS package (use the -x option). Exiting.",
                "", 1
            );
        }
    }

    # Remove incompatible packages
    if ( scalar(@ideppkglist) > 0 && $mode == 0 ) {
        my $pkgrm    = "pkgrm";
        my $pkgtrace = "";
        my $pkgforce = "";

        if ( !$yes ) {
            print scalar(@ideppkglist),
                " incompatible package"
              . ( scalar(@ideppkglist) > 1 ? "s" : "" )
              . " to remove. Do you want to continue? ([y],n,auto) ";
            chomp( my $prompt = <STDIN> );
            if ( $prompt =~ /^[nN]/ ) {
                myexit( "", "", 0 );
            }
            elsif ( $prompt =~ /^a(uto)*/i ) {
                print "Turning on automatic mode as if --yes was passed.\n";
                $yes = 1;
            }
        }
        $pkgforce = "-a $admin -n" if ( $yes && -r $admin );

        foreach (@ideppkglist) {
            print "\n=> Removing incompatible package $_\n";
            system(
                "$pkgrm",
                split( ' ', $root_path ),
                split( ' ', $pkgforce ),
                split( ' ', $pkgtrace ),
                split( ' ', $pkgrmopts ), "$_"
            );
        }
    }

    if ( $mode == 0 ) {    # If -d not used, install
        if ( scalar(@dlpkglist) > 0 ) {
            inst_pkgs( scalar(@updpkglist) ? 'upgrade' : 'install',
                scalar(@updpkglist), @dlpkglist );
        }
    }
    myexit( "", "", 0 );
}

# List dependencies (part of --cleanup)
sub list_deps {
    my $pkgdir =
      $config{root_path} ? "$config{root_path}/var/sadm/pkg" : "/var/sadm/pkg";
    my %deps;

    chdir($pkgdir) or myexit( "Could not cd to $pkgdir", "", 1 );
    foreach ( glob("OZSW*") ) {
        if ( -r "$_/install/depend" ) {
            my $FILE = gensym();
            open( $FILE, "<$_/install/depend" );
            while (<$FILE>) {
                chomp;
                if (/^P\s+\S/) {
                    my ($deptmp) = (/P\s+(\S+)/);
                    $deps{$deptmp} = 1
                      if ( $deptmp ne $_ && $deptmp !~ /^SUNW/ )
                      ;    # Skip if circular or SUNW
                }
            }
            close $FILE;
        }
    }
    my @deps = sort keys %deps;
    return @deps;
}

# List files in package (-L)
sub list_file {
    my $contents =
      $config{root_path}
      ? "$config{root_path}/var/sadm/install/contents"
      : "/var/sadm/install/contents";

    # Flush the pkgserv cache
    if ( -x "/usr/bin/pkgadm" ) {
        system( "/usr/bin/pkgadm"
              . ( $config{root_path} ? " -R " . $config{root_path} : "" )
              . " sync > /dev/null 2>&1" );
    }

    foreach (@ARGV) {
        if ( -r $_ && -f _ ) {    # Argument is a package file
            if ( $_ =~ /\.pkg$/ ) {
                my (@tmp) = `pkgchk -l -d $_ all`;
                foreach (@tmp) {
                    next unless ( $_ =~ /^Pathname:\s\// );
                    my ($file) = ( $_ =~ /^Pathname:\s(.+?)\s/ );
                    ($file) = ( $file =~ /^(.+)=/ ) if ( $file =~ /=/ );
                    print "$file\n";
                }
            }
            else {
                print "Package file name ($_) must end in .pkg\n";
                next;
            }
        }
        else {    # Argument is a package name
            my $tmp;
            if ( $_ =~ /^[A-Z]/ ) {
                $tmp = $_;
            }
            else {
                $tmp = parse_catalog( $_, 2 );
            }
            my $FILE = gensym();
            open( $FILE, "<$contents" )
              or myexit( "Can't open $contents", "$!", 1 );
            while ( my $line = <$FILE> ) {
                chomp $line;
                my ( $file, $pkg ) = ( $line =~ /^(.+?)\s.+\s(.+)$/ );
                ($file) = ( $file =~ /^(.+)=/ ) if ( $file =~ /=/ );
                print "$file\n" if ( $pkg eq $tmp );
            }
            close $FILE;
        }
    }
    myexit( "", "", 0 );
}

# List obsolete files (part of --cleanup)
sub list_obsolete {
    my $pkgdir =
      $config{root_path} ? "$config{root_path}/var/sadm/pkg" : "/var/sadm/pkg";
    my @legacy;

    chdir($pkgdir) or myexit( "Could not cd to $pkgdir", "", 1 );
    foreach ( glob("OZSW*") ) {
        push( @legacy, $_ ) if ( -e "$_/install/obsolete" );
    }
    return @legacy;
}

# List installed packages (-l)
sub list_pkgs {
    my $pkgdir =
      $config{root_path} ? "$config{root_path}/var/sadm/pkg" : "/var/sadm/pkg";

    chdir($pkgdir) or myexit( "Could not cd to $pkgdir", "", 1 );
    foreach ( $nonozsw ? glob("*") : glob("OZSW*") ) {
        if ( scalar(@ARGV) > 0 ) {
            foreach my $arg (@ARGV) {
                print "$_\n" if ( $_ =~ /$arg/ );
            }
        }
        else {
            print "$_\n";
        }
    }
    myexit( "", "", 0 );
}

# Find gzip
sub locate_gzip {
    my @locations = ( "/opt/ozsw/bin", "/bin", "/usr/local/bin" );

    foreach (@locations) {
        if ( -x "$_/gzip" ) {
            next if check_binary("$_/gzip");
            $gzip = "$_/gzip";
            last;
        }
    }
    print STDERR "DEBUG:gzip: $gzip\n" if $debug;
    return 1 if ( !$gzip );
}

# Find wget
sub locate_wget {
    my @locations =
      ( "/opt/ozsw/bin", "/usr/sfw/bin", "/usr/local/bin", "/usr/bin" );
    my $libexec = "/opt/ozsw/libexec/pkgutil";

    foreach (@locations) {
        if ( -x "$_/wget" ) {
            next if check_binary("$_/wget");
            $wget = "$_/wget";
            last;
        }
    }

    # Check for wget binary included in pkgutil package
    if ( !$wget ) {
        chomp( my $loc_arch = `uname -p` );    # Arch type sparc|i386
        $wget = "$libexec/wget-$loc_arch" if ( -x "$libexec/wget-$loc_arch" );
    }
    if ( !$wget ) {
        $wget = "$libexec/wget" if ( -x "$libexec/wget" );
    }
    print STDERR "DEBUG:wget: " . ( $wget ? $wget : "-" ) . "\n" if $debug;
    return 1 if ( !$wget );
}

# These three routines are used to capture the output of sub processes into a
# log file. If the command completes successfuly, the log file is thrown away.
# If the command fails, the log file is printed along with an error message by
# calling logfail.

sub logoutput {
    my ( $fh );
    eval { require File::Temp; };    # Check for File::Temp
    if ( $@ ) {
        $logfile = "/tmp/pkgutil.$$.log.$^T";
    }
    else {
        ( $fh, $logfile ) = File::Temp::tempfile( DIR => "/tmp" );
    }
    open( OLDOUT, '>&STDOUT' );
    open( OLDERR, '>&STDERR' );
    open( STDOUT, "> $logfile" );
    open( STDERR, "> $logfile" );
}

sub logend {
    # Reference fh to silence warning
    select( OLDERR ); select( OLDOUT );

    # Restore output.
    select( STDOUT);
    close( STDOUT );
    open( STDOUT, ">&OLDOUT" );
    close( STDERR );
    open( STDERR, ">&OLDERR" );
    unlink( $logfile );
}

sub logfail {
    close( STDOUT );
    open( STDOUT, ">&OLDOUT" );
    close( STDERR );
    open( STDERR, ">&OLDERR" );
    open( LOG, "< $logfile" ) or
        die "pkgutil: Error, @_ - can't open log\n";
    print <LOG>;
    close( LOG );
    unlink( $logfile );
}

# Make a filename from the URL - /es to _es
#   url - url to mangle
sub mangle_url {
    my ($url) = @_;

    ( my $mangled = $url ) =~ s?\w+://??;
    $mangled =~ s?/?_?g;
    my $filename = "$workdir/catalog.$mangled";
    return $filename;
}

# Check md5 on downloaded files
#   mode - 4 = stream silent
sub md5 {
    my ( $pkg, $file, $mode ) = @_;
    my $md5;
    my $FH = ( $mode == 4 ? *STDERR : *STDOUT );

    eval { require Digest::MD5; };
    if ($@) {
        if ( -x "/opt/ozsw/bin/gmd5sum" ) {
            myexit( "Can't open $file", "$!", 1 ) unless ( -r $file );
            $md5 = `/opt/ozsw/bin/gmd5sum $file`;
            ($md5) = ( $md5 =~ /^(.*\w) / );
        }
        else {
            myexit(
"\nMD5 support not available! Install OZSWcoreutils to get MD5\nsupport. Another alternative is to install OZSWperl and use\nthat for pkgutil. Do that by having /opt/ozsw/bin before\n/usr/bin in your path.",
                "", 1
            );
        }
    }
    else {
        my $FILE = gensym();
        open( $FILE, "<$file" ) or myexit( "Can't open $file", "$!", 1 );
        binmode($FILE);
        $md5 = Digest::MD5->new->addfile($FILE)->hexdigest;
        close $FILE;
    }
    my $md5cat = parse_catalog( $pkg, 4 );
    print STDERR "DEBUG:$pkg md5 file: $md5\n$pkg md5 cat:  $md5cat\n"
      if $debug;
    myexit( "MD5 for $pkg doesn't match catalog!", "", 1 )
      if ( $md5 ne $md5cat );
    print $FH "MD5 for $pkg matched.\n";
}

# Do cleanup and exit with return code
#   msg - message to stderr
#   err - error ($!)
#   rc - return code when exiting
sub myexit {
    my ( $msg, $err, $rc ) = @_;

    my $text = ( $msg && $err ) ? "$msg: $err" : "$msg";
    $rc = 0 unless $rc;
    unlink $admin if ( $admin && -r $admin && $admin =~ /admin\.run$/ );
    print STDERR "$text\n" if $text;
    exit $rc;
}

# Parse catalog
#   pkg  - package to look for, can be both common name and true name
#   mode - what we want to return, field number from catalog
sub parse_catalog {
    my ( $pkg, $mode ) = @_;
    my ( $retvalue, $name );

    print STDERR "DEBUG:Parsing catalog ... looking for: $pkg (mode $mode)\n"
      if $debug;

    # Do we have an embedded version?
    my @p       = split /-/, $pkg;
    my $vers    = pop @p;
    my $tryname = join( '-', @p );

    # First check the whole name without stripping off a "version"
    # e.g. CSWdovecot-devel
    if ( $pkg{$pkg} ) {

        # SysV, no version (e.g. CSWdovecot-devel)
        $name = $pkg;
        undef $vers;
    }
    elsif ( $commonpkg{$pkg} && $pkg{ $commonpkg{$pkg} } ) {

        # Common (e.g. dovecot_devel)
        $name = $commonpkg{$pkg};
        undef $vers;
    }
    elsif ( $pkg{$tryname} ) {

        # SysV, version (e.g. CSWdovecot-devel-1.0.13,REV=2008.03.16)
        $name = $tryname;
    }
    elsif ( $commonpkg{$tryname} && $pkg{ $commonpkg{$tryname} } ) {

        # Common (e.g. dovecot_devel-1.0.13,REV=2008.03.16)
        $name = $commonpkg{$tryname};
    }

    if ($name) {

        # Have we a version match?
        if ( $vers && exists $pkg{$name}{$vers} ) {
            my @tmpcatline = split( / /, $pkg{$name}{$vers} );
            my $fieldsincatalog = scalar(@tmpcatline);
            if ( $mode == 8 && $fieldsincatalog < 9 ) {
                $retvalue = "none";
            }
            else {
                $retvalue = ( split( / /, $pkg{$name}{$vers} ) )[$mode];
            }
        }
        elsif ( !$vers ) {

            # We may have multiple versions in the catalog, take the highest
            # Note this won't work unless at least one has a REV in it
            my $highest =
              ( sort { verscmp( $b, $a ) } keys %{ $pkg{$name} } )[0];
            my @tmpcatline = split( / /, $pkg{$name}{$highest} );
            my $fieldsincatalog = scalar(@tmpcatline);
            if ( $mode == 8 && $fieldsincatalog < 9 ) {
                $retvalue = "none";
            }
            else {
                $retvalue = ( split( / /, $pkg{$name}{$highest} ) )[$mode];
            }
        }
        else {
            $retvalue = "not in catalog" if ( !$retvalue );
        }
    }
    else {
        $retvalue = "not in catalog" if ( !$retvalue );
    }
    print STDERR "DEBUG:Return value: $retvalue\n" if $debug;
    return $retvalue;
}

# Parse command line
sub parse_cl {
    my (
        $opt_install,           $opt_upgrade,          $opt_remove,
        $opt_download,          $opt_avail,            $opt_compare,
        $opt_compare_available, $opt_upd_cat,          $opt_deptree,
        $opt_email,             @opt_temp,             $opt_help,
        $opt_version,           $opt_syscheck,         $opt_stream,
        $opt_target,            $opt_output,           $opt_workdir,
        $opt_config,            $opt_compare_diffonly, $opt_listfile,
        $opt_findfile,          $opt_describe,         $opt_extract,
        $opt_nodeps,            $opt_single,           @opt_param,
        $opt_list,              $opt_parse,            $opt_cleanup,
        $opt_pkgdir,            $opt_rootpath,         $opt_catinfo
    );

    usage() unless scalar(@ARGV);

    # This call gives hooks a chance to look at the unaltered argument list
    print STDERR "DEBUG:preargproc: " . join( ' ', @ARGV ) . "\n" if $debug;
    run_hooks( "preargproc", @ARGV );

    $opt_parse = 0;

    GetOptions(
        "i|install"       => \$opt_install,
        "u|upgrade"       => \$opt_upgrade,
        "r|remove"        => \$opt_remove,
        "d|download"      => \$opt_download,
        "a|available"     => \$opt_avail,
        "A|compare-avail" => \$opt_compare_available,
        "describe"        => \$opt_describe,
        "c|compare"       => \$opt_compare,
        "C|compare-diff"  => \$opt_compare_diffonly,
        "config=s"        => \$opt_config,
        "U|catalog"       => \$opt_upd_cat,
        "e|email=s"       => \$opt_email,
        "t|temp=s"        => \@opt_temp,
        "y|yes"           => \$yes,
        "f|force"         => \$force,
        "s|stream"        => \$opt_stream,
        "T|target=s"      => \$opt_target,
        "o|output=s"      => \$opt_output,
        "x|exclude=s"     => \@exclude,
        "W|workdir=s"     => \$opt_workdir,
        "n|nomod"         => \$nomod,
        "D|debug"         => \$debug,
        "trace"           => \$trace,
        "h|help"          => \$opt_help,
        "v|version"       => \$opt_version,
        "l|list"          => \$opt_list,
        "L|listfile"      => \$opt_listfile,
        "F|findfile"      => \$opt_findfile,
        "extract"         => \$opt_extract,
        "single"          => \$opt_single,
        "deptree:i"       => \$opt_deptree,
        "N|nodeps"        => \$opt_nodeps,
        "parse"           => \$opt_parse,
        "p|param=s"       => \@opt_param,
        "cleanup"         => \$opt_cleanup,
        "catinfo"         => \$opt_catinfo,
        "P|pkgdir=s"      => \$opt_pkgdir,
        "R|rootpath=s"    => \$opt_rootpath,
        "V|syscheck"      => \$opt_syscheck
    ) || usage();

    # If --deptree was given without an argument set it to 999
    $opt_deptree = 999 if ( defined $opt_deptree && $opt_deptree == 0 );

    print STDERR "DEBUG:pkgutil version: $pkgutilver\n" if $debug;
    print STDERR "DEBUG:perl version: $]\n" if $debug;

    # Simple options
    usage()       if $opt_help;
    pkgutilver(0) if $opt_version;
    find_file()   if $opt_findfile;
    $config{root_path} = $opt_rootpath if $opt_rootpath;
    print STDERR "DEBUG:root_path: $config{root_path}\n" if $debug;

    # Installing local packages if all args are package files
    if ( scalar(@ARGV) && ( $opt_install || $opt_upgrade ) ) {
        my $notalocpkg = 0;
        foreach (@ARGV) {
            $notalocpkg = 1 unless ( -r $_ && -s _ && $_ =~ /\.pkg(\.gz)?$/ );
            print STDERR "DEBUG:$_ : $notalocpkg\n" if $debug;
        }
        unless ($notalocpkg) {
            print "Installing your local packages ...\n";
            inst_loc_pkgs(@ARGV);
        }
    }

    init( $opt_config, @opt_param ) if ( $opt_config || scalar(@opt_param) );
    list_pkgs()   if $opt_list;
    pkgutilver(1) if $opt_syscheck;
    locate_wget();
    locate_gzip();

    if ($opt_workdir) {    # Set/create optional working dir
        $workdir = $opt_workdir;
    }
    elsif ($>) {           # If -W not used and non-root, use home dir
        $workdir = $ENV{HOME} . "/.pkgutil";
        if ( !-d $workdir ) {    # Create if non-existing
            my $status = mkdir $workdir, 0777;
            myexit( "Could not create $workdir", "$!", 1 ) unless $status;
        }
        print STDERR "DEBUG:username: " . getpwuid($>) . "\n" if $debug;
        print STDERR "You're not root and didn't set -W, using home dir.\n";
    }
    print STDERR "DEBUG:workdir: $workdir\n" if $debug;

    if ($opt_pkgdir) {           # Set/create optional package dir
        $pkgdir = $opt_pkgdir;
    }
    else {
        $pkgdir = "$workdir/packages";
    }
    print STDERR "DEBUG:pkgdir: $pkgdir\n" if $debug;

    # Create runtime copy of admin to make sure we have one
    # available even during upgrade of pkgutil itself
    $admin = "$workdir/admin";
    $admin = "/var/opt/ozsw/pkgutil/admin" unless ( -r $admin );
    my $ADMSRC = gensym();
    open( $ADMSRC, "<$admin" ) or myexit( "Can't open $admin", "$!", 1 );
    if ( -r "$workdir/admin.run" ) {
        my @pslist      = `/bin/ps -ef`;
        my $pkgutilinst = 0;
        foreach (@pslist) {
            $pkgutilinst++ if (/\bperl\s.*pkgutil\b/);
        }
        if ( $pkgutilinst > 1 ) {
            myexit( "Another instance of pkgutil is already running!", "", 1 );
        }
        else {
            unlink "$workdir/admin.run" if ( -r "$workdir/admin.run" );
            print STDERR "Stale lock file ($workdir/admin.run) removed\n";
        }
    }
    my $ADMCPY = gensym();
    open( $ADMCPY, ">$workdir/admin.run" )
      or myexit( "Can't open $workdir/admin.run", "$!", 1 );
    while (<$ADMSRC>) {
        print $ADMCPY $_;
    }
    close $ADMSRC;
    close $ADMCPY;
    $admin = "$workdir/admin.run";

    # Read state file if available
    my $statefile = "$workdir/pkgutil.state";
    if ( -r $statefile ) {    # If state file found, parse it
        my $STATE = gensym();
        open( $STATE, "<$statefile" )
          or myexit( "Can't open $statefile", "$!", 1 );
        while (<$STATE>) {
            chomp;            # Remove newline
            my ( $var, $value ) = split( /=/, $_, 2 );
            $state{$var} = $value;
            print STDERR "DEBUG:State:Found $var = $value\n" if $debug;
        }
        close $STATE;
    }
    else {
        print "DEBUG: no state file found\n" if $debug;
    }

    # Cleanup supports yes/auto so it needs to have $admin set
    cleanup() if $opt_cleanup;

    my $status = system( "/bin/mkdir", "-p", "$pkgdir" );
    myexit( "Could not create $pkgdir", "$!", 1 ) if $status;

    # Get mirrors together
    @mirror = @{ $config{mirror} };
    $mirror[0] = $defaultmirror unless @mirror;    # If no mirrors configured
          # Prepend temp mirrors so they are selected first
    unshift( @mirror, @opt_temp ) if @opt_temp;

    my @supp_arch = qw(sparc i386);
    my @supp_rel  = qw(5.8 5.9 5.10 5.11);
    my ( $ok_arch, $ok_rel ) = ( 0, 0 );
    my ( $loc_arch, $loc_rel );

    if ( $opt_target && $opt_download ) {
        if ( $opt_target =~ /.:./ ) {
            ( $loc_arch, $loc_rel ) = split( /:/, $opt_target );
        }
        else {
            myexit( "$opt_target is not a correct arch:rel combo", "", 1 );
        }
    }
    else {
        chomp( $loc_arch = `uname -p` );    # Arch type sparc|i386
        chomp( $loc_rel  = `uname -r` );    # OS version, e.g. 5.10
    }
    print STDERR "DEBUG:arch: $loc_arch\nDEBUG:os rel: $loc_rel\n" if $debug;

    foreach (@supp_arch) {
        if ( $loc_arch eq $_ ) {
            $ok_arch = 1;
            last;
        }
    }
    foreach (@supp_rel) {
        if ( $loc_rel eq $_ ) {
            $ok_rel = 1;
            last;
        }
    }
    print "Architecture $loc_arch not supported.\n" unless $ok_arch;
    print "OS release $loc_rel not supported.\n"    unless $ok_rel;
    myexit( "", "", 1 ) if ( ( $ok_arch + $ok_rel ) < 2 );

    foreach ( my $i = 0 ; $i < scalar(@mirror) ; $i++ ) {
        ( $mirror[$i] ) = ( $mirror[$i] =~ q!^(.+\w)! );
        $mirror[$i] .= "/" . $loc_arch . "/" . $loc_rel;
    }

    print STDERR "DEBUG:primary mirror: $mirror[0]\n" if $debug;
    if ( $opt_upd_cat || @opt_temp ) {
        check_catalog( 1, $opt_parse );    # Reread everything
    }
    else {
        check_catalog( 0, $opt_parse );
    }

    # Running the hook now should pass only things that remain in ARGV,
    # which should be a list of package names
    my @norm_argv;
    foreach (@ARGV) {
        push( @norm_argv, parse_catalog( $_, 2 ) );
    }
    print STDERR "DEBUG:postargproc: " . join( ' ', @norm_argv ) . "\n"
      if $debug;
    run_hooks( "postargproc", @norm_argv );

    # Complex options
    catalog_info() if $opt_catinfo;
    list_file()    if $opt_listfile;

    # Download and stream if both -d and -s selected
    stream( $loc_arch, $loc_rel, $opt_output )
      if ( $opt_stream && $opt_download );
    email($opt_email) if $opt_email;
    available( 0, $opt_parse ) if $opt_avail;
    available( 1, $opt_parse ) if $opt_compare_available;
    deptree($opt_deptree) if $opt_deptree;
    describe($opt_parse)  if $opt_describe;
    compare( 3, $opt_parse ) if $opt_compare_diffonly;
    compare( 4, $opt_parse ) if ( $opt_compare && $opt_single );
    compare( 0, $opt_parse ) if $opt_compare;

    # Download and extract if both -d and --extract selected
    install( 5, $opt_nodeps, $opt_parse ) if ( $opt_extract && $opt_download );
    install( 0, $opt_nodeps, $opt_parse ) if $opt_install;
    install( 2, $opt_nodeps, $opt_parse )
      if $opt_remove;    # Remove instead of install
    install( 1, $opt_nodeps, $opt_parse ) if $opt_download;    # Download only

    # Upgrade is a special case of install
    if ($opt_upgrade) {
        if ( scalar(@ARGV) == 0 || $ARGV[0] =~ /all/i ) {

            # Need to manipulate ARGV to match installed OZSW packages
            # that need upgrading (use -c mode 1)
            print "Looking for packages that can be upgraded ...\n";
            my @cswpkgs = compare( 1, 0 );
            my $i = 0;
            foreach (@cswpkgs) {
                $ARGV[ $i++ ] = $_;
            }
        }
        install( 0, $opt_nodeps, $opt_parse );
    }

    # Arguments but no options
    print "You need to select a valid option!\n" if ( !$opt_upd_cat );
}

# Show version info (-v/V)
#   mode - 0 = print only version, 1 = system check
sub pkgutilver {
    my ($mode) = @_;
    my $flag = 0;
    chomp( my $solver  = `uname -r` );
    chomp( my $solarch = `uname -p` );
    my ( $pkgpatch, $pkgpatchinst );

    if ($mode) {
        print "- System -\n";
        print "Pkgutil\t\t$pkgutilver\n";
        print "Arch\t\t$solarch\n";
        print "Solaris\t\t$solver\n";
        $pkgpatch = ( $solarch eq "sparc" ) ? "110934" : "110935"
          if ( "$solver" eq "5.8" );
        $pkgpatch = ( $solarch eq "sparc" ) ? "113713" : "114568"
          if ( "$solver" eq "5.9" );
        $pkgpatch = ( $solarch eq "sparc" ) ? "119317" : "119318"
          if ( "$solver" eq "5.10" );
        if ( $solver ne "5.11" ) {
            chomp( $pkgpatchinst =
`showrev -p | grep $pkgpatch | cut -d' ' -f2 | grep $pkgpatch | sort | tail -1`
            );
            $pkgpatchinst =
              ($pkgpatchinst) ? "$pkgpatchinst installed" : "not found";
        }
        else {
            $pkgpatch = $pkgpatchinst = "na";
        }
        print "Pkg patch\t$pkgpatch ($pkgpatchinst)\n";
        if ( -x "/opt/ozsw/bin/gpg" ) {
            print "GPG binary\t/opt/ozsw/bin/gpg\n";
        }
        else {
            print "GPG binary\tnot found (suggestion: install OZSWgnupg)\n";
        }
        my $status = locate_gzip();
        print "Gzip binary\t"
          . ( ($status) ? "not found (suggestion: install OZSWgzip)" : $gzip )
          . "\n";
        if ( -x "/bin/mailx" ) {
            print "Mailx binary\t/bin/mailx\n";
        }
        else {
            print "Mailx binary\tnot found (option --email not available)\n";
        }
        if ( -x "/opt/ozsw/bin/gmd5sum" ) {
            print "MD5 binary\t/opt/ozsw/bin/gmd5sum\n";
        }
        else {
            print "MD5 binary\tnot found";
            print " (suggestion: install OZSWcoreutils)" unless $flag;
            print "\n";
            $flag = 0;
        }
        eval { require Digest::MD5; };
        if ($@) {
            print "MD5 module\tnot found\n";
        }
        else {
            print "MD5 module\t", Digest::MD5->VERSION,
              " (primary choice for MD5)\n";
            $flag = 1;
        }
        print "Perl\t\t$]\n";
        print "Perl binary\t" . `which perl`;
        $status = locate_wget();
        print "Wget binary\t"
          . ( ($status) ? "not found (suggestion: install OZSWwget)" : $wget )
          . "\n";
        print "PATH\t\t$ENV{PATH}\n";
        print "\n- Configuration -\n";
        print
          "catalog_not_cached\t$config{catalog_not_cached} (default: true)\n";
        print "catalog_update\t\t"
          . (
            ( $config{catalog_update} ) ? $config{catalog_update} : "not set" )
          . " (default: 14)\n";
        print "deptree_filter_common\t"
          . ( ( $config{deptree_filter_common} eq "true" ) ? "true" : "false" )
          . " (default: false)\n";
        print "exclude_pattern\t\t"
          . (
            ( $config{exclude_pattern} )
            ? $config{exclude_pattern}
            : "not set"
          ) . " (default: none)\n";
        print "gpg_homedir\t\t"
          . ( ( $config{gpg_homedir} ) ? $config{gpg_homedir} : "not set" )
          . " (default: none)\n";
        print "maxpkglist\t\t$maxpkglist (default: 10000)\n";
        print "mirror\t\t\t"
          . (
            ( scalar( @{ $config{mirror} } ) )
            ? join( "\n\t\t\t", @{ $config{mirror} } )
            : "not set"
          ) . "\n\t\t\t(default: $defaultmirror)\n";
        print "nonozsw\t\t\t"
          . ( ($nonozsw) ? $nonozsw : "false" )
          . " (default: false)\n";
        print "pkgaddopts\t\t"
          . ( ( $config{pkgaddopts} ) ? $config{pkgaddopts} : "not set" )
          . " (default: none)\n";
        print "pkgliststyle\t\t$pkgliststyle (default: 0)\n";
        print "pkgrmopts\t\t"
          . ( ( $config{pkgrmopts} ) ? $config{pkgrmopts} : "not set" )
          . " (default: none)\n";
        print "root_path\t\t"
          . ( ( $config{root_path} ) ? $config{root_path} : "not set" )
          . " (default: /)\n";
        print "show_current\t\t$config{show_current} (default: true)\n";
        print "stop_on_hook_soft_error\t"
          . (
            ( $config{stop_on_hook_soft_error} )
            ? $config{stop_on_hook_soft_error}
            : "not set"
          ) . " (default: false)\n";
        print "use_gpg\t\t\t"
          . ( ($use_gpg) ? "true" : "false" )
          . " (default: false)\n";
        print "use_md5\t\t\t"
          . ( ($use_md5) ? "true" : "false" )
          . " (default: false)\n";
        print "wgetopts\t\t"
          . ( ( $config{wgetopts} ) ? $config{wgetopts} : "not set" )
          . " (default: none)\n";
    }
    else {
        print "$pkgutilver\n";
    }
    myexit( "", "", 0 );
}

# Read catalog into hash
#   catalog - catalog to read
#   filename - filename to use
sub read_catalog {
    my ( $catalog, $filename ) = @_;
    my @llst;
    my $i        = 0;
    my @keywords = qw(CREATIONDATE RELEASE);

    # Add catalog info to hash
    my $url = mangle_url($catalog);
    $catinfo{$url}{url}      = $catalog;
    $catinfo{$url}{filename} = $filename;

    # Initialize
    foreach (@keywords) {
        $catinfo{$url}{$_} = "-";
    }

    my $CATALOG = gensym();
    open( $CATALOG, "<$filename" ) or myexit( "Can't open $filename", "$!", 1 );
    while ( my $line = <$CATALOG> ) {
        chomp $line;

        # Look for magic comments
        if ( $line =~ /^#\s\w/ ) {
            foreach (@keywords) {
                if ( $line =~ /^#\s.+\s.+$/ ) {
                    my ( $key, $value ) = ( $line =~ /^#\s(.+)\s(.+)$/ );
                    if ( $key eq $_ ) {
                        $catinfo{$url}{$_} = $value;
                    }
                }
            }
        }
        if ( $line =~ /^\w/ ) {
            @llst = split( / /, $line );
            if (   $llst[0]
                && $llst[1]
                && $llst[2]
                && $llst[3]
                && $llst[4]
                && $llst[5]
                && $llst[6]
                && $llst[7] )
            {
                $pkg{ $llst[2] }{ $llst[1] } = $line;
                $commonpkg{ $llst[0] } = $llst[2];
                push( @{ $pkgmirror{ $llst[3] } }, $catalog );
                $i++;
            }
        }
    }

    # Compare state file and catalog releases
    print "DEBUG:Catalog release:$catinfo{ $url }{ RELEASE }\n" if $debug;
    $catinfo{$url}{RELEASE} = "-"
      unless defined $catinfo{$url}{RELEASE};
    $state{RELEASE} = "-" unless defined $state{RELEASE};

    if ( "$catinfo{ $url }{ RELEASE }" ne "$state{ RELEASE }" ) {
        unless ( $yes || "$catinfo{ $url }{ RELEASE }" eq "-" ) {
            print "The release level in the previous state file ("
              . $state{RELEASE}
              . ")\nand the current catalog ("
              . $catinfo{$url}{RELEASE}
              . ") differs.\nBe aware that many "
              . "packages may have changed.\nDo you want to continue? ([y],n) ";
            chomp( my $prompt = <STDIN> );
            if ( $prompt =~ /^[nN]/ ) {
                myexit( "", "", 0 );
            }
        }

        # Write the state file with the new release
        my $STATE = gensym();
        open( $STATE, ">$workdir/pkgutil.state" )
          or myexit( "Can't open $workdir/pkgutil.state", "$!", 1 );
        print $STATE "RELEASE=$catinfo{ $url }{ RELEASE }";
        close $STATE;
    }

    close $CATALOG;
    $catinfo{$url}{num_of_pkgs} = $i;
    return $i;
}

# Remove packages (-r)
#   pkgs - packages to remove
sub remove {
    my @pkgs      = @_;
    my $retval    = "";
    my $pkgrm     = "pkgrm";
    my $pkgrmopts = "";
    my $pkgforce  = "";
    my $pkgtrace  = "";
    my ( $i, $depinuse, $skip );
    my ( @cswpkgs, @deptmp, @rempkgs, @cswpkgstmp );

    $pkgtrace = "-v" if $trace;
    my $root_path = $config{root_path} ? "-R $config{root_path}" : "";
    $pkgrmopts .= " $config{pkgrmopts}" if $config{pkgrmopts};

    print "Looking at installed packages ...\n";
    if ($nonozsw) {
        @cswpkgstmp = `pkginfo $root_path`;
    }
    else {
        @cswpkgstmp = `pkginfo $root_path | grep OZSW`;
    }
    foreach my $pkg (@cswpkgstmp) {
        ($pkg) = ( $pkg =~ /\s+?(\S+?)\s/ );
        $skip = 0;
        foreach (@pkgs) {
            $skip = 1 if ( $_ eq $pkg );
        }
        push( @cswpkgs, $pkg ) unless $skip;
    }
    print "Examining dependencies for:\n";
    foreach my $userpkg (@pkgs) {
        ($i) = ( $userpkg =~ /(\S+)-/ );
        print "  $i";
        $depinuse = 0;
        foreach my $j (@cswpkgs) {
            $retval = parse_catalog( $j, 6 );
            if ( $retval ne "none" && $retval ne "not in catalog" && $i ne $j )
            {
                (@deptmp) = split( /\|/, $retval );
                foreach my $k (@deptmp) {
                    if ( $k eq $i ) {
                        print STDERR "DEBUG:\n$i is in use by $j\n" if $debug;
                        $depinuse = 1;
                        last;
                    }
                }
            }
            $depinuse ? last : next;
        }
        push( @rempkgs, $userpkg ) unless $depinuse;
        $depinuse ? print " (in use)\n" : print " (remove)\n";
    }

    if ( scalar(@rempkgs) > 0 && !$yes ) {
        print scalar(@rempkgs),
            " package"
          . ( scalar(@rempkgs) > 1 ? "s" : "" )
          . " to remove. Do you want to continue? ([y],n,auto) ";
        chomp( my $prompt = <STDIN> );
        if ( $prompt =~ /^[nN]/ ) {
            myexit( "", "", 0 );
        }
        elsif ( $prompt =~ /^a(uto)*/i ) {
            print "Turning on automatic mode as if --yes was passed.\n";
            $yes = 1;
        }
    }
    $pkgforce = "-a $admin -n" if $yes;

    if ( scalar(@rempkgs) > 0 ) {
        run_hooks( 'prebatchremove', @rempkgs );

        foreach (@rempkgs) {
            my ( $p, $v ) = ( $_ =~ /(\S+)-/ );
            $retval = check_pkg( $p, 0 );
            if ( $retval ne "notinst" ) {
                myexit( "Option -n selected, stopping here.", "", 0 ) if $nomod;
                print "\n=> Removing $p\n";
                run_hooks( 'preremove', ($_) );
                system(
                    "$pkgrm",
                    split( ' ', $root_path ),
                    split( ' ', $pkgforce ),
                    split( ' ', $pkgtrace ),
                    split( ' ', $pkgrmopts ),
                    "$p"
                );
                run_hooks( 'postremove', ($_) );
            }
            else {
                print "$p not installed\n";
            }
        }

        run_hooks( 'postbatchremove', @rempkgs );
    }
    else {
        print "Nothing to remove.\n";
    }
    myexit( "", "", 0 );
}

# Remove list of packages with pkgrm
#   pkgs - packages to remove
sub rem_pkgs {
    my @pkgs      = @_;
    my $pkgrm     = "pkgrm";
    my $pkgrmopts = "";
    my $pkgforce  = "";
    my $pkgtrace  = "";
    $pkgrmopts .= " $config{pkgrmopts}" if $config{pkgrmopts};

    if ( scalar(@pkgs) > 0 && !$yes ) {
        print scalar(@pkgs),
            " package"
          . ( scalar(@pkgs) > 1 ? "s" : "" )
          . " to remove. Do you want to continue? ([y],n,auto) ";
        chomp( my $prompt = <STDIN> );
        if ( $prompt =~ /^[nN]/ ) {
            myexit( "", "", 0 );
        }
        elsif ( $prompt =~ /^a(uto)*/i ) {
            print "Turning on automatic mode as if --yes was passed.\n";
            $yes = 1;
        }
    }

    $pkgforce = "-a $admin -n" if $yes;
    $pkgtrace = "-v"           if $trace;
    my $root_path = $config{root_path} ? "-R $config{root_path}" : "";

    if ( scalar(@pkgs) > 0 ) {
        run_hooks( 'prebatchremove', @pkgs );

        foreach (@pkgs) {
            myexit( "Option -n selected, stopping here.", "", 0 ) if $nomod;
            print "\n=> Removing $_\n";
            my $pkgver = check_pkg( $_, 0 );
            run_hooks( 'preremove', ("$_-$pkgver") );
            system(
                "$pkgrm",
                split( ' ', $root_path ),
                split( ' ', $pkgforce ),
                split( ' ', $pkgtrace ),
                split( ' ', $pkgrmopts ), "$_"
            );
            run_hooks( 'postremove', ("$_-$pkgver") );
        }

        run_hooks( 'postbatchremove', @pkgs );
    }
    else {
        print "Nothing to remove.\n";
    }
}

# Run hooks
sub run_hooks {
    my $hook_name = shift;
    my @hook_args = @_;

    my $hook_dir = "/etc/opt/ozsw/pkg-hooks/$hook_name.d";
    my ( $hook, $hookfile, $oldwd, $status, $code );
    my @hooks_to_run;
    my @valid_hooks = (
        'prebatchinstall',  'prebatchupgrade',
        'prebatchremove',   'preinstall',
        'preupgrade',       'preremove',
        'postbatchinstall', 'postbatchupgrade',
        'postbatchremove',  'postinstall',
        'postupgrade',      'postremove',
        'prefetch',         'postfetch',
        'preargproc',       'postargproc'
    );

    myexit( "Invalid hook: $hook_name", "", 1 )
      unless ( grep { /$hook_name/ } @valid_hooks );

    if ( !-d $hook_dir ) {
        print STDERR "DEBUG:No $hook_name hook scripts\n" if $debug;
    }
    else {
        $oldwd = getcwd();
        chdir($hook_dir) or myexit( "Could not cd to $hook_dir", "", 1 );

        @hooks_to_run = glob("*");

        print STDERR "DEBUG:Executing $hook_name hook scripts\n"
          if ( scalar(@hooks_to_run) && $debug );

        foreach my $hookfile ( sort { $a cmp $b } (@hooks_to_run) ) {
            $hook = "$hook_dir/$hookfile";
            if ( -x $hook ) {
                print STDERR "DEBUG:Running $hook\n" if $debug;

                # @hook_args is (in most cases) a list of
                # CSWfoo-1.2.3,REV=YYYY.mm.dd affected packages, but in other
                # cases, it might be single or multiple URL's of packages
                # being pulled down, etc
                system( "$hook", @hook_args );
                $status = $?;
                $code   = $status >> 8;

                print STDERR
                  "DEBUG:Script $hook_name.d/$hookfile returned code: $code\n"
                  if $debug;

                # This allows a hook to stop execution of any further
                # package actions
                if ( $code == 1 ) {
                    myexit( "Hook $hook_name.d/$hookfile returned with code 1",
                        "dying", 1 );
                }
                elsif ($code == 2
                    && $config{'stop_on_hook_soft_error'} eq 'true' )
                {
                    myexit(
                        "Hook $hook_name.d/$hookfile returned with code 2",
"respecting configuration option stop_on_hook_soft_error",
                        1
                    );
                }
                elsif ( $code > 2 ) {
                    print STDERR
"Hook $hook_name.d/$hookfile returned undefined code $code.\nContinuing anyway ...\n";
                }
            }
        }

        chdir($oldwd);
    }
}

# Signal handler
#   signame - signal caught
sub signal_handler {
    my $signame = shift;

    myexit( "\npkgutil caught a SIG$signame. Exiting.", "", 1 );
}

# Build package streams (-s)
#   arch   - architecture
#   rel    - os release
#   output - file name for package stream
sub stream {
    my ( $arch, $rel, $output ) = @_;
    my ( $FH, $mode, $transfile );

    if ($output) {
        $FH   = ( $output eq "-" ? *STDERR : *STDOUT );
        $mode = ( $output eq "-" ? 4       : 3 );
    }
    else {
        $FH   = *STDOUT;
        $mode = 3;
    }

    if ($output) {
        if ( $output eq "-" ) {
            $transfile = "/dev/fd/1";
        }
        else {
            if ( $output =~ /\// ) {
                if ( $output =~ /^\// ) {
                    $transfile = $output;    # Output given is an absolute path
                }
                else {
                    myexit( "Relative output paths are not allowed", "", 1 );
                }
            }
            else {
                $transfile = "$pkgdir/$output";
            }
        }
    }
    else {
        $output    = "$ARGV[0].$arch.$rel.pkg";
        $transfile = "$pkgdir/$output";
    }
    print STDERR "DEBUG:output: $output\ntransfile: $transfile\n" if $debug;

    my @install_order = install( $mode, 0, 0 );    # Download packages needed
    myexit( "", "", 0 )
      if ( scalar(@install_order) == 0 );          # Empty list, nothing to do
    print STDERR "DEBUG:install order: " . join( ' ', @install_order ) . "\n"
      if $debug;

    foreach (@install_order) {                     # Delete package directories
        my $status;
        $status = system( "/bin/rm", "-rf", "$pkgdir/$_" )
          if ( -d "$pkgdir/$_" );
        myexit( "Could not delete $pkgdir/$_", "$!", 1 ) if $status;
    }

    foreach (@install_order) {                     # pkgtrans them one by one
        my $file = parse_catalog( $_, 3 );
        print $FH "Transforming $_ ...\n";
        my $status = system(
"$gzip -c -f -d $pkgdir/$file | /bin/pkgtrans /dev/fd/0 $pkgdir all 2> /dev/null"
        );
        myexit( "Could not transform $file", "$!", 1 ) if $status;
    }

    # pkgtrans all of them into one package stream
    print $FH "Transforming packages into stream ($transfile) ...\n";
    my $status;
    $status = system( "/bin/touch", "$transfile" ) unless ( $output eq "-" );
    myexit( "Could not create stream file", "$!", 1 ) if $status;
    my $transpkgs = join( " ", @install_order );
    $status =
      system("/bin/pkgtrans -s $pkgdir $transfile $transpkgs 2> /dev/null");
    myexit( "Could not transform packages into stream", "$!", 1 ) if $status;

    foreach (@install_order) {    # Delete package directories
        my $status = system( "/bin/rm", "-rf", "$pkgdir/$_" );
        myexit( "Could not delete $pkgdir/$_", "$!", 1 ) if $status;
    }

    # Print cmd needed to install
    print $FH "\nInstall commands in dependency safe order:\n\n";
    foreach (@install_order) {
        print $FH "pkgadd -d $output $_\n";
    }

    myexit( "", "", 0 );
}

# Show usage info
sub usage {
    print <<EOF;
pkgutil $pkgutilver, install Solaris packages the easy way.

Usage: pkgutil [option]... [package](-[version])...

  -i, --install         Install package
  -u, --upgrade         Upgrade package
  -r, --remove          Remove package (experimental)
  -d, --download        Download only
  -U, --catalog         Update catalog
  -a, --available       Show available packages
      --describe        Describe available packages
  -c, --compare         Compare installed packages to current
  -C, --compare-diff    Same as -c but only show different versions
  -A, --compare-avail   Compare available packages to those installed
  -e, --email=address   Send e-mail with available updates
  -t, --temp=site       Temporarily use this site as primary for download
  -x, --exclude=pattern Pattern to exclude
  -W, --workdir=path    Path to use for work directory
  -P, --pkgdir=path     Path to use for package downloads
  -R, --rootpath=path   Path to use for root_path
      --config=file     Use this configuration file
  -y, --yes             Answer yes on all prompts
  -f, --force           Force updates (sync with mirror)
  -n, --nomod           No modifications are made to the system
  -N, --nodeps          No dependencies
  -D, --debug           Debug mode
      --trace           Set trace mode (-v) for pkgadd/pkgrm
  -h, --help            Show this help
  -v, --version         Show version
  -V, --syscheck        System check
  -l, --list            List installed packages
  -L, --listfile        List files in package
  -F, --findfile        Find files in package
      --deptree=depth   Display dependency tree
      --extract         Extract package content (use with -d)
  -s, --stream          Build a package stream (use with -d)
  -o, --output=file     File name for package stream (use with -s)
  -T, --target=arch:rel Specify architecture and OS release for download
      --single          Single package check (use with -c)
  -p, --param=opt:val   Override configuration option
      --parse           Machine parsable output
      --cleanup         Clean up obsolete packages
      --catinfo         Catalog info

Example: pkgutil -i OZSWwget\ (install wget and its dependencies)

Written and maintained by Peter Bonivart. Web site: http://pkgutil.net.
EOF
    myexit( "", "", 0 );
}

# Comparison of two package versions as per
# http://pkgutil.net/get-install-and-configure#toc8
# As per cmp or <=>, -1, 0, or 1 if p1 is less than, equal to or greater than p2
# Note that if neither has a REV code, -1 is *ALWAYS* returned.
#   p1rev, p2rev - versions to compare
sub verscmp {
    my ( $p1rev, $p2rev ) = @_;    # crev (new), irev (old), then 1 => upgrade
    my ( @p1list, @p2list );
    my $p1tmp  = "";
    my $p2tmp  = "";
    my $update = 0;

    my $skip = 0;

    # 1st case: same strings => no upgrade
    $skip = 1 if ( $p1rev eq $p2rev );

    # 2nd case: installed no REV, catalog REV => upgrade
    if ( !$skip ) {
        if ( $p1rev !~ /REV=/ && $p2rev =~ /REV=/ ) {
            $update = -1;
            $skip   = 1;
        }
        elsif ( $p2rev !~ /REV=/ && $p1rev =~ /REV=/ ) {

            # Reverse case (installed REV, catalog no REV => "downgrade")
            $update = 1;
            $skip   = 1;
        }
    }

    # 3rd case: installed REV, catalog REV => if newer, upgrade
    if ( !$skip ) {
        if ( $p1rev =~ /REV=/ && $p2rev =~ /REV=/ ) {
            if ( $p1rev =~ /rev=/ ) {
                ($p1tmp) = ( $p1rev =~ /REV=(.+)_/ );
            }
            else {
                ($p1tmp) = ( $p1rev =~ /REV=(.+)$/ );
            }
            @p1list = split( /\./, $p1tmp );

            if ( $p2rev =~ /rev=/ ) {
                ($p2tmp) = ( $p2rev =~ /REV=(.+)_/ );
            }
            else {
                ($p2tmp) = ( $p2rev =~ /REV=(.+)$/ );
            }
            @p2list = split( /\./, $p2tmp );

            for ( my $i = 0 ; $i < scalar(@p1list) ; $i++ ) {
                $p1list[$i] =~ s/[^0-9]//g;    # Only digits
                $p2list[$i] =~ s/[^0-9]//g;
                if ( $i >= scalar(@p2list) ) {    # p1list is longer than p2list
                    $update = 1;
                    last;
                }
                if ( $p1list[$i] != $p2list[$i] ) {
                    if ( $p1list[$i] > $p2list[$i] ) {
                        $update = 1;
                    }
                    else {
                        $update = -1;
                    }
                    print STDERR "DEBUG:$i $p1list[$i] $p2list[$i] $update\n"
                      if $debug;
                    last;
                }
                print STDERR "DEBUG:$i $p1list[$i] $p2list[$i] $update\n"
                  if $debug;
            }
            $update = -1 if ( !$update && scalar(@p2list) > scalar(@p1list) );

            $skip = 1;
        }
    }

    # 4th case: installed no REV, catalog no REV => upgrade
    if ( !$skip ) {
        $update = 1 if ( $p1rev !~ /REV=/ && $p2rev !~ /REV=/ );
    }

    return $update;
}

# Main

init();
parse_cl();

myexit( "", "", 0 );

# POD start

=head1 NAME

pkgutil - install Solaris packages the easy way

=head1 SYNOPSIS

pkgutil [option]... [package]...

=head1 DESCRIPTION

Pkgutil, written in Perl and licensed under GPL, is a tool to make installation of packages in Solaris easier.

It handles package dependencies so all required packages are installed before the desired package automatically.

A catalog is used to make this possible. The format is:

S<common_name version package_name file_name md5_hash pkg_size dependencies category>

Example:

S<bind 9.4.2,REV=2008.07.09_rev=p1 CSWbind bind-9.4.2,REV=2008.07.09_rev=p1-SunOS5.8-sparc-CSW.pkg.gz f68df57fcf54bfd37304b79d6f7eeacc 2954112 OZSWcommon|CSWosslrt net>

In the example CSWbind has two dependencies separated by a pipe. Multiple categories can also be separated by pipe characters.

=head1 OPTIONS

=over 5

=item B<-i, --install>

Install package. Will install the specified packages with all their dependencies.  You may specify an explicit version (e.g. amarok-1.4.8,REV=2008.02.26) if desired, otherwise the latest version found is chosen.

=item B<-u, --upgrade>

Upgrade package. A special argument is "all" which will upgrade all installed packages if possible, no argument is the same as "all". If one or more packages are given as arguments only those will be upgraded.

=item B<-r, --remove>

Remove package. This will remove a package including all dependencies that are not in use by other packages.

Note that this feature is experimental. Use with caution.

=item B<-d, --download>

Download only. Same as install but stops after downloading the packages.

=item B<-U, --catalog>

Update catalog.

=item B<-a, --available>

Show available packages.

=item B<--describe>

Describe available packages.

=item B<-c, --compare>

Compare installed packages to current.

=item B<-C, --compare-diff>

Compare installed packages to current, show only different versions.

=item B<-A, --compare-avail>

Compare available packages to those installed.

=item B<-e, --email=address>

Send e-mail with available updates. E-mail address as argument. Don't use with other options.

=item B<-t, --temp=site>

Temporarily use this site for download. Must be the complete url where to find the catalog and associated packages. May be specified multiple times.

=item B<-s, --stream>

Build a package stream for a certain architecture and OS release. All dependencies will be included in the stream and the needed command to install them in the correct order will be displayed. Must be used with -d.

=item B<-T, --target=arch:rel>

Specify architecture and OS release when downloading, e.g. i386:5.9 or sparc:5.10. If not specified defaults to architecture and release of the host running pkgutil.

=item B<-o, --output=file>

File name for package stream. Used with -s. A special argument is "-" which sends the stream to standard output, see example below. The default name is the first package argument followed by architecture and OS release, for example "bind.i386.5.10.pkg".

=item B<-x, --exclude=pattern>

Simple pattern that will exclude matching packages. This option can be specified multiple times.

=item B<-W, --workdir=path>

Path to use for work directory. This can be used to run pkgutil as non-root, e.g. called from a web server.

=item B<-P, --pkgdir=path>

Path to use for package downloads. This can be used to run pkgutil as non-root, e.g. called from a web server. Default is <workdir>/package.

=item B<-R, --rootpath=path>

Path to use for root_path. This is used by Solaris pkg tools like pkgadd, pkgrm and so on. Setting this option overrides root_path in pkgutil.conf. Default is not set.

=item B<--config=file>

Use this configuration file instead of the default ones.

=item B<-y, --yes>

Answer yes on all prompts. Skips prompt when multiple packares are to be downloaded. Makes pkgrm/pkgadd operations more silent and without prompts. Only makes sense with -i and -u.

=item B<-f, --force>

Force updates regardless of version compare result, as long as local and mirror version is not the same an update is carried out. This can be used as a rollback from the testing mirror to current or from current to stable for example.

=item B<-n, --nomod>

No modifications are made to the system. Stops before downloading catalog/packages. Use it to simulate if catalog needs updating or which packages will be downloaded. Only makes sense with -i, -u and -U.

=item B<-N, --nodeps>

No dependencies, only what you specify on the command line will be used. Useful when using pkgutil as a download tool of specific packages or if you use the testing repo that might pull in unwanted packages.

=item B<-D, --debug>

Debug mode. Adds a lot of info about what's going on. Only use if you're having problems you think is a bug.

=item B<--trace>

Set trace mode (-v) for pkgadd/pkgrm.

=item B<-h, --help>

Show this help.

=item B<-v, --version>

Show pkgutil version.

=item B<-V, --syscheck>

System check. This shows info about binaries and more that pkgutil needs to support all features.

=item B<-l, --list>

List installed packages.

=item B<-L, --listfile>

List files in packages. Works on both installed packages as well as on (uncompressed) package files. You can even mix them on the command line.

=item B<-F, --findfile>

Find files in installed packages.

=item B<--deptree=depth>

Display dependency tree. Depth is the number of levels to show, minumum is 1.

=item B<--extract>

Will fetch and extract package content. Must be used with -d.

=item B<--single>

Single package check. This is much faster than checking all installed packages, note that using something like "-c foo" still checks all packages, it just filters the output to only show lines containing foo. Must be used with -c.

=item B<-p, --param=opt:val>

Override configuration option. All configuration options in pkgutil.conf can be overridden except the mirror option (use -t for that). This can be useful if you temporarily want to disable gpg checks for example.

=item B<--parse>

Machine parsable output, no headers and all columns are separated by one tab. Use this together with scripts to get a reliable output format that is simpler to parse.

=item B<--cleanup>

Clean up obsolete packages. For example a renamed package that is not needed any more.

=item B<--catinfo>

Display information about the catalogs used, e.g. number of packages and local path.

=back

=head1 EXAMPLES

=head2 Example 1

C<# pkgutil -i CSWbind>

Will install CSWbind with all required dependencies.

=head2 Example 2

C<# pkgutil -e nobody@foo.bar>

Will run silently and send an e-mail (with mailx) to the given address if there's updated packages available. Run it from crontab once weekly.

=head2 Example 3

C<# pkgutil -y -o - -s bind | gzip E<gt> bind.pkg.gz>

Will send a package stream including all dependencies needed for CSWbind to standard output which is piped to gzip. With "-o -" the normal output is sent to standard error and "-y" is used to skip the confirmation prompt.

=head2 Example 4

C<# pkgutil -T i386:5.10 -s bind>

Will make a package stream including all dependencies needed for CSWbind. The package stream will contain packages for the i386 architecture and Solaris 10 regardless of host used to run pkgutil.

=head1 CONFIGURATION

Pkgutil uses a configuration file, pkgutil.conf, that contains a few important settings. No modifications are mandatory though since pkgutil will happily run with the defaults.

There's a configuration file in /opt/ozsw/etc/pkgutil.conf. The only thing you normally need to change there is the mirror location. If you have a pkgutil.conf file in /etc/opt/ozsw it will override the one in /opt/ozsw/etc on a setting-by-setting basis. If no pkgutil.conf is found in either of those two locations defaults are used.

The settings that can be used are:

C<catalog_not_cached>

Tells your proxy server to disable caching during fetching of the catalog and descriptions. Default is true.

C<catalog_update>

Number of days between automatic catalog updates. Special cases are -1 if you want to disable automatic updates and 0 if you always want to update the catalog. Default is 14.

C<deptree_filter_common>

Filter out OZSWcommon from --deptree output. Useful since almost every package depends on OZSWcommon, setting this option to true reduces the number of lines a lot without affecting the actual information much. Default is false.

C<exclude_pattern>

Space separated list of simple patterns that will exclude matching packages when updating or installing. If you, e.g., never want cups related packages updated accidentally you can add the string cups here, this will exclude all package names that contain that string. Default is blank.

C<gpg_homedir>

The path to the gpg directory (instead of ~/.gnupg or /var/opt/ozsw/pki if OZSWpki is installed) when verifying the catalogs integrity.

C<maxpkglist>

During dependency calculation the recursive algorithm needs to be protected against cyclic dependencies in the catalog (packages that list each other as dependencies) or pkgutil will not stop until memory is exhausted. This sets the maximum number of recursions before pkgutil stops.

C<mirror>

This is the mirror that is used for downloading the catalog and packages. Try setting it to your nearest mirror to improve download speed. This option may be specified multiple times.

It also supports (in an experimental way) the file:// protocol for local/NFS mirrors. Example: C<mirror=file:///absolute/path/to/files>

C<nonozsw>

Enable (default is disable) support for non-OZSW prefixed packages.

Pkgutil also uses a file to configure pkgadd when the yes (-y) option is used. This file is located in /var/opt/ozsw/pkgutil and is called admin. It's by default set up to fully automate installations but if you change something in this file it will be preserved during upgrades of pkgutil. Read more about the admin file with "man -s4 admin".

C<pkgaddopts>

Send options to pkgadd, one example is -G that will only install the packages in the global zone for Solaris 10. A useful setting is -S, that will not display the license during install.

C<pkgliststyle>

Style of package list when installing/upgrading packages. 0 is the one used since the first version of pkgutil, it's space efficient but less readable. 1 is the one introduced in v1.7, it's one package per line which is easier to read. 2 is the same as 1 but also displays which distribution a package comes from, e.g. unstable or stable.

C<pkgrmopts>

Send options to pkgrm, one example is "-O nozones" that will only deinstall the packages in the global zone for Solaris 10. 

C<root_path>

Set alternate root path (-R with pkg commands). Default is /.

C<show_current>

Show the current list of packages or not. You can choose to not show the list of current packages to get less output when doing operations. Default is true.

C<stop_on_hook_soft_error>

This option determines whether or not execution will be halted if a hook returns 2 as its exit code. Execution is always stopped if a hook returns 1. Set this to true to enable it.

C<use_gpg>
C<use_md5>

These two can enable (default is disabled for both) checking the catalogs integrity (GPG) and that each downloaded file is unchanged from the published one (MD5). To use GPG you need OZSWgnupg installed. MD5 is supported by default in Solaris 10 and on Solaris 8 and 9 if you have OZSWperl installed and have /opt/ozsw/bin first in your path. You can also install OZSWcoreutils to get MD5 support.
 
C<wgetopts>

Send options to wget, for example to show more or less information during downloads.

=head1 PKGASK RESPONSE FILES

Pkgutil supports pkgask response files and will use them if you provide them. Create a pkgask directory in your work directory (normally /var/opt/ozsw/pkgutil) and put your response files there.

=head1 SEE ALSO

http://pkgutil.net

http://sourceforge.net/projects/pkgutil

=head1 TODO

Add unit testing.

=head1 AUTHOR

Peter Bonivart <bonivart@opencsw.org>

=head1 CREDITS

Mark Bannister

Joe Baro

Maciej Blizinski

Dennis Clarke

Ihsan Dogan

David Everly

Neil Houston

Don Jackson

Sebastian Kayser

Trygve Laugstol

James Lee

Juraj Lutter

Dagobert Michelsen

Yann Rouillard

Remko de Vrijer

Ben Walton

Derek Whayman

For more details, see the readme file.

=head1 COPYRIGHT AND DISCLAIMER

Copyright (C) 2008-2012 Peter Bonivart. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.

=cut

