#!/usr/bin/perl

# Script to do cvsupdate on ports, while displaying progress, sorted
# from the directories with the fewest ports to the directories with
# the most ports.
#
# Without arguments, it does all. Option -i means check the important
# stuff. Option -u means don't check the important stuff.
# Option -q means don't print progress info.
#
# ToDo: An option to only check individual ports that are installed.

# Written 1 February 2024 by Jim Lippard. version 1.0
# Modified 3 February 2024 by Jim Lippard to use pledge/unveil. version 1.1
# Modified 29 May 2024 by Jim Lippard to add -h hostname option. version 1.2
# Modified 3 August 2024 by Jim Lippard to remove unnecessary pledges. v1.3
# Modified 1 October 2024 by Jim Lippard for tiny efficiency improvement. v1.4
# Modified 25 January 2025 by Jim Lippard to use "port" or "port category"
#    appropriately in error messages. v1.5

use strict;
use Getopt::Std;
use if $^O eq "openbsd", "OpenBSD::Pledge";
use if $^O eq "openbsd", "OpenBSD::Unveil";

my @IMPORTANT = (
    'databases',
    'devel',
    'lang',
    'mail',
    'net',
    'security',
    'www'
    );

my @EXCLUDE = (
    'bulk',
    'CVS',
    'distfiles',
    'mystuff',
    'packages',
    'plist',
    'pobj'
    );

my %ANONCVS_HOSTS = (
    'usa', 'anoncvs.usa.openbsd.org',
    'usa2', 'anoncvs4.usa.openbsd.org',
    'australia', 'anoncvs.au.openbsd.org',
    'canada', 'obsdacvs.cs.toronto.edu',
    'canada2', 'anoncvs.comstyle.com',
    'france', 'anoncvs.fr.openbsd.org',
    'germany', 'mirror.osn.de',
    'germany2', 'ftp.hostserver.de',
    'japan', 'anoncvs.jp.openbsd.org',
    'lithuania', 'mirror.litnet.lt',
    'sweden', 'anoncvs.eu.openbsd.org',
    'uk', 'anoncvs.spacehopper.org'
    );

my $ANONCVS = 'anoncvs.usa.openbsd.org';

my $CVS = '/usr/bin/cvs';
my $PORTS_DIR = '/usr/ports';
my $START_WD = '/usr';
my $UNAME = '/usr/bin/uname';

my (%opts, $quiet_mode, $my_anoncvs, $arg, @portcats, @files, $file, $portcat,
    $version, $openbsd_version, @cvs_command,
    %port_counts, $chars_printed);

getopts ('iuqh:', \%opts) || die "Usage: portcheck.pl [-i|-u][-h anoncvs-host|-h list for list] [-q] [portcats|ports]\n";

$quiet_mode = $opts{'q'};

if ($opts{'h'}) {
    if (defined ($ANONCVS_HOSTS{$opts{'h'}})) {
	$my_anoncvs = $ANONCVS_HOSTS{$opts{'h'}};
    }
    elsif ($opts{'h'} = 'list') {
	print "Available hosts:\n";
	foreach $my_anoncvs (sort keys (%ANONCVS_HOSTS)) {
	    print "$my_anoncvs ($ANONCVS_HOSTS{$my_anoncvs})\n";
	}
	exit;
    }
    else {
	$my_anoncvs = $opts{'h'};
    }
}
else {
    $my_anoncvs = $ANONCVS;
}

die "-i and -u are mutually exclusive options.\n" if ($opts{'i'} && $opts{'u'});

if ($#ARGV >= 0) {
    die "-i cannot be used if port categories or individual ports are specified.\n" if ($opts{'i'});
    die "-u cannot be used if port categories or individual ports are specified.\n" if ($opts{'u'});    
    foreach $arg (@ARGV) {
	my $portcat = $arg;
	if ($arg =~ /(.*)\//) {
	    $portcat = $1;
	}
	die "No such port category $portcat.\n" if (!-d "$PORTS_DIR/$portcat");
	die "No such port $arg.\n" if (!-d "$PORTS_DIR/$arg");
	push (@portcats, $arg);
    }
}
elsif ($opts{'i'}) {
    @portcats = @IMPORTANT;
}
else {
    opendir (DIR, $PORTS_DIR) || die "Cannot open $PORTS_DIR. $!\n";
    @files = grep (!/^\.{1,2}$/, readdir (DIR));
    closedir (DIR);
    foreach $file (@files) {
	next if ($opts{'u'} && grep (/^$file$/, @IMPORTANT));
	next if (grep /^$file$/, @EXCLUDE);
	push (@portcats, $file) if (-d "$PORTS_DIR/$file");
    }
}

# Get current system version.
$version = `$UNAME -a`;
chop ($version);
if ($version =~ /OpenBSD [\w\.]+ (\d+)\.(\d+)/) {
    $openbsd_version = 'OPENBSD_' . $1 . '_' . $2;
    @cvs_command = ("$CVS",
		    '-qd',
		    "anoncvs\@$my_anoncvs:/cvs",
		    'up',
		    '-Pd',
		    "-r$openbsd_version"
	);
}
else {
    die "Cannot identify system version. $version\n";
}

chdir ($START_WD);

# Pledge and unveil.
# Can't touch anything except ports and cvs command.
if ($^O eq 'openbsd') {
    pledge ('stdio', 'rpath', 'unveil', 'proc', 'exec') || die "Cannot pledge promises. $!\n";
    # Don't need wpath, cpath due to cvs doing the work.
    unveil ($PORTS_DIR, 'rwc');
    unveil ($CVS, 'x');
    unveil ();
 }

# Count ports per category.
foreach $portcat (@portcats) {
    opendir (DIR, "$PORTS_DIR/$portcat") || die "Cannot open $PORTS_DIR/$portcat. $!\n";
    @files = grep (!/^\.{1,2}$/, readdir (DIR));   
    closedir (DIR);
    $port_counts{$portcat} = $#files;
}

print "Updating ports tree:\n" unless ($quiet_mode);
foreach $portcat (sort numerically keys (%port_counts)) {
    if (!$quiet_mode) {
	print " " if ($chars_printed > 0);
	print "$portcat";
	$chars_printed += length ($portcat) + 1;
	if ($chars_printed > 75) {
	    $chars_printed = 0;
	    print "\n";
	}
    }
    system (@cvs_command, "ports/$portcat");
}
print ".\n" unless ($quiet_mode);

# Sort numerically.
sub numerically { $port_counts{$a} <=> $port_counts{$b}; }
