#!/usr/bin/perl # # prt-auf --- add/update frontend to CRUX pkgutils (offers mostly the same # user experience as prt-get except for the slight delay # entailed by Perl having to compile this file on startup) # # distributed under the same license as the pkgutils, # https://crux.nu/gitweb/?p=tools/pkgutils.git;a=blob_plain;f=COPYING;hb=HEAD # use warnings; use strict; ################### Initialize global variables ####################### my $title="prt-auf"; my $version=0.51; my $cache_ver="V5.1"; my $CONFDIR = "/var/lib/pkg"; my $prtalias="/etc/prt-get.aliases"; my $pkgdb="$CONFDIR/db"; my $prtlocker="$CONFDIR/prt-get.locker"; my $prtcache="$CONFDIR/prt-get.cache"; my @LOCKED; my %ALIASES; my %DEPENDS; my @allports; my %V_REPO; my %V_INST; my %DESC; my %SOFTDEPS; my @results; my $strf; my $ind; my $hh; my $portpath; my $built_pkg; my %osearch = ( cache => 0, regex => 0, path => 0, exact => 0, verbose => 0 ); my %odepends = ( soft => 0, tree => 0, recursive => 0, all => 0 ); my %opkg = ( margs => "", aargs => "", rargs => "", runscripts => "yes", makecommand => "/usr/bin/pkgmk", addcommand => "/usr/bin/pkgadd", removecommand => "/usr/bin/pkgrm", test => "no", group => "no" ); my %olog = ( write => "disabled", mode => "overwrite", rm_on_success => "yes", file => "/var/log/pkgbuild/%n.log" ); my $prtconf = "/etc/prt-get.conf"; ################### Process the given command ######################### my ($action, @query) = parse_args(@ARGV); $osearch{cache}=1 if ($0 =~ /cache$/); my @bldirs = parse_prt_conf($prtconf); my @basedirs = @{$bldirs[0]}; my @localports = @{$bldirs[1]}; # load some data structures into memory for the actions that need them if (($action !~ /^(fsearch|isinst|current)$/) and ($osearch{cache}==0)) { @allports = list_ports(); fill_hashes_from_pkgfiles(); } if ($osearch{cache}==1) { fill_hashes_from_cache(); } if ($action !~ /^(quickdep|search|dsearch|fsearch|info|dup|readme|cat)$/) { open (DB, $pkgdb) or die "Could not open package database!\n"; local $/=""; while () { $V_INST{$1} = $2 if m/^(.*)\n(.*)\n/; } close (DB); } if ($action =~ /^(diff|quickdiff|listlocked|depends|deptree|remove|install|update|depinst|grpinst|sysup)$/) { get_locked_and_aliased(); } ############## Branch based on the requested action ################# if ($action eq "path") { @results = find_port_by_name($query[0],1,1,0); } elsif ($action eq "search") { @results = find_port_by_name(@query,0,$osearch{path},1); } elsif ($action eq "fsearch") { $hh = find_port_by_file(".footprint", @query); } elsif ($action eq "dsearch") { @results = find_port_by_desc(@query); } elsif ($action eq "info") { $portpath = find_port_by_name(@query,1,1,0); @results = get_pkgfile_fields($portpath,"all") if ($portpath); } elsif ($action eq "cache") { printf_ports("CACHE",@allports); } elsif ($action eq "printf") { @results = ($osearch{filter}) ? find_port_by_name($osearch{filter},0,1,1) : @allports; printf_ports($query[0],@results); } elsif ($action eq "lock") { port_lock(@query); } elsif ($action eq "unlock") { port_unlock(@query); } elsif ($action eq "ls") { port_ls(@query); } elsif ($action =~ /^(cat|edit|readme)$/) { port_edit($1,@query); } elsif ($action =~ /^(depends|quickdep)$/) { @results=deporder($1,@query); } elsif ($action =~ /^dep(endent|tree)$/) { @results=list_ports($action,@query); } elsif ($action eq "sysup") { my @outdated = grep { s/ .+// } port_diff("quick"); shift @outdated; @results = up_inst("sysup", @outdated); } elsif ($action =~ /^(install|update)$/) { @results = up_inst($action,@query); } elsif ($action eq "depinst") { @results = up_inst("depinst",@query); } elsif ($action eq "dup") { $ind=find_dups(@query); } elsif ($action eq "remove") { $ind=uninstall(@query); } elsif ($action =~ /^(isinst|current)$/) { @results = port_diff($1,@query); } elsif ($action =~ /(.*)diff$/) { @results = port_diff($1); } elsif ($action =~ /^list(.*)/) { @results = list_ports($1); } elsif ($action eq "help") { print_help(); } elsif ($action eq "version") { print "$title $version\n"; } else { printf "Unsupported command '$action'.\n"; } #################### Post-transaction reports ####################### $strf = "%s\n"; if (($action =~ /^(listinst|listorphans|dependent)/) and ($odepends{tree}==0)) { foreach my $result (sort @results) { $result = ($osearch{verbose}==1) ? " $result: $V_INST{$result}" : " $result"; $result = ($osearch{verbose}>1) ? " $result: $V_INST{$result}\n$DESC{$result}\n" : $result; printf $strf, $result; } } elsif ($action =~ /^(list|search|dsearch|path)/) { foreach my $result (@results) { next if ((! $result) or ($result =~ /^\s*$/)); $result =~ s/.*\/(.*)$/$1/ if (($action ne "path") and ($osearch{path}==0)); $result .= " $V_REPO{$result}" if (($osearch{verbose}==1) and ($action ne "path")); $result .= " $V_REPO{$result}\n$DESC{$result}\n" if (($osearch{verbose}>1) and ($action ne "path")); printf $strf, $result; } } elsif ($action =~ /^(fsearch)/) { my %hits = %{$hh}; $strf = "%20s %s\n"; my @fmatch; printf $strf, "Found in", "Matching File"; foreach my $fh (keys %hits) { chomp($hits{$fh}); @fmatch = split /\s/, $hits{$fh}; foreach my $fileN (@fmatch) { printf $strf, $fh, $fileN; } } } elsif ($action =~ /^(current|isinst|dup|diff|quickdiff)$/) { my $format = "%20s %15s %20s\n"; my $ind = shift(@results); if ($action eq "diff") { printf $format, "Port", "Installed", "Available in Repo"; } foreach my $diffline (@results) { if ($action =~ /^(current|isinst)$/) { print "$diffline\n"; next; } my ($diffN, $diffI, $diffR) = split(/ /, $diffline); next if (($osearch{filter}) and ($diffN !~ /$osearch{filter}/)); next if ((grep {$_ eq $diffN} @LOCKED) and ($odepends{all}==0)); printf "$format", $diffN, $diffI, $diffR if ($action eq "diff"); printf "%s ", $diffN if (($action eq "quickdiff") and ($diffR ne "MISSING")); } print "\n" if ($action eq "quickdiff"); exit $ind; } elsif ($action =~ /^(depends|quickdep)$/) { print "-- dependency list ([i] = installed, [a] = alias installed)\n" if ($action =~ /^dep/); my $strf="%3s %s\n"; my $dep; my $missing=0; foreach $dep (@results) { if ($dep =~ /MISSING/) { $missing=1; print "-- missing packages\n"; next; } next if (! $dep); my $alias; if ($action ne "quickdep") { $ind = (grep { $_ eq $dep } keys %V_INST) ? "[i]" : "[ ]"; $alias = who_aliased_to($dep); $ind = ((! $V_INST{$dep}) and ($alias)) ? "[a]" : $ind; $dep .= " (provided by $alias)" if ($ind eq "[a]"); $dep .= " $V_REPO{$dep}" if (($osearch{verbose}==1) and ($ind ne "[a]")); $dep .= " $V_REPO{$dep}\n$DESC{$dep}" if (($osearch{verbose}>1) and ($ind ne "[a]")); } printf $strf, $ind, $dep unless ($action eq "quickdep"); printf "%s ", $dep if ($action eq "quickdep"); } print "\n" if ($action eq "quickdep"); } elsif ($action eq "info") { $strf = "%14s: %-s\n"; exit 1 if ($#results < 0); my @fields = ("Name", "Repository", "Version", "Release", "Description", "Dependencies", "URL", "Optional Deps", "Maintainer", "Readme", "PreInstall", "PostInstall"); for (my $i=0; $i<9; $i++) { printf $strf, $fields[$i], $results[$i]; } } elsif ($action eq "remove") { my %removed = %$ind; my @successes = grep { $removed{$_}==1 } keys %removed; my @failures = grep { $removed{$_}==0 } keys %removed; print "Ports removed:\n" if (@successes); foreach my $p (@successes) { print "$p\n"; } } elsif ($action =~ /^(install|update|depinst|sysup)$/) { my @ok = @{$results[0]}; my @ok_pre = @{$results[1]}; my @ok_post = @{$results[2]}; my @ok_readme = @{$results[3]}; my @not_ok = @{$results[4]}; my $note; if ($opkg{test} eq "yes") { print "\n$action successful.\n"; print "*** prt-auf: test mode end\n\n"; } if (($opkg{test} eq "no") and (@ok)) { print "Successful ports:\n"; foreach my $k (@ok) { $note = (grep /^$k$/, @ok_pre) ? " pre: ok. " : ""; $note .= (grep /^$k$/, @ok_post) ? " post: ok. " : ""; $note = ((grep /^$k$/, @ok_pre) or (grep /^$k$/, @ok_post))? "($note)" : ""; print " $k $note\n"; } print "\n"; } if (@ok_readme) { print "Ports with README files:\n"; foreach (@ok_readme) { print " $_\n"; } print "\n"; } if (@not_ok) { print "Ports with pkgmk/pkgadd failures:\n"; foreach (@not_ok) { print " $_\n"; } print "\n"; } } else {} # Done! #################### Begin Subroutines ####################### sub parse_args { my @query; while (my $arg = shift) { if ($arg =~ /^(search|dsearch|fsearch|path|info|list|remove)$/) { $action = $1; } elsif ($arg =~ /^(install|update|depinst|sysup)$/) { $action = $1; } elsif ($arg eq "grpinst") { $action = "install"; $opkg{group} = "yes"; print "Warning: grpinst is obsolescent, using install --group\n"; } elsif ($arg =~ /^(lock|unlock|listlocked|current|isinst)$/) { $action = $1; } elsif ($arg =~ /^(diff|quickdiff|printf|listinst|listorphans)$/) { $action = $1; $odepends{tree} = 0; } elsif ($arg =~ /^(depends|quickdep|dup|dependent)$/) { $action = $1; } elsif ($arg eq "deptree") { $action = $arg; $odepends{tree} = 1; } elsif ($arg =~ /^(readme|cat|edit|ls|help|version|cache)$/) { $action = $1; } elsif ($arg eq "--tree") { $odepends{tree} = 1; } elsif ($arg eq "--all") { $odepends{all} = 1; } elsif ($arg eq "--softdeps") { $odepends{soft} = 1; } elsif ($arg eq "--recursive") { $odepends{recursive} = 1; } elsif ($arg eq "--cache") { $osearch{cache} = 1; } elsif ($arg =~ /^--config=(.+)$/) { $prtconf = $1; } elsif ($arg eq "--path") { $osearch{path} = 1; } elsif ($arg eq "--regex") { $osearch{regex} = 1; } elsif ($arg =~ /^--filter=(.+)/) { $osearch{filter} = $1; } elsif ($arg eq "-v") { $osearch{verbose} += 1; } elsif ($arg eq "-vv") { $osearch{verbose} += 2; } elsif ($arg eq "--test") { $opkg{test} = "yes"; } elsif ($arg eq "--group") { $opkg{group} = "yes"; } elsif ($arg eq "-fr") { $opkg{margs} .= " -f"; } elsif ($arg =~ /^(-uf|-if|-us|-is|-ns|-kw)$/) { $opkg{margs} .= " $1"; } elsif ($arg =~ /^--margs=(.+)/) { $opkg{margs} .= $1; } elsif ($arg =~ /^--aargs=(-r|--root)=(.+)/) { $opkg{aargs} .= "$1 $2"; } elsif ($arg =~ /^--rargs=(-r|--root)=(.+)/) { $opkg{rargs} .= "$1 $2"; } elsif ($arg =~ /^-/) { print "'$arg' is not a recognized option.\n"; } else { push (@query, $arg); } } if (! $action) { print_help(); } if (($#query > -1) and ($action =~ /^(diff|quickdiff|cache|list|sysup)/)) { print "warning: $1 takes no arguments; ignoring those given.\n"; } if (($#query > 0) and ($action =~ /^(search|dsearch|fsearch|info|readme|path|ls)$/)) { print "warning: $1 takes only one argument; ignoring all but the first.\n"; } if ((! @query) and ($action =~ /^(search|dsearch|fsearch|info|readme|path|ls)$/)) { print "$1 requires an argument.\n"; exit 1; } if (($#query != 0) and ($action =~ /^(deptree|dependent)$/)) { print "$1 requires exactly one argument.\n"; exit 1; } if (($#query < 0) and ($action =~ /^(install|update|depinst|remove)$/)) { print "$1 requires at least one argument.\n"; exit 1; } return $action, @query; } sub parse_prt_conf { my @basedirs; my @localports; my $conf = shift; open(PORTS, $conf) or die "could not open $conf"; while () { chomp; if ( /^prtdir\s+/ ) { my $line = $_; $line =~ s/^prtdir\s+//; #remove the leading directive $line =~ s/#.*$//; #strip inline comments like this one $line =~ s/\s+//g; #collapse all whitespace, even if in a path! if ( $line !~ /:/ ) { push @basedirs, $line if (-d $line); } else { my @a = split(/:/, $line); my @b = split(/,/, $a[1]); while ( my $c = pop @b ) { my $port = $a[0] . "/" . $c; push @localports, $port if (-d $port); } } } $opkg{runscripts} = $1 if /^runscripts\s+(yes|no)/; $opkg{makecommand} = $1 if /^makecommand\s+(.*)(#|$)/; $opkg{addcommand} = $1 if /^addcommand\s+(.*)(#|$)/; $opkg{removecommand} = $1 if /^removecommand\s+(.*)(#|$)/; $olog{write} = $1 if /^writelog\s+(enabled|disabled)/; $olog{mode} = $1 if /^logmode\s+(append|overwrite)/; $olog{rm_on_success} = $1 if /^rmlog_on_success\s+(no|yes)/; $olog{file} = $1 if /^logfile\s+(.*)\s*(#|$)/; $prtcache = $1 if /^cachefile\s+(.*)\s*(#|$)/; } close(PORTS); return \@basedirs, \@localports; } sub find_dups { my %seen; my $format=shift; my @dupinfo; my @info1; my $dupstr; my @hits; foreach my $pp (@allports) { my $pn = (split /\//, $pp)[-1]; $seen{$pn}++; } my @dups = grep { $seen{$_} > 1 } keys %seen; my %subscripts = ( "%n"=>0, "%p1"=>1, "%v1"=>2, "%u1"=>3, "%M1"=>4, "%p2"=>5, "%v2"=>6, "%u2"=>7, "%M2"=>8 ); if (($osearch{verbose}==0) and (! $format)) { foreach my $dup (@dups) { print "$dup\n"; } } elsif (($osearch{verbose}>0) and (! $format)) { foreach my $dup (@dups) { @hits = grep /\/$dup$/, @allports; print "$hits[0] > $hits[1]\n" if $osearch{verbose}==1; printf "* %s\n"x(1+$#hits), @hits if $osearch{verbose}>1; } } else { # the user has given us a format string; let's respect it foreach my $dup (@dups) { @hits = grep /\/$dup$/, @allports; @dupinfo = (get_pkgfile_fields($hits[0],"all"))[1,2,6,8]; @info1 = (get_pkgfile_fields($hits[1],"all"))[1,2,6,8]; push(@dupinfo,@info1); unshift(@dupinfo,$dup); $dupstr = "$format\n"; $dupstr =~ s/(%n|%p1|%v1|%u1|%M1|%p2|%v2|%u2|%M2)/$dupinfo[$subscripts{$1}]/g; print $dupstr; } } return 1+$#dups; } sub get_locked_and_aliased { if (-f $prtalias) { open (AL, $prtalias); while () { $ALIASES{$1} = $2 if m/^\s*(.*)\s*:\s*(.*)/; } close (AL); } if (-f $prtlocker) { open (LK, $prtlocker) or return; while () { push (@LOCKED, $_) unless /^\s*$/; } close (LK); } } sub who_aliased_to { my $target = shift; my @substitutes = grep { defined $V_INST{$_} } keys %ALIASES; @substitutes = grep { $ALIASES{$_} eq $target } @substitutes; my $who = (@substitutes) ? $substitutes[0] : undef ; return $who; } sub printf_ports { my $FS; my @pstats; my $p; my $inputf=shift; my @targets=@_; my @pos; my @outfields; my $outputf; my %FS = ( "t"=>"\t", "n"=>"\n" ); my %subscripts = ( "n"=>0, "p"=>1, "v"=>2, "r"=>3, "d"=>4, "u"=>5, "P"=>7, "M"=>8, "R"=>9, "E"=>10, "O"=>11, "l"=>12, "i"=>13); if ($inputf eq "CACHE") { open (CACHE,'>',$prtcache) or die "cannot create a new cache file"; print CACHE "V5.1\n"; my %cached; foreach my $pp (@targets) { $p = (split /\//, $pp)[-1]; next if ($cached{$p}); @pstats = get_pkgfile_fields($pp,"all"); printf CACHE "%s\n"x($#pstats+1), @pstats; printf CACHE "\n"; $cached{$p}=1; } close (CACHE); print "cache created.\n"; } else { @outfields = split /(\\t|\\n)/, $inputf; foreach (@outfields) { if (m/\\(t|n)/) { $outputf .= $FS{$1}; next; } $strf = $_; s/%(p|n|v|r|d|u|P|M|R|E|O|l|i)/_Z_$subscripts{$1}/g; push @pos, grep { s/([0-9]+)(.*)/$1/ } (split /_Z_/, $_); $strf =~ s/%(p|n|v|r|d|u|P|M|R|E|O|l|i)/%s/g; $outputf .= $strf; } foreach my $pp (@targets) { $p = (split /\//, $pp)[-1]; @pstats = get_pkgfile_fields($pp,"all"); $pstats[12] = (grep /^$p$/, @LOCKED) ? "yes" : "no"; $pstats[13] = (grep /^$p$/, keys %V_INST) ? "yes" : "no"; if (($pstats[13] eq "yes") and ($V_INST{$p} ne $V_REPO{$p})) { $pstats[13] = "diff" } printf STDOUT $outputf, @pstats[@pos]; } } } sub fill_hashes_from_cache { open (my $cf,$prtcache) or die "cannot use $prtcache as a cache!\n"; my $p; my $parent; my $deps; my $softDeps; my $ignored=<$cf>; chomp($ignored); if ($ignored ne "$cache_ver") { die "incompatible cache format; regenerate by running $0 cache"; } while (1) { $p = <$cf>; last unless defined $p; chomp($p); $parent = <$cf>; chomp($parent); push @allports, "$parent/$p"; $V_REPO{$p} = <$cf>; chomp($V_REPO{$p}); $V_REPO{$p} .= "-".<$cf>; $DESC{$p} = <$cf>; $deps = <$cf>; $ignored=<$cf>; $softDeps = <$cf>; chomp($deps, $softDeps, $DESC{$p}, $V_REPO{$p}); $DEPENDS{$p} = $deps; $SOFTDEPS{$p} = $softDeps; $DEPENDS{$p} =~ s/, / /g; $DEPENDS{$p} =~ s/,/ /g; $SOFTDEPS{$p} =~ s/, / /g; $SOFTDEPS{$p} =~ s/,/ /g; for (my $i=8; $i<13; $i++) { $ignored = <$cf>; } } close ($cf); } sub fill_hashes_from_pkgfiles { foreach my $pp (@allports) { my $p = (split /\//, $pp)[-1]; if (! $V_REPO{$p}) { # only populate hashes with the first port found my ($rver, $rrel, $rdesc, $rdeps, $rsoftdeps) = get_pkgfile_fields($pp); $V_REPO{$p} = ($rver) ? $rver : "0"; $V_REPO{$p} .= ($rrel) ? "-$rrel" : "-1"; $DEPENDS{$p} = ($rdeps) ? $rdeps : ""; $SOFTDEPS{$p} = ($rsoftdeps) ? $rsoftdeps : ""; $DEPENDS{$p} =~ s/, / /g; $DEPENDS{$p} =~ s/,/ /g; $SOFTDEPS{$p} =~ s/, / /g; $SOFTDEPS{$p} =~ s/,/ /g; $DESC{$p} = ($rdesc) ? $rdesc : ""; } } } sub get_pkgfile_fields { my ($descrip, $url, $maintainer, $Version, $Release)=('','','',0,0); my ($readme, $preInstall, $postInstall, $Dependencies, $SoftDeps)=("no","no","no",'',''); my $portpath = shift; my $Name = (split /\//, $portpath)[-1]; my $pkgfile = "$portpath/Pkgfile"; $readme = "yes" if (-f "$portpath/README") or (-f "$portpath/README.md"); $preInstall = "yes" if (-f "$portpath/pre-install"); $postInstall = "yes" if (-f "$portpath/post-install"); $portpath =~ s/\/[^\/]+?$//; # now it should be called repository path! open(PF,$pkgfile) or die "Cannot open $pkgfile for reading!\n"; while () { chomp; if (s/^# Description:\s*(.*)/$1/) { $descrip = $_; } elsif (s/^# URL:\s*(.*)/$1/) { $url = $_; } elsif (s/^version=(.*)/$1/) { $Version = $_; } elsif (s/^release=(.*)/$1/) { $Release = $_; } elsif (s/^# Depends on:\s*(.*)/$1/) { $Dependencies = $_; } elsif (s/^# (Optional|Nice to have):\s*(.*)/$2/) { $SoftDeps = $_; } elsif (s/^# Maintainer:\s*(.*)/$1/) { $maintainer = $_; } else {} } close(PF); $Dependencies =~ s/, / /g; $Dependencies =~ s/,/ /g; $SoftDeps =~ s/, / /g; $SoftDeps =~ s/,/ /g; if (shift) { return $Name, $portpath, $Version, $Release, $descrip, $Dependencies, $url, $SoftDeps, $maintainer, $readme, $preInstall, $postInstall; } else { return $Version, $Release, $descrip, $Dependencies, $SoftDeps; } } sub find_port_by_file { # for now only used to search footprints, but can be generalized my $portfile = shift; my $query = shift; my ($lp, $candidate, $fh); my %hits=(); my $linewanted = qr/$query/is; LOCALENTRY: foreach $lp (@localports) { open ($fh, "$lp/$portfile") or die "cannot open $portfile for $lp\n"; while (<$fh>) { $hits{$lp} .= (split /\s/, $_)[2]." " if $_ =~ $linewanted; } close ($fh); } foreach my $collection (@basedirs) { my $prefix = ( $osearch{path} == 1 ) ? "$collection/" : ""; opendir (DIR, $collection) or return; PORTENTRY: foreach $candidate (sort(readdir(DIR))) { next if (! -f "$collection/$candidate/$portfile"); open ($fh, "$collection/$candidate/$portfile") or die "cannot open $portfile in $candidate\n"; while (<$fh>) { $hits{"$prefix$candidate"} .= (split /\s/, $_)[2]." " if $_ =~ $linewanted; } close ($fh); } closedir(DIR); } return \%hits; } sub find_port_by_desc { my $query=shift; my @hits = grep { (/$query/i) or ($DESC{$_} =~ /$query/i) } keys %DESC; return @hits; } sub find_port_by_name { my $query = shift; my $exact=shift; my $fullpath=shift; my $exhaustive=shift; $query =~ s/\+/\\\+/g unless (($action =~ /^(search|dsearch)$/) and ($osearch{regex}==1)); $query =~ s/\./\\\./g unless (($action =~ /^(search|dsearch)$/) and ($osearch{regex}==1)); my $pattern = ($exact==1) ? qr/^$query$/s : qr/$query/is; my %names_only = map { ($_ => (split /\//, $_)[-1]) } @allports; my @hits = grep { $names_only{$_} =~ $pattern } @allports; @hits = map { $names_only{$_} } @hits if $fullpath==0; return @hits if ($exhaustive==1); return $hits[0] if ($exhaustive==0); } sub uninstall { my $PKGRM = $opkg{removecommand}; my @targets = grep { defined $V_INST{$_} } @_; my @rubbish = grep { ! defined $V_INST{$_} } @_; foreach my $r (@rubbish) { print "$r not installed; ignoring.\n"; } my %removed = map { $_ => 0 } @targets; foreach my $t (@targets) { ($opkg{test} eq "no") ? system($PKGRM,$opkg{rargs},$t) : print "$PKGRM $opkg{rargs} $t\n"; $removed{$t}=1 if ($?>>8 == 0); if ((grep /^$t$/, @LOCKED) and ($opkg{test} eq "no")) { port_unlock($t); } } return \%removed; } sub port_lock { my %oldlocks = map { $_ => "L" } @LOCKED; my @newlocks = grep { ! defined $oldlocks{$_} } @_; if (@newlocks) { open (LK,'>>',$prtlocker) or die "cannot open $prtlocker for writing.\n"; foreach my $lp (@newlocks) { print LK "$lp\n"; print STDOUT "$lp locked.\n"; } close (LK); } } sub port_unlock { my %unlocks = map { $_ => "U" } @_; my @newlocks = grep { ! defined $unlocks{$_} } @LOCKED; open (LL, '>', $prtlocker."-tmp"); foreach my $nl (@newlocks) { print LL "$nl\n" unless $nl =~ /^\s*$/; } close (LL); system ("mv",$prtlocker."-tmp",$prtlocker); } sub list_ports { my @found; my $subset = shift; if (! $subset) { # empty arg: list all valid ports foreach my $collection (@basedirs) { opendir (DIR, $collection) or next; foreach my $port (sort(readdir DIR)) { next if (! -f "$collection/$port/Pkgfile"); push (@found, "$collection/$port"); } closedir (DIR); } foreach my $lp (@localports) { push (@found, $lp) if (-f "$lp/Pkgfile"); } } elsif ($subset eq "inst") { @found = keys %V_INST; } elsif ($subset eq "locked") { @found=@LOCKED; } elsif ($subset =~ /^(orphans|dependent|deptree)$/) { my $seed; my $sseed; our @searchspace=(($subset eq "orphans") or ($odepends{all} == 0)) ? keys %V_INST : keys %V_REPO ; @searchspace = grep { defined $DEPENDS{$_} } @searchspace; if ($subset =~ /^dep/) { $seed=shift; $sseed = $seed; if (! find_port_by_name($seed,1,1,0)) { print "$seed not found in the ports tree.\n"; return; } # workaround for any port with a plus sign in its name $sseed =~ s/\+/\\\+/g } if ($subset eq "orphans") { my %not_orphans = map { $_ => 0 } @searchspace; foreach my $port (@searchspace) { map { $not_orphans{$_} = 1 } split(/ /, $DEPENDS{$port}); if ($odepends{soft} == 1) { map { $not_orphans{$_} = 1 } split(/ /, $SOFTDEPS{$port}); } } @found = grep { $not_orphans{$_} eq 0 } keys %V_INST; } elsif (($subset eq "dependent") and ($odepends{recursive}==0)) { @found = grep { " $DEPENDS{$_} " =~ / $sseed / } @searchspace; if ($odepends{soft}==1) { push (@found, grep{ " $SOFTDEPS{$_} " =~ / $sseed / } @searchspace); } if ($odepends{tree}==1) { unshift (@found, "$seed"); } } elsif ($subset =~ /^dep(endent|tree)/) { our $direction = ($subset eq "deptree") ? "fwd" : "rev"; my $header = (($subset eq "deptree") and ($odepends{tree} == 1)) ? "-- dependencies ([i] = installed, '-->' = already shown)\n": "-- reverse dependencies ('-->' = already shown)\n"; if (($direction eq "fwd") and ($odepends{soft} == 1)) { $header =~ s/installed,/installed, [s] = installed softdep,/; } print $header unless ($odepends{tree} == 0); our $indent=" "; our $height=0; our $ind; our %seen; our @lineage; my @fosters=(); $ind = ($V_INST{$seed}) ? "[i]" : "[ ]"; print "$ind $seed\n" if ($odepends{tree}==1); $seen{$seed} = 1; my @children = ($direction eq "fwd") ? split /[ ,]/, $DEPENDS{$sseed}: grep { " $DEPENDS{$_} " =~ / $sseed / } @searchspace; if ($odepends{soft}==1) { @fosters = ($direction eq "fwd") ? grep { ($V_INST{$_}) } split /[ ,]/, $SOFTDEPS{$sseed}: grep { " $SOFTDEPS{$_} " =~ / $sseed / } @searchspace; } foreach my $sd (@children) { recurse_tree(0,$sd,$direction); } foreach my $sd (@fosters) { recurse_tree(1,$sd,$direction); } sub recurse_tree { my $greedy = shift; my $s = shift; my $direction=shift; my $ps = (($seen{$s}) and ($odepends{all} !=1)) ? "-->\n" : "\n"; $ind = ($V_INST{$s}) ? "[i]" : "[ ]"; $ind = (($ind eq "[i]") and ($greedy)) ? "[s]" : $ind; print $ind.(${indent}x(1+$height))."$s".$ps if ($odepends{tree}==1); return if (($seen{$s}) and ($odepends{all} !=1)); $seen{$s} = 1; my %curdeps = ($direction eq "fwd") ? map {$_ => 0} split /[ ,]/, $DEPENDS{$s} : map {$_ => 0} grep { " $DEPENDS{$_} " =~ / $s / } @searchspace; if ($odepends{soft} == 1) { my @optionals = ($direction eq "fwd") ? grep { ($V_INST{$_}) } split /[ ,]/, $SOFTDEPS{$s} : grep { " $SOFTDEPS{$_} " =~ / $s / } @searchspace; map {$curdeps{$_} = 1} @optionals; } foreach my $dc (keys %curdeps) { if (grep /^$dc$/, @lineage) { print "Warning: dependency cycle => ".$dc."\n" unless ($greedy|$curdeps{$dc}); return; } push (@lineage, $dc); $height = $#lineage+1; recurse_tree($greedy|$curdeps{$dc},$dc,$direction); pop(@lineage); $height = $#lineage+1; } } delete $seen{$seed} if ($odepends{tree} == 0); @found = sort(keys %seen); } # possibilities for the recursive switch have been exhausted else { } } # possibilities for the filter have been exhausted else { } return @found if ((! $subset) or ($subset =~ /^(orphans|locked)$/)); if (! $osearch{filter}) { return @found; } else { return grep {$_ !~ /$osearch{filter}/} @found; } } sub port_diff { # returns a list of all the ports with differences my $dtype=shift; my @argq=@_; my @outfile=(); my $retval=0; if ($dtype !~ /^(current|isinst|utd)/) { foreach my $p (sort(keys %V_INST)) { push @outfile, "$p $V_INST{$p} $V_REPO{$p}" if (($V_REPO{$p}) and ($V_INST{$p} ne $V_REPO{$p})); push @outfile, "$p $V_INST{$p} MISSING" if ((! $V_REPO{$p}) and ($dtype ne "quick")); } } elsif ($dtype eq "utd") { my $q=shift(@argq); if (! $V_INST{$q}) { $retval--; } elsif (($V_REPO{$q}) and ($V_INST{$q} ne $V_REPO{$q})) { $retval++; } else {} } elsif ($dtype =~ /^(current|isinst)$/) { foreach my $q (@argq) { if (! $V_INST{$q}) { push @outfile, "$q: not installed"; $retval++; } else { push @outfile, "$q: version $V_INST{$q}" if ($dtype eq "current"); push @outfile, "$q is installed." if ($dtype eq "isinst"); } } } else {} return $retval, @outfile unless ($dtype eq "utd"); return $retval if ($dtype eq "utd"); } sub deporder { # returns an annotated list using a variation on depth-first search. # Recursion does NOT continue beyond a dependency satisfied by an alias. my $type=shift; my @seeds=@_; our @treewalk=(); our %seen; our @missing; our %imark; our %fmark; our $fmarks; our @results; # first strip out all the names not found in the ports collections push @missing, grep { (! $V_REPO{$_}) } @seeds; @seeds = grep { ($V_REPO{$_}) } @seeds; # determine the minimal set of targets needed to satisfy all dependencies foreach (@seeds) { recurse_deptree($_) } sub recurse_deptree { my $s=shift; my %curdeps; my @optionals; my $substitute = who_aliased_to($s); if ((($substitute) and (! $V_INST{$s})) or ($V_REPO{$s})) { $seen{$s} = 1; } else { push (@missing, $s); return; } return if ($substitute); # cycle detection if (! grep { $_ eq $s } @treewalk) { push (@treewalk, $s); } else { return; } %curdeps = map { $_ => 0 } split /[ ,]/, $DEPENDS{$s}; # if the user toggles --softdeps, consider only the # optional dependencies that are already installed if ($odepends{soft} == 1) { @optionals = grep { ($V_INST{$_}) } split /[ ,]/, $SOFTDEPS{$s}; foreach (@optionals) { $curdeps{$_} = 1 } } foreach my $sd (keys %curdeps) { next if ($seen{$sd}); recurse_deptree($sd); } pop (@treewalk); } # proceed with the topological sort my $graphsize = keys %seen; $fmarks = 0; while ($fmarks < $graphsize) { my @unvisited = grep { ! ($fmark{$_}) } keys %seen; my $node = pop @unvisited; visit($node,0); } sub visit { my $node = shift; my $greedy = shift; if ( $fmark{$node} ) { return; } if ( $imark{$node} ) { print "cycle detected!\n" unless ($greedy==1); return; } $imark{$node} = 1; foreach my $dep (split /[ ,]/, $DEPENDS{$node}) { visit($dep,$greedy); } if ( $odepends{soft} == 1 ) { foreach my $sd (grep { ($V_INST{$_}) } split /[ ,]/, $SOFTDEPS{$node}) { visit($sd,1); } } delete $imark{$node}; $fmark{$node} = 1; $fmarks = keys %fmark; push (@results, $node); } if (($#missing>-1) and ($type ne "quickdep")) { push (@results, "MISSING", @missing); } return @results; } sub up_inst { # returns scalar references to five arrays my $type=shift; my @requested=@_; my @targets; my %EXEMPT; my %pdirs; my %builtpkg; my %mkcmd; my %addcmd; my %status; my %logfile; my %pvars; my $PKGMK=$opkg{makecommand}; my $PKGADD=$opkg{addcommand}; # respect the user-supplied list of ports unless 'depinst' or 'sysup', # in which case put glibc{,-32} at the front of the queue @targets=grep { ($V_REPO{$_}) } @requested; if ($type =~ /^(depinst|sysup)$/) { @requested = deporder("quick",@requested); @targets=grep { !m/^glibc(|-32)$/ } @requested; unshift @targets, grep { m/^glibc(|-32)$/ } @requested; } # exempt any locked ports unless they were explicitly requested %EXEMPT = map { $_ => 1 } @LOCKED; if ($type =~ /^(depinst|sysup)$/) { @targets = grep {(! $EXEMPT{$_})} @targets; } # first determine the directories from which pkgmk must be called, # the package that will appear after a successful build, # and where to save the build log. my ($COMPRESSION, $PKG_DIR) = parse_pkgmk_conf(); foreach my $t (@targets) { $opkg{$t} = $opkg{margs}; $pvars{'%n'}=$t; $opkg{$t} =~ s/-f// unless (grep { $_ eq $t } @requested); $pvars{'%p'} = find_port_by_name($t,1,1,0); $pdirs{$t} = $pvars{'%p'}; $pvars{'%v'} = $1 if ( $V_REPO{$t} =~ m/(.+)-[0-9]+$/ ); $pvars{'%r'} = $1 if ( $V_REPO{$t} =~ m/-([0-9]+)$/ ); $builtpkg{$t} = ($PKG_DIR) ? "$PKG_DIR/$t#$pvars{'%v'}-$pvars{'%r'}.pkg.tar.$COMPRESSION" : "$pvars{'%p'}/$t#$pvars{'%v'}-$pvars{'%r'}.pkg.tar.$COMPRESSION"; $builtpkg{$t} =~ s/\$name/$t/g; $builtpkg{$t} =~ s/\$\{name\}/$t/g; $mkcmd{$t} = "$PKGMK -d $opkg{$t}"; $addcmd{$t} = "$PKGADD -u $builtpkg{$t}"; $status{$t} = "not done"; if ($olog{write} eq "enabled") { $logfile{$t} = $olog{file}; $logfile{$t} =~ s/(%n|%v|%r|%p)/$pvars{$1}/g; $mkcmd{$t} .= ($olog{mode} eq "append") ? " 2>&1 |/usr/bin/tee -a $logfile{$t}" : " 2>&1 |/usr/bin/tee $logfile{$t}"; } } if ($opkg{test} eq "yes") { print("*** prt-auf $type: test mode\n\n"); print "-- Packages changed\n"; } # build each package, unless already installed or satisfied by an alias BUILDLOG: foreach my $t (@targets) { if (who_aliased_to($t)) { $mkcmd{$t} = "echo \"skipped build ($t provided by an alias)\""; } else { $mkcmd{$t} = "echo \"skipped build ($t up to date)\"" if ((-f $builtpkg{$t}) and ($opkg{$t} !~ /-f/)) and ((-M $builtpkg{$t}) < (-M "$pdirs{$t}/Pkgfile")); $mkcmd{$t} = "" if (($opkg{$t} !~ /-f/) and (port_diff("utd",$t)==0)); } if ($opkg{test} eq "yes") { print("$t\n") if ($mkcmd{$t} ne ""); next BUILDLOG; } if ($mkcmd{$t} ne "") { if ((-f "$pdirs{$t}/pre-install") and ($opkg{runscripts} eq "yes")) { system("sh","$pdirs{$t}/pre-install"); $status{$t} .= ( $?>>8 == 0 ) ? "pre-install ok. " : "pre-install failed. "; } chdir $pdirs{$t}; system("$mkcmd{$t}"); $status{$t} .= ( $?>>8 == 0 ) ? "build ok. " : "build failed. " unless ($logfile{$t}); $status{$t} .= (! log_failure($logfile{$t})) ? "build ok. " : "build failed. " if ($logfile{$t}); $status{$t} = ( $mkcmd{$t} =~ /skipped/ ) ? "build skipped. " : $status{$t}; if (($status{$t} =~ /build ok/) or ($mkcmd{$t} =~ /up to date/)) { $addcmd{$t} =~ s/ -u / / if (port_diff("utd",$t)<0); system("$addcmd{$t}"); $status{$t} .= ( $?>>8 == 0 ) ? "pkgadd ok. " : "pkgadd failed. "; unlink($logfile{$t}) if (($status{$t} =~ /ok. $/) and ($logfile{$t}) and ($olog{rm_on_success} eq "yes")); } if (($status{$t} =~ /pkgadd ok/) and (-f "$pdirs{$t}/post-install") and ($opkg{runscripts} eq "yes")) { system("sh","$pdirs{$t}/post-install"); $status{$t} .= ( $?>>8 == 0 ) ? "post-install ok. " : "post-install failed. "; } } last if (($status{$t} =~ /failed/) and ($opkg{group} eq "yes")); } sub log_failure { my $lf=shift; local $/=""; my $failed=0; open(FH,$lf) or return 0; while () { $failed=1 if m/=====> ERROR: /; } close(FH); return $failed; } my @not_ok = grep { $status{$_} =~ /(pkgadd|build) failed/ } @targets; my @ok = grep { $status{$_} !~ /(pkgadd|build) failed/ } @targets; my @ok_pre = grep { $status{$_} !~ /pre-install failed/ } @targets; my @ok_post = grep { $status{$_} !~ /post-install failed/ } @ok; my @ok_readme = grep -f $pdirs{$_}."/README", @ok; return \@ok, \@ok_pre, \@ok_post, \@ok_readme, \@not_ok; } sub parse_pkgmk_conf { my $CONF="/etc/pkgmk.conf"; my $COMPRESSION; my $PKG_DIR=""; open (CF,$CONF) or return; while () { $COMPRESSION=$1 if m/^PKGMK_COMPRESSION_MODE=(.*)/; $PKG_DIR=$1 if m/^PKGMK_PACKAGE_DIR=(.*)/; } close (CF); if ($COMPRESSION) { $COMPRESSION =~ s/#(.*)$//; # remove same-line comments like this one $COMPRESSION =~ s/"//g; # remove double-quotes (thanks jaeger!) } else { $COMPRESSION = "gz"; } $PKG_DIR =~ s/"//g; return $COMPRESSION, $PKG_DIR; } sub port_ls { my $port=shift; my $pp=find_port_by_name($port,1,1,0); return if (! defined $pp); opendir (DIR,$pp) or die "Cannot open $pp for directory listing!\n"; foreach my $l (sort(readdir(DIR))) { next if (($l eq ".") or ($l eq "..")); print "$l\n"; } closedir (DIR); } sub port_edit { my $type=shift; my $port=shift; my $file=shift; my $pp=find_port_by_name($port,1,1,0); return if (! defined $pp); my $EDITOR = ($ENV{EDITOR}) ? $ENV{EDITOR} : "/usr/bin/vi"; if ($type eq "readme") { port_edit("cat",$port,"README") if (-f "$pp/README"); port_edit("cat",$port,"README.md") if (-f "$pp/README.md"); } if ($type eq "edit") { exec ($EDITOR,"$pp/$file") if (($file) and (-f "$pp/$file")); exec ($EDITOR,"$pp/Pkgfile") if ((! $file) or (! -f "$pp/$file")); } if ($type eq "cat") { if (($file) and (-f "$pp/$file")) { open (PF, "$pp/$file") or die "Could not open $pp/$file.\n"; while () { print $_ ; } close (PF); } else { open (PF, "$pp/Pkgfile") or die "Could not open $pp/Pkgfile.\n"; while () { print $_ ; } close (PF); } } } sub print_help { print < [options] where the actions include: SEARCH search show port names containing dsearch show ports containing in the name or description fsearch show ports that provide filenames matching DIFFERENCES / DEPENDENCIES quickdiff show outdated packages on a single line, separated by spaces quickdep show the dependencies needed by , on a single line deptree show dependency tree for dependent show installed packages which depend on INSTALL, UPDATE and REMOVAL install [opt] install/update ports in the listed order depinst [opt] install/update ports and their dependencies remove [opt] remove ports lock lock each at its current version unlock release the lock on each sysup update all outdated ports, except those that are locked GENERAL INFORMATION list ports in the active repositories listinst ports currently installed listlocked ports that are locked at their current version listorphans installed ports that no other port claims as a hard dependency dup ports that appear more than once in the active collections info version, description, dependencies of path location from which pkgmk would be called to build cat the contents of / on STDOUT isinst whether port is installed current installed version of port COMMON OPTIONS -v show version in listing -vv show version and decription in listing --path print path to port if appropriate (search, list, depends) --regex treat search term as a Perl-compatible regular expression --cache use a cache file --group stop the install/update operation if any port fails --test do not actually run pkgmk/pkgadd, just print the commands on STDOUT EOF exit; }