#!/usr/bin/env perl -w

# 2002-08-15  markus@gyger.org
# pkgzip: turns Solaris SVR4 pkg into internally (b)zipped pkg

$usage = <<__EOT__;
usage: $0 [-b|-c|-z] <pkgdir> [<pkgdirs>...]
	-b  use bzip2/cpio
	-c  use compress/cpio
	-z  use zip (default)
__EOT__

# -z: requires unzip to install pkg, available on:
#     Solaris 2.5   + patch 105708-02
#     Solaris 2.5.1 + patch 105701-02
#     Solaris 2.6   + patch 10619[34]-02
#     Solaris 7 FCS and up
#
# -b: requires bzcat to install pkg, available on:
#     Solaris 2.6   + patch 11166[45]-01
#     Solaris 7     + patch 11166[67]-01
#     Solaris 8 FCS and up (SUNWbzip on miniroot starting with Solaris 8 7/01)
#
# -c: uses zcat & cpio to install pkg, available on all Solaris versions
#     basically the same as having a reloc.cpio.Z file, minimally faster

# installation class action scripts
$cas{unzip} = <<'__EOT__';
if read pkgdir
then	xargs -e -n 2048 unzip -o${PKG_INIT_INSTALL+q}d "${BASEDIR:-/}" \
		"$pkgdir/archive/`expr \"//$0\" : '.*/i\.\(.*\)'`.zip"
fi >&2 || exit 2
__EOT__

$cas{bzcat} = <<'__EOT__';
if read pkgdir
then	cd "${BASEDIR:-/}" && cat >$PKGSAV/filelist &&
	bzcat "$pkgdir/archive/`expr \"//$0\" : '.*/i\.\(.*\)'`.cpio.bz2" | 
	cpio -idmukv -C 512 -E "$PKGSAV/filelist" && rm -f "$PKGSAV/filelist"
fi >&2 || exit 2
__EOT__

$cas{zcat} = <<'__EOT__';
if read pkgdir
then	cd "${BASEDIR:-/}" && cat >$PKGSAV/filelist &&
	zcat "$pkgdir/archive/`expr \"//$0\" : '.*/i\.\(.*\)'`.cpio.Z" | 
	cpio -idmukv -C 512 -E "$PKGSAV/filelist" && rm -f "$PKGSAV/filelist"
fi >&2 || exit 2
__EOT__

$method = "unzip";
while (@ARGV and $ARGV[0] =~ /^[+-]/ and $_ = shift) {
    print($usage), exit if /^-?-h(elp)?$/;
    last if /^--$/;
    $method = "bzcat", next if /^-b$/;
    $method = "zcat",  next if /^-c$/;
    $method = "unzip", next if /^-z$/;
    die "$0: illegal option -- $_\n$usage";
}
die $usage unless @ARGV;

foreach $pkgdir (@ARGV) {
    # read pkginfo and pkgmap
    unless (open PKGINFO, "<$pkgdir/pkginfo") {
	warn "$0: $pkgdir: not a package\n";
	next
    }
    @pkginfo = <PKGINFO>;
    close PKGINFO or die "close $pkgdir/pkginfo: $!";
    open PKGMAP, "<$pkgdir/pkgmap" or die "open $pkgdir/pkgmap: $!";
    @pkgmap = <PKGMAP>;
    close PKGMAP or die "close $pkgdir/pkgmap: $!";

    # turn root object only pkg into relocatable pkg with BASEDIR /
    unless (grep /^(?:\d+\s+)?[bcdeflpsvx]\s+\S+\s+[^\/]/, @pkgmap) {
	foreach (@pkgmap) {
	    s/^((?:\d+\s+)?d\s+\S+\s+)\/+(\s)/$1.$2/;
	    s/^((?:\d+\s+)?[bcdeflpsvx]\s+\S+\s+)\/+/$1/;
	}
	@pkginfo = grep !/^BASEDIR=/, @pkginfo;
	push @pkginfo, "BASEDIR=/\n";
    }

    # get files and their attributes for each class
    %class = %uid = %gid = %mode = %modtime = ();
    foreach (@pkgmap) {
	# replace parametric $BASEDIR paths with relative ones if possible
	s/\$BASEDIR\/// if /^(?:\d+\s+)?[bcdeflpsvx]\s+\S+\s+\$BASEDIR\//;
	s/\$BASEDIR/./	if /^(?:\d+\s+)?[bcdeflpsvx]\s+\S+\s+\$BASEDIR\s/;
	if (($class, $file, $mode, $uid, $gid, $modtime) =
	    /^(?:\d+\s+)? [efv]\s+ (\S+)\s+ (\S+)\s+ (\S+)\s+
	    (\S+)\s+ (\S+)\s+ \S+\s+ \S+\s+ (\d+)/x) {
	    push @{$class{$class}}, $file;
	    $uid{$file} = getpwnam $uid unless $uid =~ /[\$?]/;
	    $uid{$file} = -1 unless defined $uid{$file};
	    $gid{$file} = getgrnam $gid unless $gid =~ /[\$?]/;
	    $gid{$file} = -1 unless defined $gid{$file};
	    $mode{$file} = (0777 & oct $mode) | 0400 if $mode =~ /^[0-7]+$/;
	    $modtime{$file} = $modtime;
	}
    }

    undef $prevclass;
    foreach $class (sort keys %class) {
	# skip classes using a (system) installation class action script or
	# if already compressed or not relative or using pattern characters
	if ($class =~ /^(CompCpio|awk|build|rbac|sed)$/ ||
	    $class eq "preserve" && $method ne "unzip" ||
	    -f "$pkgdir/install/i.$class" || -f "$pkgdir/reloc.cpio.Z" ||
	    grep /^\/|\$/, @{$class{$class}} || # absolute or parametric path
	    $method ne "unzip" && grep /[?*[]/, @{$class{$class}}) { # pattern
	    delete $class{$class};
	    next
	}

	# write or link installation class action script install/i.<class>
	-d "$pkgdir/install" || mkdir "$pkgdir/install", 0777 or
	    die "mkdir $pkgdir/install: $!";
	if (defined $prevclass && $class ne "preserve") {
	    link "$pkgdir/install/i.$prevclass", "$pkgdir/install/i.$class" or
		die "ln install/i.$prevclass $pkgdir/install/i.$class: $!";
	} else {
	    $prevclass = $class unless $class eq "preserve";
	    open CAS, ">$pkgdir/install/i.$class" or die "open i.$class: $!";
	    $cas = $cas{$method};
	    $cas =~ s/-o/-n/ if $class eq "preserve"; # unzip only
	    print CAS $cas;
	    close CAS or die "close $pkgdir/install/i.$class: $!";
	}

	# in case it was a root object only pkg before
	unless (-d "$pkgdir/reloc") {
	    rename "$pkgdir/root", "$pkgdir/reloc" or
		die "mv $pkgdir/root $pkgdir/reloc: $!";
	}

	# set owner, group, mode, timestamp according to pkgmap
	foreach $file (sort @{$class{$class}}) {
	    chown $uid{$file}, $gid{$file}, "$pkgdir/reloc/$file"; # if root
	    chmod $mode{$file}, "$pkgdir/reloc/$file" if defined $mode{$file};
	    utime $modtime{$file}, $modtime{$file}, "$pkgdir/reloc/$file" or
		warn "utime $pkgdir/reloc/$file: $!";
	}

	# create zip/bzip2/compress/cpio archive and rm the files in reloc
	-d "$pkgdir/archive" || mkdir "$pkgdir/archive", 0777 or
	    die "mkdir $pkgdir/archive: $!";
	if ($method eq "unzip") {
	    open ZIP, "| cd '$pkgdir/reloc' && " .
		"zip -9qmT@ '../archive/$class.zip'" or die "can't fork: $!";
	    print ZIP join "\n", @{$class{$class}}, "";
	    close ZIP or die "zip error: $! $?";
	} elsif ($method eq "bzcat") {
	    open BZIP2, "| cd '$pkgdir/reloc' && " .
		"cpio -oc -C 512 | bzcat -z9 >'../archive/$class.cpio.bz2'" or
		die "can't fork: $!";
	    print BZIP2 join "\n", @{$class{$class}}, "";
	    close BZIP2 or die "cpio/bzip2 error: $! $?";
	    unlink map "$pkgdir/reloc/$_", @{$class{$class}} or warn "rm: $!";
	} elsif ($method eq "zcat") {
	    open COMPRESS, "| cd '$pkgdir/reloc' && " .
		"cpio -oc -C 512 | compress >'../archive/$class.cpio.Z'" or
		die "can't fork: $!";
	    print COMPRESS join "\n", @{$class{$class}}, "";
	    close COMPRESS or die "cpio/compress error: $! $?";
	    unlink map "$pkgdir/reloc/$_", @{$class{$class}} or warn "rm: $!";
	}
    }
    
    if (%class) {
	# rmdir empty directories in reloc
	system "cd '$pkgdir' && find reloc -type d -exec rmdir -ps {} +";

	# PKG_SRC_NOVERIFY: don't check for files in reloc directory
	# PKG_DST_QKVERIFY: quick verify: fix instead of check of file attribs
	# PKG_CAS_PASSRELATIVE: instead of <src> <dst> pair lines, first
	#			pass $SUNW_PKG_DIR and then <dst> lines to cas
	$classes = join " ", sort keys %class;
	push @pkginfo, "PKG_SRC_NOVERIFY=$classes\n",
		       "PKG_DST_QKVERIFY=$classes\n",
		       "PKG_CAS_PASSRELATIVE=$classes\n";

	# write new pkginfo
	open PKGINFO, ">$pkgdir/pkginfo" or die "open $pkgdir/pkginfo: $!";
	print PKGINFO @pkginfo;
	close PKGINFO or die "close $pkgdir/pkginfo: $!";

	# fix mtime, checksum & size of pkginfo, add compressed size of package
	$mtime	  = (stat "$pkgdir/pkginfo")[9];
	$size	  = -s _ or die "empty file $pkgdir/pkginfo";
	($sum)	  = `sum '$pkgdir/pkginfo'` =~ /(\d+)/ or die "sum pkginfo: $?";
	($blocks) = `du -s '$pkgdir'` =~ /(\d+)/ or die "du -s '$pkgdir': $?";
	foreach (@pkgmap) {
	    s/^(:\s*\d+\s+\d+).*/$1 $blocks/;
	    s/^((?:\d+\s+)?i\s+pkginfo\s+)\d+\s+\d+\s+\d+/$1 $size $sum $mtime/;
	}

	# write new pkgmap
	open PKGMAP, ">$pkgdir/pkgmap" or die "open $pkgdir/pkgmap: $!";
	print PKGMAP @pkgmap;
	close PKGMAP or die "close $pkgdir/pkgmap: $!";
    } else {
	warn "$0: nothing to zip in $pkgdir\n";
    }
}
