#!/usr/bin/perl -w
#-
# Copyright (c) 2000-2002 Dag-Erling Codan Smrgrav
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer
#    in this position and unchanged.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
# 3. The name of the author may not be used to endorse or promote products
#    derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#      $FreeBSD: ports/misc/porteasy/src/porteasy.pl,v 1.25 2002/07/16 22:54:05 des Exp $
#

use strict;
use Fcntl;
use Getopt::Long;

my $VERSION	= "2.7.3";
my $COPYRIGHT	= "Copyright (c) 2000-2002 Dag-Erling Smrgrav. " .
		  "All rights reserved.";

# Constants
sub ANONCVS_ROOT	{ ":pserver:anoncvs\@anoncvs.FreeBSD.org:/home/ncvs" }
sub REQ_EXPLICIT	{ 1 }
sub REQ_IMPLICIT	{ 2 }
sub REQ_MASTER		{ 4 }

sub CVS_PASSFILE	{ "/usr/local/share/porteasy/cvspass" }

sub PATH_CVS		{ "/usr/bin/cvs" }
sub PATH_LDCONFIG	{ "/sbin/ldconfig" }
sub PATH_MAKE		{ "/usr/bin/make" }

# Global parameters
my $dbdir     = "/var/db/pkg";	# Package database directory
my $index     = undef;		# Index file
my $portsdir  = "/usr/ports";	# Ports directory
my $tag	      = undef;		# cvs tag to use
my $date      = undef;		# cvs date to use

# Global flags
my $anoncvs   = 0;		# Use anoncvs.FreeBSD.org
my $clean     = 0;		# Clean ports
my $cvsroot   = 0;		# CVS root directory
my $exclude   = 0;		# Do not list installed ports
my $fetch     = 0;		# Fetch ports
my $force     = 0;		# Force package registration
my $installed = 0;		# Select installed ports
my $info      = 0;		# Show port info
my $dontclean = 0;		# Don't clean after build
my $packages  = 0;		# Build packages
my $list      = 0;		# List ports
my $plist     = 0;		# Print packing list
my $build     = 0;		# Build ports
my $status    = 0;		# List installed ports and their status
my $update    = 0;		# Update ports tree from CVS
my $verbose   = 0;		# Verbose mode
my $website   = 0;		# Show website URL

# Global variables
my $need_deps;			# Need dependency information
my $have_index;			# Index has been read
my %ports;			# Maps ports to their directory.
my %pkgname;			# Inverse of the above map
my %masterport;			# Maps ports to their master ports
my %reqd;			# Ports that need to be installed
my %have_dep;			# Dependencies that are already present
my %port_dep;			# Map ports to their dependency lists
my %installed;			# Installed ports
my $capture;			# Capture output

#
# Shortcut for 'print STDERR'
#
sub stderr(@) {
    print(STDERR @_);
}

#
# Similar to err(3)
#
sub bsd::err($$@) {
    my $code = shift;		# Return code
    my $fmt = shift;		# Format string
    my @args = @_;		# Arguments

    my $msg;			# Error message

    $msg = sprintf($fmt, @args);
    stderr("$msg: $!\n");
    exit($code);
}

#
# Similar to errx(3)
#
sub bsd::errx($$@) {
    my $code = shift;		# Return code
    my $fmt = shift;		# Format string
    my @args = @_;		# Arguments

    my $msg;			# Error message

    $msg = sprintf($fmt, @args);
    stderr("$msg\n");
    exit($code);
}

#
# Similar to warn(3)
#
sub bsd::warn($@) {
    my $fmt = shift;		# Format string
    my @args = @_;		# Arguments

    my $msg;			# Error message

    $msg = sprintf($fmt, @args);
    stderr("$msg: $!\n");
}

#
# Similar to warnx(3)
#
sub bsd::warnx($@) {
    my $fmt = shift;		# Format string
    my @args = @_;		# Arguments

    my $msg;			# Error message

    $msg = sprintf($fmt, @args);
    stderr("$msg\n");
}

#
# Call the specified sub with $capture set
#
sub capture($@) {
    my $subr = shift;		# Subroutine to call
    my @args = @_;		# Arguments

    my $oldcapture;		# Old capture flag
    my $rtn;			# Return value

    $oldcapture = $capture;
    $capture = 1;
    $rtn = &{$subr}(@args);
    $capture = $oldcapture;
    return $rtn;
}

#
# Print an info message
#
sub info(@) {

    my $msg;			# Message

    if ($verbose) {
	$msg = join(' ', @_);
	chomp($msg);
	stderr("$msg\n");
    }
}

#
# Print an info message about a subprocess
#
sub cmdinfo(@) {
    info(">>>", @_);
}

#
# Change working directory
#
sub cd($) {
    my $dir = shift;		# Directory to change to

    cmdinfo("cd $dir");
    chdir($dir)
	or bsd::err(1, "unable to chdir to %s", $dir);
}

#
# Run a command and return its output
#
sub cmd($@) {
    my $cmd = shift;		# Command to run
    my @args = @_;		# Arguments

    my $pid;			# Child pid
    local *PIPE;		# Pipe
    my $output;			# Output
    my $rtn;			# Return value

    cmdinfo(join(" ", $cmd, @args));
    $pid = ($capture || $verbose) ? open(PIPE, "-|") : fork();
    if (!defined($pid)) {
	bsd::err(1, ($capture || $verbose) ? "open()" : "fork()");
    } elsif ($pid == 0) {
	exec($cmd, @args);
	die("child: exec(): $!\n");
    }
    if ($capture || $verbose) {
	$output = "";
	while (<PIPE>) {
	    $output .= $_;
	    if ($verbose) {
		stderr($_);
	    }
	}
    }
    $rtn = ($capture || $verbose) ? close(PIPE) : (waitpid($pid, 0) == $pid);
    if (!$rtn) {
	if ($? & 0xff) {
	    bsd::warnx("%s caught signal %d", $cmd, $? & 0x7f);
	} elsif ($? >> 8) {
	    bsd::warnx("%s returned exit code %d", $cmd, $? >> 8);
	} else {
	    bsd::warn("close()");
	}
	return undef;
    }
    if ($capture) {
	$output =~ s/\n*$//s;
	return $output;
    }
    return 1;
}

#
# Run CVS
#
sub cvs($;@) {
    my $cmd = shift;		# CVS command

    my @args;			# Arguments to CVS

    if (!$update) {
	return "\n";
    }
    if (!$verbose) {
	push(@args, "-q");
    }
    push(@args, "-f", "-z3", "-R", "-d$cvsroot", $cmd, "-A");
    if ($cmd eq "checkout") {
	push(@args, "-P");
    } elsif ($cmd eq "update") {
	push(@args, "-P", "-d");
    }
    if ($tag) {
	push(@args, "-r$tag");
    }
    if ($date) {
	push(@args, "-D$date");
    }
    push(@args, @_);
    return cmd(&PATH_CVS, @args);
}

#
# Run make
#
sub make($@) {
    my $port = shift;		# Port category/name
    my @args = @_;

    push(@args, "PORTSDIR=$portsdir")
	unless ($portsdir eq "/usr/ports");
    cd("$portsdir/$port");
    return cmd(&PATH_MAKE, @args);
}

#
# The undocumented command.
#
sub ecks() {

    local *FILE;		# File handle

    sysopen(FILE, "/var/db/port.mkversion", O_RDWR|O_CREAT|O_TRUNC, 0644)
	or bsd::err(1, "open()");
    print(FILE "20380119\n");
    close(FILE);
}

#
# Update the index file
#
sub update_index() {

    my $parent;		# Parent directory

    $parent = $portsdir;
    $parent =~ s/\/*ports\/*$//;
    if (! -f "ports/INDEX" || ! -d "ports/CVS") {
	cd($parent);
	cvs("checkout", "-l", "ports")
	    or bsd::errx(1, "error checking out the index file");
	cd($portsdir);
    } else {
	cd($portsdir);
	cvs("update", "Makefile", "INDEX")
	    or bsd::errx(1, "error updating the index file");
    }
    cvs("update", "Mk")
	or bsd::errx(1, "error updating the Makefiles");
}

#
# Read the ports index
#
sub read_index() {

    local *INDEX;		# File handle
    my $line;			# Line from file

    return if ($have_index);
    info("Reading index file");
    sysopen(INDEX, $index, O_RDONLY)
	or bsd::err(1, "can't open $index");
    while ($line = <INDEX>) {
	my @port;		# Port info

	@port = split(/\|/, $line, 3);
	$port[1] =~ s|^/usr/ports/*||;
	$ports{$port[0]} = $port[1];
	$pkgname{$port[1]} = $port[0];
    }
    close(INDEX);
    info(keys(%ports) . " ports in index");
    $have_index = 1;
}

#
# Find a port by a portion of it's package name
#
sub find_port($) {
    my $port = shift;		# Port to find

    my @suggest;		# Suggestions

    stderr("Can't find required port '$port'");
    @suggest = grep(/^$port/i, keys(%ports));
    if (@suggest == 1 && $suggest[0] =~ m/^$port[0-9.-]/) {
	$port = $ports{$suggest[0]};
	stderr(", assuming you mean $pkgname{$port}.\n");
	return $port;
    } elsif (@suggest) {
	stderr(", maybe you mean:\n  " . (join("\n  ", @suggest)));
    }
    stderr("\n");
    return undef;
}

#
# Add a port to the list of required ports
#
sub add_port($$) {
    my $port = shift;		# Port to add
    my $req = shift;		# Requirement (explicit or implicit)

    my $realport;		# Real port name

    if ($port =~ m|^([^/]+/[^/]+)$|) {
	$realport = $1;
    } else {
	if (!$have_index) {
	    read_index();
	}
	if (exists($ports{$port})) {
	    $realport = $ports{$port};
	} else {
	    $realport = find_port($port);
	}
    }
    if (!$realport) {
	return 1;
    }
    if (!exists($reqd{$realport})) {
	$reqd{$realport} = 0;
    }
    $reqd{$realport} |= $req;
    return 0;
}

#
# Get the ORIGIN line from a manifest
#
sub get_origin($) {
    my $port = shift;		# Port to inspect

    local *FILE;		# File handle
    my $origin;			# Origin

    if (!sysopen(FILE, "$dbdir/$port/+CONTENTS", O_RDONLY)) {
	bsd::warn("can't read manifest for $port");
	return undef;
    }
    while (<FILE>) {
	if (m/^\@comment\s+ORIGIN:(.*)\s*$/) {
	    $origin = $1;
	    last;
	}
    }
    close(FILE);
    if ($origin) {
	info("$port -> $origin\n");
    }
    return $origin;
}

#
# Select installed ports
#
sub add_installed() {

    local *DIR;			# Directory handle
    my $port;			# Installed port
    my $origin;			# Port's origin

    opendir(DIR, $dbdir)
	or bsd::err(1, "can't read database directory");
    foreach $port (readdir(DIR)) {
	next if ($port eq "." || $port eq ".." || ! -d "$dbdir/$port");
	if (!defined($origin = get_origin($port))) {
	    bsd::warn("$port has no \@origin line\n");
	    if (!defined($origin = $ports{$port})) {
		bsd::warnx("installed port %s is unknown", $port);
	    }
	}
	if (defined($installed{$port} = $origin)) {
	    add_port($origin, &REQ_EXPLICIT);
	}
    }
    closedir(DIR);
}

#
# Find master directory for a port
#
sub find_master($) {
    my $port = shift;		# Port

    local *FILE;		# File handle
    my $master;			# Master directory

    if ($masterport{$port}) {
	return $masterport{$port};
    }

    # Look for MASTERDIR in the Makefile. We can't use 'make -V'
    # because the Makefile might try to include the master port's
    # Makefile, which might not be checked out yet.
    open(FILE, "$portsdir/$port/Makefile")
	or bsd::err(1, "unable to read Makefile for $port");
    while (<FILE>) {
	if (/^(?:MAIN|MASTER)DIR\s*=\s*(\S+)\s*$/) {
	    $master = $1;
	} elsif (/^\.?include \"([^\"]+)\/Makefile(?:[^\/\"]*)\"\s*$/) {
	    $master = $1;
	}
	if (defined($master) && $master !~ m/WRKDIRPREFIX/) {
	    $master =~ s/^\$\{.CURDIR\}//;
	    $master = "/$port/$master";
	    $master =~ s|/+|/|g;
	    1 while ($master =~ s|/[^\./]*/\.\./|/|);
	    $master =~ s|^/||;
	    if ($master !~ m|^[^/]+/[^/]+$|) {
		bsd::warn("invalid master for %s: %s", $port, $master);
		next;
	    }
	    close(FILE);
	    info("$master is master for $port\n");
	    return $masterport{$port} = $master;
	}
    }
    close(FILE);
    return undef;
}

#
# Find a dynamic library
#
sub find_library($) {
    my $library = shift;	# Library to find

    my $ldconfig;		# Output from ldconfig(8)

    $ldconfig = capture(\&cmd, (&PATH_LDCONFIG, "-r"));
    defined($ldconfig)
	or errx(1, "unable to run ldconfig");
    if ($ldconfig =~ m/^\s*\d+:-l$library(\.\d+)* => (.*)$/m) {
	info("The $library library is installed as $2");
	return 1;
    }
    return 0;
}

#
# Find a binary
#
sub find_binary($) {
    my $binary = shift;		# Binary to find

    my $dir;			# Directory

    if ($binary =~ m|^/|) {
	info("$binary is installed as $binary");
	return (-x $binary);
    }
    foreach $dir (split(/:/, $ENV{'PATH'})) {
	if (-x "$dir/$binary") {
	    info("$binary is installed as $dir/$binary");
	    return 1;
	}
    }
    return 0;
}

#
# Process a dependency list
#
sub add_dependencies($$@) {
    my $port = shift;		# Port
    my $finder = shift;		# Finder function
    my @dependlist = @_;	# Dependency list

    my $item;			# Iterator

    foreach $item (@dependlist) {
	$item =~ s|\`([^\`]+)\`|capture(\&cmd, "sh", "-c", $1)|eg;
	if ($item !~ m|^(?:([^:]+):)?$portsdir/([^/:]+/[^/:]+)/?(:[^:]+)?$|) {
	    bsd::warnx("invalid dependency: %s", $item);
	    next;
	}
	my ($lhs, $rhs, $target) = ($1, $2, $3);
	next if ($port_dep{$port}->{$rhs});
	# XXX this isn't quite right; lhs-less dependencies should be
	# XXX checked against /var/db/pkg or something.
	if ($exclude && defined($lhs)) {
	    if ($have_dep{$rhs}) {
		next;
	    }
	    info("Verifying status of $rhs ($lhs)");
	    if (($lhs =~ m|^/| && -f $lhs) || &{$finder}($lhs)) {
		info("$rhs seems to be installed");
		$have_dep{$rhs} = 1;
		next;
	    }
	    $have_dep{$rhs} = -1;
	}
	info("Adding $rhs as a dependency for $port");
	$port_dep{$port}->{$rhs} = $target || 'install';
    }
}

#
# Find a port's dependencies
#
sub find_dependencies($) {
    my $port = shift;		# Port

    my $dependvars;		# Dependency variables

    return () unless $need_deps;
    if (!exists($port_dep{$port})) {
	$dependvars = capture(\&make, ($port, "-VLIB_DEPENDS"));
	defined($dependvars)
	    or bsd::errx(1, "failed to obtain dependency list");
	add_dependencies($port, \&find_library, split(' ', $dependvars));
	$dependvars = capture(\&make, ($port,
				       "-VFETCH_DEPENDS",
				       "-VBUILD_DEPENDS",
				       "-VRUN_DEPENDS",
				       "-VDEPENDS"));
	defined($dependvars)
	    or bsd::errx(1, "failed to obtain dependency list");
	add_dependencies($port, \&find_binary, split(' ', $dependvars));
    }
    return keys(%{$port_dep{$port}});
}

#
# Update all necessary files to build the specified ports
#
sub update_ports_tree(@) {
    my @ports = @_;		# Ports to update

    my $port;			# Port name
    my $category;		# Category name
    my %upd_cat;		# Hash of updated categories
    my %upd_port;		# Hash of updated ports
    my %processed;		# Hash of processed ports
    my @additional;		# Additional dependencies
    my $n;			# Pass count
    my $makev;			# Output from 'make -v'

    foreach $port (@ports) {
	push(@additional, $port);
    }
    for ($n = 0; ; ++$n) {
	my @update_now;		# Ports that need updating now
	my $item;		# Iterator
	my $master;		# Master port
	my $dependency;		# Dependency

	# Determine which ports need updating
	foreach $item (@additional) {
	    next if $processed{$item};
	    ($category, $port) = split(/\//, $item);
	    if (!exists($upd_port{$category})) {
		$upd_port{$category} = {};
	    }
	    if (!exists($upd_port{$category}->{$port})) {
		$upd_port{$category}->{$port} = 0;
	    }
	    push(@update_now, $item);
	}
	last unless @update_now;
	info("Pass $n:", @update_now);

	# Update the relevant sections of the ports tree
	foreach $category (keys(%upd_port)) {
	    my @ports;		# Ports to update

	    if (!$upd_cat{$category}) {
		cd($portsdir);
		cvs("update", "-l", $category)
		    or bsd::errx(1, "error updating the '$category' category");
		$upd_cat{$category} = 1;
	    }
	    foreach $port (keys(%{$upd_port{$category}})) {
		next if ($upd_port{$category}->{$port});
		push(@ports, $port);
		$upd_port{$category}->{$port} = 1;
	    }
	    if (@ports) {
		cd("$portsdir/$category");
		cvs("update", @ports)
		    or bsd::errx(1, "error updating the '$category' category");
	    }
	}

	# Process all unprocessed ports we know of so far
	foreach $port (@update_now) {
	    # See if the port has an unprocessed master port
	    if (($master = find_master($port)) && !$processed{$master}) {
		add_port($master, &REQ_MASTER);
		info("Adding $master to head of line\n");
		unshift(@additional, $master);
		# Need to process master before we continue
		next;
	    }

	    # Find the port's package name
	    if (!exists($pkgname{$port})) {
		$makev = capture(\&make, ($port, "-VPKGNAME"));
		if ($makev =~ m/^\s*(\S+)\s*$/s) {
		    $pkgname{$port} = $1;
		} else {
		    bsd::warnx("failed to obtain package name for $port");
		}
	    }

	    # Find the port's dependencies
	    foreach $dependency (find_dependencies($port)) {
		next if ($processed{$dependency});
		if ($reqd{$port} == &REQ_MASTER) {
		    add_port($dependency, &REQ_MASTER);
		} else {
		    add_port($dependency, &REQ_IMPLICIT);
		}
		info("Adding $dependency to back of line\n");
		push(@additional, $dependency);
	    }

	    # Mark port as processed
	    $processed{$port} = 1;
	}
    }
}

#
# Find a specific file belonging to a specific port
#
sub find_port_file($$) {
    my $port = shift;		# Port
    my $file = shift;		# File to look for

    my $master;			# Master port

    $master = $port;
    while (!-f "$portsdir/$master/$file") {
	if (!($master = $masterport{$master})) {
	    bsd:errx(1, "$port has no $file");
	}
    }
    return "$portsdir/$master/$file";
}

#
# Show port info
#
sub show_port_info($) {
    my $port = shift;		# Port to show info for

    local *FILE;		# File handle
    my $info;			# Port info

    sysopen(FILE, find_port_file($port, "pkg-descr"), O_RDONLY)
	or bsd::err(1, "can't read description for $port");
    $info = join("| ", <FILE>);
    close(FILE);
    print("+--- Description for $port ($pkgname{$port}):\n| ${info}+---\n");
}

#
# Show port's website URL
#
sub show_port_website($) {
    my $port = shift;		# Port to show info for

    local *FILE;		# File handle
    my $website;		# Port's website

    sysopen(FILE, find_port_file($port, "pkg-descr"), O_RDONLY)
	or bsd::err(1, "can't read description for $port");
    while (<FILE>) {
	if (m/^WWW:\s*(\S+)\s*$/) {
	    $website = $1;
	}
    }
    close(FILE);
    if (!defined($website)) {
	bsd::warnx("No website for $port");
    } else {
	print("$website\n");
    }
}

#
# Show port plist
#
sub show_port_plist($) {
    my $port = shift;		# Port to show plist for

    my $master;			# Master port
    local *FILE;		# File handle
    my $file;			# File name
    my %files;			# Files to list
    my $prefix;			# Prefix

    $prefix = capture(\&make, ($port, "-VPREFIX"));
    chomp($prefix);
    sysopen(FILE, find_port_file($port, "pkg-plist"), O_RDONLY)
	or bsd::err(1, "can't read packing list for $port");
    while (<FILE>) {
	chomp();
	$file = undef;
	if (m/^[^\@]/) {
	    $file = $_;
	} elsif (m/^\@cwd\s+(\S+)\s*$/) {
	    $prefix = $1;
	} elsif (m/^\@dirrm\s+(\S+)\s*$/) {
	    $file = "$1/";
	} elsif (m/^\@comment\s+/) {
	    # ignore
	} elsif (m/^\@(un)?exec\s+/) {
	    # ignore
	} else {
	    bsd::warnx("unrecognized plist directive: %s", $_);
	}
	if (defined($file)) {
	    if ($file !~ m/^\//) {
		$file = "$prefix/$file";
	    }
	    $file =~ s|/+|/|g;
	    $files{$file} = 1;
	}
    }
    close(FILE);
    # XXX list man pages?
    print("+--- Packing list for $port ($pkgname{$port}):\n");
    foreach (sort(keys(%files))) {
	print("| $_\n");
    }
    print("+---\n");
}

#
# Compare two package names to determine which is newer
#
sub cmp_version($$) {
    my $inst = shift;		# Installed package
    my $port = shift;		# Origin port

    my $tree;			# Version in tree

    # Shortcut
    if (($tree = $pkgname{$port}) eq $inst) {
	return '=';
    }

    # Compare port epochs
    $inst =~ s/,(\d+)$//
	and $a = $1;
    $tree =~ s/,(\d+)$//
	and $b = $1;
    if (defined($a) || defined($b)) {
	$a = int($a || 0);
	$b = int($b || 0);
	if ($a != $b) {
	    return ($a > $b) ? '>' : '<';
	}
    }

    # Split it into components
    my @a = split(/[\._-]/, $inst);
    my @b = split(/[\._-]/, $tree);

    # Compare the components one by one
    while (@a && @b) {
	($a, $b) = (shift(@a), shift(@b));
	next if $a eq $b;
	if ($a =~ m/^\d+$/ && $b =~ m/^\d+$/) {
	    return ($a > $b) ? '>' : '<';
	}
	return ($a gt $b) ? '>' : '<';
    }

    # Anything left?
    if (@a) {
	return '>';
    } elsif (@b) {
	return '<';
    }
    return '=';
}

#
# List installed ports
#
sub list_installed() {

    my $pkg;			# Installed package
    my $origin;			# Origin
    my $cmp;			# Comparator

    foreach $pkg (sort(keys(%installed))) {
	$origin = $installed{$pkg};
	if (!defined($origin) || !defined($pkgname{$origin})) {
	    print(" ? $pkg\n");
	} else {
	    $cmp = cmp_version($pkg, $origin);
	    if ($cmp eq '=') {
		print("   $pkg\n");
	    } else {
		printf(" $cmp $pkg ($pkgname{$origin})\n");
	    }
	}
    }
}

#
# Clean a port
#
sub clean_port($) {
    my $port = shift;		# Port to clean

    make($port, "clean")
	or bsd::warnx("failed to clean %s", $port);
}

#
# Clean the tree
#
sub clean_tree() {

    my $port;			# Port name

    # We could just cd to $portsdir and 'make clean', but it'd
    # be extremely noisy due to only having a partial tree
    foreach $port (keys(%ports)) {
	if (-d "$portsdir/$port") {
	    make($port, "clean", "NO_DEPENDS=yes")
		or bsd::warnx("failed to clean %s", $port);
	}
    }
}

#
# Fetch a port
#
sub fetch_port($) {
    my $port = shift;		# Port to fetch

    make($port, "fetch")
	or bsd::errx(1, "failed to fetch %s", $port);
}

#
# Build a port
#
sub build_port($) {
    my $port = shift;		# Port to build

    my @makeargs;		# Arguments to make()

    if ($packages) {
	push(@makeargs, "package");
	push(@makeargs, "DEPENDS_TARGET=package clean", "-DNOCLEANDEPENDS")
	    unless ($dontclean);
    } else {
	push(@makeargs, "install");
	push(@makeargs, "DEPENDS_TARGET=install clean", "-DNOCLEANDEPENDS")
	    unless ($dontclean);
    }
    if ($force) {
	push(@makeargs, "-DFORCE_PKG_REGISTER");
    }
    if (!$dontclean) {
	push(@makeargs, "clean");
    }
    make($port, @makeargs)
	or bsd::errx(1, "failed to %s %s", $packages ? "package" : "build", $port);
}

#
# Print usage message and exit
#
sub usage() {

    stderr("Usage: porteasy [-abCceFfhIikLlsuVvw] [-D date] [-d dir]\n" .
	   "    [-p dir] [-r dir] [-t tag] [port ...]\n");
    exit(1);
}

#
# Print version
#
sub version() {

    stderr("This is porteasy $VERSION.
$COPYRIGHT
");
    exit(1);
}

#
# Print help text
#
sub help() {

    stderr("This is porteasy $VERSION.
$COPYRIGHT

Options:
  -a, --anoncvs            Use the FreeBSD project's anoncvs server
  -b, --build              Build required ports
  -C, --dontclean          Don't clean after build
  -c, --clean              Clean the specified ports
  -e, --exclude-installed  Exclude installed ports
  -F, --force-pkg-register Force package registration
  -f, --fetch              Fetch distfiles
  -h, --help               Show this information
  -I, --installed          Select installed ports
  -i, --info               Show info about specified ports
  -k, --packages           Build packages for the specified ports
  -L, --plist              Show the packing lists for the specified ports
  -l, --list               List required ports and their dependencies
  -s, --status             List installed ports and their status
  -u, --update             Update relevant portions of the ports tree
  -V, --version            Show version number
  -v, --verbose            Verbose mode
  -w, --website            Show the URL to the port's website

Parameters:
  -D, --date=DATE          Specify CVS date
  -d, --dbdir=DIR          Specify package directory (default $dbdir)
  -p, --portsdir=DIR       Specify ports directory (default $portsdir)
  -r, --cvsroot=DIR        Specify CVS root
  -t, --tag=TAG            Specify CVS tag

Report bugs to <des\@freebsd.org>.
");
    exit(1);
}

MAIN:{
    my $port;			# Port name
    my $err = 0;		# Error count

    # Show usage if no arguments were specified on the command line
    if (!@ARGV) {
	usage();
    }

    # Get option defaults
    if ($ENV{'PORTEASY_OPTIONS'}) {
	foreach (split(' ', $ENV{'PORTEASY_OPTIONS'})) {
	    unshift(@ARGV, $_);
	}
    }

    # Scan command line options
    Getopt::Long::Configure("auto_abbrev", "bundling");
    GetOptions(
	       "a|anoncvs"		=> \$anoncvs,
	       "b|build"		=> \$build,
	       "c|clean"		=> \$clean,
	       "C|dontclean"		=> \$dontclean,
	       "D|date=s"		=> \$date,
	       "d|dbdir=s"		=> \$dbdir,
	       "e|exclude-installed"	=> \$exclude,
	       "F|force-pkg-register"	=> \$force,
	       "f|fetch"		=> \$fetch,
	       "h|help"			=> \&help,
	       "I|installed"		=> \$installed,
	       "i|info"			=> \$info,
	       "k|packages"		=> \$packages,
	       "L|plist"		=> \$plist,
	       "l|list"			=> \$list,
	       "p|portsdir=s"		=> \$portsdir,
	       "r|cvsroot=s"		=> \$cvsroot,
	       "s|status"		=> \$status,
	       "t|tag=s"		=> \$tag,
	       "u|update"		=> \$update,
	       "V|version"		=> \&version,
	       "v|verbose"		=> \$verbose,
	       "w|website"		=> \$website,
	       "x|ecks"			=> \&ecks,
	       )
	or usage();

    if (!@ARGV &&
	($build || $fetch || $list || $packages || $plist || $website)) {
	usage();
    }

    if ($portsdir !~ m/^\//) {
	$portsdir = `pwd` . $portsdir;
	$portsdir =~ s/\n/\//s;
    }

    if ($portsdir !~ m/\/ports\/?$/) {
	bsd::errx(1, "ports directory must be named 'ports'");
    }

    $index = "$portsdir/INDEX";

    # 'package' implies 'build'
    if ($packages) {
	$build = 1;
    }

    # 'status' implies 'installed'
    if ($status) {
	$installed = 1;
    }

    # Set and check CVS root
    if ($anoncvs && !$cvsroot) {
	$cvsroot = &ANONCVS_ROOT;
	if (-f &CVS_PASSFILE) {
	    $ENV{'CVS_PASSFILE'} = &CVS_PASSFILE;
	}
    }
    if (!$cvsroot) {
	$cvsroot = $ENV{'CVSROOT'};
    }
    if ($update && !$cvsroot) {
	bsd::errx(1, "No CVS root, please use the -r option or set \$CVSROOT");
    }

    # Step 1: update the ports index
    update_index();

    # Step 2: build list of explicitly required ports
    foreach $port (@ARGV) {
	$err += add_port($port, &REQ_EXPLICIT);
    }
    if ($err) {
	bsd::errx(1, "some required ports were not found.");
    }
    if ($installed) {
	add_installed();
    }

    # Step 3: update port directories and discover dependencies
    $need_deps = ($update || $fetch || $list);
    update_ports_tree(keys(%reqd));

    # Step 4: deselect ports which are already installed
    if ($exclude) {
	foreach $port (keys(%reqd)) {
	    if ((exists($installed{$port}) && $installed{$port} > 0) ||
		-d "$dbdir/$pkgname{$port}") {
		info("$port is already installed");
		delete $reqd{$port};
	    }
	}
    }

    # Step 5: list selected ports
    if ($list) {
	foreach $port (sort(keys(%reqd))) {
	    next if ($reqd{$port} == &REQ_MASTER);
	    print((($reqd{$port} & &REQ_EXPLICIT) ? " * " : "   "),
		  "$port ($pkgname{$port})\n");
	}
    }

    # Step 6: list installed ports
    if ($status) {
	list_installed();
    }

    # Step 7: show info
    if ($info) {
	foreach $port (keys(%reqd)) {
	    if ($reqd{$port} & &REQ_EXPLICIT) {
		show_port_info($port);
	    }
	}
    }

    # Step 8: show packing list
    if ($plist) {
	foreach $port (keys(%reqd)) {
	    if ($reqd{$port} & &REQ_EXPLICIT) {
		show_port_plist($port);
	    }
	}
    }

    # Step 9: show website URL
    if ($website) {
	foreach $port (keys(%reqd)) {
	    if ($reqd{$port} & &REQ_EXPLICIT) {
		show_port_website($port);
	    }
	}
    }

    # Step A: clean the ports directories (or the entire tree)
    if ($clean) {
	if (!@ARGV) {
	    clean_tree();
	} else {
	    foreach $port (keys(%reqd)) {
		if ($reqd{$port} & &REQ_EXPLICIT) {
		    clean_port($port);
		}
	    }
	}
    }

    # Step B: fetch distfiles
    if ($fetch) {
	foreach $port (keys(%reqd)) {
	    if ($reqd{$port} != &REQ_MASTER) {
		fetch_port($port);
	    }
	}
    }

    # Step C: build ports - only the explicitly required ones, since
    # some dependencies (most commonly XFree86) may be bogus.
    if ($build || $packages) {
	foreach $port (keys(%reqd)) {
	    if ($reqd{$port} & &REQ_EXPLICIT) {
		build_port($port);
	    }
	}
    }

    # Done!
    exit(0);
}
