#!/usr/bin/perl
#
# Update/install packages.
# Usage: packager [options] mode package-name...
# where mode is one of "check", "download", "install", "reinstall",
# "upgrade", "Clean", or "Zap", or the initial letters "c", "d", "i", "r",
# "u", "C", or "Z" (all case sensitive).
# Options:
#    -a: Tells the program not to ask anything interactively (see below for
#        details).
#    -A: Like -a, but causes "suggested" questions to be answered with Y as
#        well.
#    -g: Causes all executables to be built with debugging information and
#        no optimization.  Equivalent to "-o debug".  Overrides -s (or its
#        equivalent "-o striplib").
#    -l logdir: Specifies directory (which must exist) for logging make
#        activity (default is $TMPDIR/log.packager-$$).  The logfile will
#        be stored in the file "log" in this directory.  A log file is not
#        created if the make succeeds.
#    -n: Like -a, but assumes an answer of N for everything and skips
#        packages that cannot be processed as-is.
#    -o option[=value]: Passes the given option to the package's
#        download/installation rules (see below).  "value" defaults to 1 if
#        not given.  If the first character of "option" is a "!", then the
#        variable is unset if previously set (by an earlier -o option or
#        previous use of the -O/-S options).
#    -s: Specifies that all libraries should be stripped of debugging
#        information, and debugging libraries (e.g. lib*_g.a) should not be
#        installed.  (Binaries are always stripped.)  Equivalent to
#        "-o striplib".
#    -t tmpdir: Specifies that all files should be placed in `tmpdir'
#        (which must exist) rather than creating a new temporary directory.
#        The directory will not be deleted when the program exits.
#    -u: When installing a package that requires or suggests other packages
#        which are not installed, this option indicates that the program
#        should always check for a new version of each package and download
#        it if found.  Normally, no check is done unless the package has
#        not been downloaded at all.
#    -v: Causes download/install operations to be verbose (i.e. print out
#        all "make" output).
#    -w: Sets timeout ("wait time") for network connections.
#    -D: Causes download operations to delete any old versions of the
#        source tarball present in the source directory after successfully
#        downloading a new version.
#    -O option[=value]: Like -o, but causes the variable and its value to
#        be retained for future invocations of "packager" on the same
#        package(s).  If the first character of "option" is a "!", then the
#        option is unset regardless of system-wide options (-S); if the
#        first character is a "-", then the option itself is removed from
#        the list of package-specific permanent options, and the systemwide
#        option value, if any, is applied.
#    -P: Causes configuration and log files to be removed as well when
#        removing (Zapping) a package.
#    -S option[=value]: Like -O, but the variable and value are applied to
#        all future invocations of "packager" regardless of package, except
#        where overridden by -O for a specific package.
#    -T: Forces recreation of the repack/install timing data used to
#        display time remaining in non-verbose mode (this is done
#        automatically if timing data does not yet exist).
#    -Z: Enables zap-before-install mode for packages that request it.
#    -3: Causes code to be generated that will work on all i386-and-later
#        processors.
#
# If "ALL" is specified as a package name, the given action will be
# performed on all installed packages (however, "check" and "Zap" are not
# allowed and will exit with code 1).  When used with "ALL", packages that
# lack the requisite data for a particular operation, as described below,
# will be skipped over; in the case of "upgrade", if a package does not
# have Source: and Repack: lines but does have source downloaded, the
# upgrade will proceed normally.  Additionally, specifying "-package" (a
# minus sign followed by a package name) after "ALL" will cause the named
# packages to be skipped over.
#
# For "check", check whether the package is installed on the system, and
# exit with code 0 if so, 2 if not.  Note that only the first package given
# on the command line is checked.
#
# For "download", attempt to download the most recent version of the
# package's source (as specified in the Source: line in the package data
# file, see below) into /usr/src/package-version.tar.gz.  If a new source
# set is successfully downloaded and the package data file contains a
# Repack: rule, then that rule is executed.  The exit code is 0 if a new
# version was successfully downloaded or if the current version in /usr/src
# is up to date, 2 if an error occurred while downloading, or 3 if an error
# occurred while executing the Repack: rule.
# Versions are considered as dot-separated sequences of values, and each
# value is compared in sequence as follows:
#    - Numeric comparison; a larger value is newer than a smaller value.
#    - If one version has a subversion (subsequent value) but the other
#      does not, the one with the subversion is newer.  For example,
#      "2.1.0" is considered newer than "2.1rc2", and "2.2" is newer than
#      both.
#    - Comparison of all characters in the value following any initial
#      digits, using the following rules (where NN is any number, and "<"
#      means "older than"):
#          "aNN..." < "bNN..." < "preNN..." < "rcNN..." < "" < anything else
#      If both values are "aNN", "bNN", "preNN", or "cNN", the numeric part
#      of the value is compared numerically (i.e. "pre1" < "pre9" < "pre10"),
#      then compared using a string comparison if they are numerically
#      equal (i.e. "pre1" < "pre1b" < "pre2").  If both values fall into
#      the "anything else" category, they are compared with a standard
#      string comparison ("A" < "Z" < "a" < "z").
# Normally, the program will prompt before downloading a new version; the
# "-a" option suppresses this prompt and causes new versions to be
# downloaded without user interaction (with "-n", the package will be
# skipped instead).  Additionally, the "-D" option can be used to remove
# any older versions of the source already downloaded.
#
# For "install", attempt to install the package from the currently
# downloaded source.  If the directory /usr/src/package-version (with
# "package" and "version" replaced by appropriate strings) does not exist,
# the source is extracted into that directory; then the Install: rule is
# executed; then, if the source was extracted, it is deleted.  The exit
# code is 0 on success, 2 on error.  If the same version of the package
# is already installed, the program exits without doing anything; if the
# source package is older than the installed version, the program prompts
# the user for whether to continue with the install.
#
# If the package requires other packages (as defined by a Requires: or
# Compile-Requires: line) which are not present, the program will ask the
# user whether the package(s) should be installed; if the user answers "y"
# (the default), the program performs an "upgrade" on those packages,
# otherwise it aborts.  If packages listed in a Suggested: line are not
# installed, the user is likewise prompted, but the default is "n", and the
# program continues normally even if the packages are not installed.  If
# "-a" is given on the command line, required packages are downloaded
# and/or installed if they have not been already.  (For "-A", suggested
# packages are installed as well; with "-n", packages with missing
# prerequisites will be skipped.)
#
# "reinstall" is identical to "install", with the exception that the
# install continues normally rather than exiting if the package has
# already been installed.
#
# For "upgrade", first a "download" is performed, then, if successful,
# an "install" is performed.  "packager upgrade package" is exactly
# equivalent to "packager download package && packager install package",
# with the exception that if the package information does not include a
# Source: specification and the package is already installed, the package
# is skipped rather than causing the program to terminate.
#
# For "Clean", remove any out-of-date installed files, such as files left
# over from previous versions.  If the package does not have a Clean: rule,
# nothing is done (but no error is raised).
#
# For "Zap", remove all package files from the system.  Configuration files
# and the like are left alone unless -P is specified, in which case they
# are removed as well.
#
# During an install or upgrade, the following happens.  Installation is
# aborted if an error occurs at any step.
#    - The source is extracted into the temporary directory used by this
#      program.
#    - If a Patch keyword is specified, the given patch is applied to the
#      source using "patch -p0".
#    - The Compile rule is run.
#    - The entire target directory ($(PREFIX)) is set to owner root.root,
#      mode a+rX.
#    - All executable ELF files in $(PREFIX)/{bin,libexec,sbin} are stripped.
#    - If -s was specified, all libraries in $(PREFIX)/lib are stripped.
#    - All .gz files in $(PREFIX)/info and $(PREFIX)/man (if they exist)
#      are deleted.
#    - All files in $(PREFIX)/info and $(PREFIX)/man are compressed with
#      gzip -9.
#    - The Install rule is run.
#
# If the program is given the wrong number of or invalid arguments, it
# exits with code 1.
#
# Package data files consist of information lines (in the format
# "Keyword: data", or "Keyword:" followed by tab-indented lines for the
# Unpack and Install keywords--see below), followed by a blank line,
# followed by a list of files the package installs on the system.  In the
# list of files, which must all be absolute pathnames, any pathnames with
# trailing slashes indicate that the entire directory is included.
#
# Recognized keywords are:
#
# Package-Name: REQUIRED.  Specifies the name of the package (used as the
# prefix for the source tarball filename and as the name of the install
# directory in /packages).  Must be the same as the name of the package
# description file.
#
# Requires: OPTIONAL.  Specifies packages which are required for this
# package to function properly.  Package names are separated by spaces;
# "one of any of these packages" is indicated using a vertical bar ("|").
# For example, "bash perl|python" means "bash AND (perl OR python)".
#
# Compile-Requires: OPTIONAL.  Specifies packages which are required to
# compile and/or install this package, but which are not required for the
# package itself to function.
#
# Suggests: OPTIONAL.  Specifies packages which enable added functionality
# or are otherwise beneficial to have installed when using this package.
#
# Rebuild-For: OPTIONAL.  Specifies packages which are not Suggested or
# Required, but which are used in some fashion by this package, and should
# cause this package to be recompiled if they are installed, uninstalled,
# or upgraded.  For example, packages which are automatically detected and
# linked against should be listed here.  (FIXME: not implemented yet)
#
# Homepage: OPTIONAL.  Specifies the URL of the package's home page.
#
# Source: OPTIONAL (but required for download).  Specifies the URL from
# which the package's source can be downloaded, and how to obtain the
# version number from the filename.  The data for this keyword is specified
# as "<urldir>/<file-regex> <subst-string> [<new-name>]", where:
#    <urldir> is the URL of the source file, up to the last directory name;
#    <file-regex> is a Perl regular expression (\d, \w, etc. allowed)
#        describing the filename--however, dots (.) are treated as literal
#        characters (use [^\n] for a regex dot);
#    <subst-string> is the string used as the second parameter to a
#        substitution (s///), where the first parameter is <file-regex>,
#        and should give the version number from the filename; and
#    <new-name> is a string used as the second parameter to a substitution,
#        where the first parameter is <file-regex>, that gives the name of
#        the file to download when it is a different file from that given
#        by <urldir>/<file-regex>.  For example, if an FTP site keeps
#        different versions of a program in different directories, one
#        could use:
#            Source: ftp://site/dir/(\d+.\d+(.\d+)?) $1 $1/package-$1.tar.gz
#        As a special exception, $0 is replaced with the entire version
#        string.
# If <subst-string> or <new-name> begin with the two characters "e:", then
# they are treated as expressions to be evaluated, as in s///e.
# NOTE: A URL of "uue://Source-Data/." will cause the source data to be
#       taken directly from the Source-Data keyword (see below).  In this
#       case the version number must be given explicitly.
#
# Source-Data: OPTIONAL.  Used with the uue:// pseudo-URL (see above) to
# include the source data directly in the package description file.  The
# name of this directive can be changed, as long as it matches the name
# given in the uue:// URL.  The first line should be "<<EOT" (or any other
# terminator line); all text up to the terminator line is taken as the
# uuencoded data.  "begin" and "end" lines are not required, and will be
# ignored if present.
#
# More-Source-N (where N is a number): OPTIONAL.  Specifies URLs of
# additional files to download.  $1-$9 can be used to refer to
# parenthesized subpatterns in the Source: file-regex, and $0 to the
# entire version string.
#
# Repack: OPTIONAL (but required for download/upgrade).  This keyword is
# followed by a set of tab-indented commands to run after downloading a
# new source set for a package to extract the source set into a directory
# with the name <package>-<version> under the current directory (a
# temporary directory which will be deleted when the program exits).  The
# following variables are set for this rule:
#    PACKAGE: The package's name.
#    PKGOPT_xxx: The value of option "xxx", as specified with the "-o" option.
#    PREFIX: The absolute path to where the program will eventually be
#       installed.
#    SOURCE: The absolute path to the downloaded file.  If this file still
#       exists when the rule completes, it will be deleted.
#    SOURCE<N>: The absolute path to each of the More-Source-<N> files.
#       SOURCE1 corresponds to More-Source-1, SOURCE2 to More-Source-2, etc.
#    VERSION: The package version.
#
# Patch: OPTIONAL.  If present, the following text is passed to "patch -p0"
#    immediately before the Compile rule is run.  The first line should be
#    "<<EOT" (or any other terminator line); all text up to the terminator
#    line is taken as the patch.
#
# Zap-Before-Install: OPTIONAL.  If present with a non-empty value other
# than "0", a Zap operation is performed on the package before installation
# or reinstallation; this Zap operation ignores package dependencies,
# forcing removal of all package files (except configuration files).
#
# Compile-In-Place: OPTIONAL.  If present with a non-empty value other than
# "0", the tarball is extracted into /usr/src instead of into the temporary
# directory, and is not extracted if the source directory already exists;
# the source directory is not deleted on completion (whether successful or
# not).
#
# No-Chown: OPTIONAL.  If present with a non-empty value other than "0",
# the package directory will not be chown'd to root.root or chmod'd +rX
# before the Install rule is run.
#
# Interactive-Compile: OPTIONAL.  If present with a non-empty value other
# than "0", compilation will always be executed interactively (i.e., as if
# the "-v" option was given).
#
# Explicit-Only: OPTIONAL.  If present with a non-empty value other than
# "0", the package will not be affected by any operations performed on all
# packages (by use of the package name "ALL").
#
# Compile: OPTIONAL (but required for install/upgrade).  This keyword is
# followed by a set of tab-indented commands run in order to install the
# package into its home in the /packages directory.  The PREFIX, VERSION,
# and PKGOPT_* variables are set as with the Repack rule above;
# additionally, the variable GCC_OPT_FLAGS is set to optimization flags
# that should be passed to GCC for maximum opimization, and the variable
# PACKAGER_TMPDIR contains the absolute path to the temporary directory
# used by this program.  This rule may recursively call "make" (using
# "$(MAKE) -f $(PACKAGER_TMPDIR)/Makefile", or ../Makefile if
# Compile-In-Place is not set, as the command name) with the following
# predefined target:
#    configure: Runs $(CONFIGURE) ("./configure" if not set) with
#       --prefix="$(PREFIX)" and $(CONFIGURE_ARGS), and with the
#       environment variables OPT, CFLAGS, CXXFLAGS, and LDFLAGS set to:
#                OPT="-O3 $(GCC_OPT_FLAGS) $(CUSTOM_OPT)"
#             CFLAGS="-O3 $(GCC_OPT_FLAGS) $(CUSTOM_OPT) $(CUSTOM_CFLAGS)"
#           CXXFLAGS="-O3 $(GCC_OPT_FLAGS) $(CUSTOM_OPT) $(CUSTOM_CFLAGS) $(CUSTOM_CXXFLAGS)"
#            LDFLAGS="$(CUSTOM_LDFLAGS)
#       $(CUSTOM_OPT), $(CUSTOM_CFLAGS), $(CUSTOM_CXXFLAGS), and
#       $(CUSTOM_LDFLAGS) default to empty, and can be set by the caller as
#       needed.  Additionally, the following package options cause
#       additional options to be passed to ./configure:
#          Option  Value  ./configure Option
#          ------  -----  ------------------
#          nonls   (any)  --disable-nls
#          shared    0    --disable-shared
#                    1    --enable-shared
#          static    0    --disable-static
#                    1    --enable-static
#
# Install: OPTIONAL (but required for install/upgrade).  This keyword is
# followed by a set of tab-indented commands to run in order to install the
# package on the system (i.e. links into /usr, etc.).  The current
# directory is set to the top-level temporary directory (where this
# Makefile resides); variables are set as with the Compile rule.  The
# command may recursively call "make" (using $(MAKE) as the command name)
# with any of the following predefined targets:
#    instbin: Makes symbolic links in /usr/bin to all files specified by
#       $(PREFIX)/bin/$(BIN) (where $(BIN) is a space-separated list of
#       filenames).  Wildcards may be used in $(BIN).
#    instsbin: Likewise, but uses /usr/sbin and $(PREFIX)/sbin/$(SBIN).
#    instlib: Makes symbolic links in /usr/lib to
#       $(PREFIX)/lib/lib$(LIB).{a,la,so,so.?,so.??} for each library name
#       in $(LIB).  Wildcards may NOT be used.
#    addldso: Adds "$(PREFIX)/lib" to /etc/ld.so.conf.  This path can be
#       changed by specifying $(LDSOPATH) in the make invocation.
#    instpc: Creates symbolic links to $(PREFIX)/lib/pkgconfig/$(PC).pc
#       from /usr/lib/pkgconfig.  Wildcards may be used.
#    instaclocal: Creates symbolic links to $(PREFIX)/share/aclocal/$(M4).m4
#       from /usr/share/aclocal.  Wildcards may be used.
#    instetc: Installs files in $(PREFIX)/etc/$(DIR) to /etc/$(DEST)/$(DIR).
#       For files in $(ETC), the source file is copied to the destination
#       directory if it does not exist there, and replaced in the source
#       directory with a symbolic link to the file in the destination
#       directory.  For files in $(LINK), a symbolic link to the file in
#       the source directory is created in the destination directory.
#    instinc: Creates directory /usr/include/$(DIR) if it does not exist
#       (DIR may be empty), and creates links in that directory to
#       $(PREFIX)/include/$(DIR)/$(INC) for each file in $(INC).  Leading
#       pathnames on files in $(INC) are stripped; so, for example,
#       "DIR=dir INC=file.h" links /usr/include/dir/file.h to
#       $(PREFIX)/include/dir/file.h, while "INC=dir/file.h" (with no DIR=)
#       makes a link to the same file as /usr/include/file.h.  Directories
#       may also be specified in $(INC).  Wildcards may be used.
#    instinit: Creates a symbolic link to $(PREFIX)/$(SRC) from
#       /etc/rc.d/init.d/$(DEST).  If $(S) or $(K) are not empty, they
#       should contain the runlevel and priority in the format "r/pp" at
#       which to install the script; if an active ([SK]*) or inactive
#       ([sk]*) link for $(DEST) does not already exist in the appropriate
#       rc.N directory, one will be created (active).  If $(SRC) is not
#       specified, /etc/rc.d/init.d/generic-daemon is used instead.
#    instlocale: Creates symbolic links to
#       $(PREFIX)/share/locale/*/LC_MESSAGES/$(FILE).mo from the respective
#       directories in /usr/share/locale.  Only one filename may be
#       specified for $(FILE), and wildcards may NOT be used.  However, if
#       the "nonls" package option is set (with "-o nonls"), instead
#       removes all files matching
#       {$(PREFIX),/usr}/share/locale/*/LC_MESSAGES/$(FILE).mo as well as
#       any subsequently empty parent directories (up to .../share).
#    instinfo: Creates symlinks to the files specified by $(INFO) (the
#       suffix .info*.gz is appended to each filename) in /usr/share/info.
#       Wildcards may NOT be used.
#    instman: Creates symlinks to all files in $(PREFIX)/man/man$(SECTION)
#       specified by $(MAN) (the suffix .$(SECTION)$(SUFFIX).gz is appended
#       to each filename) in /usr/share/man/man$(SECTION).  Wildcards may
#       be used.
#    instfont-ttf: Creates symlinks in /usr/share/fonts/TTF to all files in
#       $(PREFIX)/$(DIR) specified by $(FONTS), after removing all entries
#       in /usr/share/fonts/TTF starting with each word in $(FONTS).
#    instfont-x11: As instfont-ttf, but creates symlinks in
#       /usr/share/fonts/X11.  Also, if an X11 package is installed, runs
#       mkfontdir in that directory after creating the symlinks.
#
# Post-Install: OPTIONAL.  If this rule is defined, it will be executed
# after a successful install; the difference between this rule and the
# Install rule is that this rule will always be executed on the user's
# terminal, and is not considered part of the installation time.  This is
# useful for packages that require user interaction to be installed
# completely.
#
# Clean: OPTIONAL.  If this rule is defined, it will be executed when a
# Clean operation is performed on the package.

# TODO:
# * allow for things like /pub/gtk/vX.Y/gtk+-X.Y.Z.tar.gz (also glib, atk,
#   pango)
# - have separate install targets for binary and documentation (i.e.
#   essential stuff vs frills)
#   (for upgrade, only installs packages which were downloaded); also want
#   -v to turn on per-package status notices (or -q to turn them off)
# - remember which version is _installed_ (e.g. in an upgrade, download
#   succeeds but install fails)
# - make exit codes work like we say they do
# - when installing, make sure package's list of files is correct (needs
#   chroot jail?)
# - need Zap: rule (e.g. to remove ld.so.conf entries)
# - handle _g libraries (e.g. ncurses)
# - handle [] wildcards (e.g. glibc)
# - time out on downloads as needed
# - add option to not abort on first failure (e.g. for "cd /pkg;packager u *")
# - allow alternate source paths (e.g. ftp+http, try the second if the
#   first fails because of e.g. too many users logged on)
# - ntp breaks with HTTP_PROXY set
# - need a way to insert $(PREFIX) in Patch: text (e.g. xanim)
# - also need a way to insert $(PREFIX) in file list
# - using $(PREFIX)/.pkgopt causes options to be lost if pkg is uninstalled

###########################################################################
###########################################################################

$INSTDIR = "/pkg";			# directory for installation
$PKGDIR = "$INSTDIR/packager/packages";	# directory with package data
$SRCDIR = "/usr/src";			# directory with source tarballs

$FTPPASS = "achurch\@achurch.org";

###########################################################################

require "$INSTDIR/packager/lib/cvs.pl";
require "$INSTDIR/packager/lib/ftp.pl";
require "$INSTDIR/packager/lib/http.pl";
require "$INSTDIR/packager/lib/make.pl";
require "$INSTDIR/packager/lib/status.pl";
require "$INSTDIR/packager/lib/svn.pl";
require "$INSTDIR/packager/lib/util.pl";

###########################################################################
###########################################################################

my @ARGO = ();		# list of argument options (-o/-O/-S)
my @packages = ();	# list of all packages to process
my %modified = ();	# set of packages modified during this run
my $mode = "";		# set to first letter of mode (cdiruCZ)

$tmpdir = "";		# directory where we dump temporary stuff
$logfile = "";		# place (filename) to put logfile if needed
			#     (global so make() can access it)

$allpkg = 0;		# are we doing all packages?
@failed = ();		# list of packages that failed, when doing all

$dont_ask = 0;		# -a: don't ask anything (if 2 [-A], Suggest: gets Y;
			#     if -1 [-n], everything gets N)
$my_logdir = "";	# -l: user-specified logdir
%options = ();		# -o: package options
$my_tmpdir = "";	# -t: user-specified tmpdir
$always_upgrade = 0;	# -u: always upgrade prerequisites
$verbose = 0;		# -v: verbose
$waittime = 0;		# -w: timeout for network connections
$delete_old = 0;	# -D: delete old source tarballs
%pkg_options = ();	# -O: package-specific permanent options
$purge = 0;		# -P: purge configuration/log files
%sys_options = ();	# -O: system-wide permanent options
$do_timing = 0;		# -T: do timing of repack/install
$gen_i386 = 0;		# -3: generate i386-compatible code

###########################################################################

# Information about each supported protocol.  Each protocol requires five
# support functions:
#    - open(host,port,pkgdata): [`pkgdata' is a reference to the hash]
#                          Connects to the given host and port; returns
#                              1 on success, 0 on failure.
#    - dir(path,filemask): Returns an array of all files in directory
#                              `path' matching regexp `filemask'.
#    - size(fullpath):     Returns the size of the file `fullpath', -1 if
#                              unknown, or undef on failure.
#    - get(fullpath,cb):   Returns the data contained in file `fullpath'
#                              as a string reference, or undef on failure.
#                              `cb' is a subroutine reference to be called
#                              periodically, e.g. after each read()/recv().
#                              The subroutine takes one parameter: the
#                              number of additional bytes read since the
#                              last call.
#    - close():            Closes the currently open connection.
# No more than one connection will ever be opened at once.
#
# Note that the routines can be anonymous subroutines as well as
# subroutine references, as in the "sourceforge://" entry below.
# (sourceforge:///project/file.tar.gz)
#
# For the HTTP protocol, given a URL "http://host[:port]/path/file.html",
# specify the source URL as:
#     http://host[:port]/path/file.html/regex/[\0-\377]*file-(version).tar.gz
# Here, "regex" is a regular expression indicating what text in the given
# page indicates a potential download candidate, and "file-(version).tar.gz"
# is the filename regex as required by the Source: line format (e.g.
# "foo-(\d+.\d+).tar.gz")--make sure to include the [\0-\377]* in front, to
# strip off information used by the protocol handler.  Note that "regex"
# cannot contain slashes; use \x2F instead.

%protocols = (
	      ftp => {
		  open  => \&ftp_open,
		  dir   => \&ftp_dir,
		  size  => \&ftp_size,
		  get   => \&ftp_get,
		  close => \&ftp_close
	      },
	      sourceforge => {
	          open  => sub { 0&& ($sf_path="/pub/sourceforge", &ftp_open("jaist.dl.sourceforge.net",21,$_[2]))
			      || ($sf_path="", &ftp_open("nchc.dl.sourceforge.net",21,$_[2]))
			      || ($sf_path="/pub/sourceforge", &ftp_open("umn.dl.sourceforge.net",21,$_[2]))
			      || ($sf_path="/pub/sourceforge", &ftp_open("unc.dl.sourceforge.net",21,$_[2]))
			      || 0&& &ftp_open("belnet.dl.sourceforge.net",21,$_[2]); },
		  dir   => sub { &ftp_dir($sf_path.substr($_[0],0,2).substr($_[0],0,3).$_[0],$_[1]); },
		  size  => sub { &ftp_size($sf_path.substr($_[0],0,2).substr($_[0],0,3).$_[0]); },
		  get   => sub { &ftp_get($sf_path.substr($_[0],0,2).substr($_[0],0,3).$_[0],$_[1]); },
		  close => \&ftp_close
	      },
	      gnu => {
	          open  => sub { 0&& ($gnu_path="/pub/GNU", &ftp_open("core.ring.gr.jp",21,$_[2]))  # data transfers hang for some reason, depending on IP
			      || ($gnu_path="/pub/GNU", &ftp_open("ftp.ring.gr.jp",21,$_[2]))   # data transfers hang for some reason, depending on IP
			      || ($gnu_path="/pub/GNU", &ftp_open("ftp.iij.ad.jp",21,$_[2]))
			      || ($gnu_path="/pub/GNU/gnu", &ftp_open("ftp.nara.wide.ad.jp",21,$_[2]))
			      || ($gnu_path="/pub/gnu", &ftp_open("ftp.keystealth.org",21,$_[2]))
			      || ($gnu_path="/pub/gnu", &ftp_open("ftp.gnu.org",21,$_[2])); },
		  dir   => sub { &ftp_dir("$gnu_path$_[0]",$_[1]); },
		  size  => sub { &ftp_size("$gnu_path$_[0]"); },
		  get   => sub { &ftp_get("$gnu_path$_[0]",$_[1]); },
		  close => \&ftp_close
	      },
	      ibiblio => {
	          open  => sub { &ftp_open("ftp.ibiblio.org",21,$_[2]); },
		  dir   => sub { &ftp_dir($_[0],$_[1]); },
		  size  => sub { &ftp_size($_[0]); },
		  get   => sub { &ftp_get($_[0],$_[1]); },
		  close => \&ftp_close
	      },
	      ibibliolinux => {
	          open  => sub { ($ibibliolinux_path = "/pub/linux", &ftp_open("ftp.ibiblio.org",21,$_[2]))
			      || ($ibibliolinux_path = "/pub/ibiblio", &ftp_open("planetmirror.com",21,$_[2])); },
		  dir   => sub { &ftp_dir("$ibibliolinux_path$_[0]",$_[1]); },
		  size  => sub { &ftp_size("$ibibliolinux_path$_[0]"); },
		  get   => sub { &ftp_get("$ibibliolinux_path$_[0]",$_[1]); },
		  close => \&ftp_close
	      },
	      gentoo => {
	          open  => sub { ($gentoo_path = "/GENTOO", &ftp_open("ftp.ecc.u-tokyo.ac.jp",21,$_[2]))
			      || ($gentoo_path = "/pub/gentoo", &ftp_open("ftp.oregonstate.edu",21,$_[2]))
			      || ($gentoo_path = "/pub/linux/gentoo", &ftp_open("gg3.net",21,$_[2])) },
		  dir   => sub { &ftp_dir("$gentoo_path$_[0]",$_[1]); },
		  size  => sub { &ftp_size("$gentoo_path$_[0]"); },
		  get   => sub { &ftp_get("$gentoo_path$_[0]",$_[1]); },
		  close => \&ftp_close
	      },
	      cpan => {
	          open  => sub { ($CPAN_path="/pub/CPAN",&ftp_open("ftp.jaist.ac.jp",21,$_[2]))
			      || 0&& ($CPAN_path="/pub/CPAN",&ftp_open("ftp.ayamura.org",21,$_[2]))
			      || ($CPAN_path="/CPAN",&ftp_open("ftp.kddlabs.co.jp",21,$_[2]))
			      || ($CPAN_path="/pub/CPAN",&ftp_open("ftp.u-aizu.ac.jp",21,$_[2]))
			      || ($CPAN_path="/pub/CPAN",&ftp_open("ftp.cpan.org",21,$_[2])); },
		  dir   => sub { &ftp_dir("$CPAN_path$_[0]",$_[1]); },
		  size  => sub { &ftp_size("$CPAN_path$_[0]"); },
		  get   => sub { &ftp_get("$CPAN_path$_[0]",$_[1]); },
		  close => \&ftp_close
	      },
	      http => {
		  open  => \&http_open,
		  dir   => \&http_dir,
		  size  => \&http_size,
		  get   => \&http_get,
		  close => \&http_close
	      },
	      cvs => {
		  # specify source URL as:
		  #   cvs://username[:password]@pserver/cvsroot/module/([\0-\377]*)
		  # datestamp (YYYYMMDD.hhmm) returned in $1 for version number
		  # get() returns a tar.gz
		  open  => \&cvs_open,
		  dir   => sub { my @tm = localtime(time());
				 return sprintf("%04d%02d%02d.%02d%02d",
						$tm[5]+1900, $tm[4]+1,
						$tm[3], $tm[2], $tm[1]); },
		  size  => sub { return -1; },
		  get   => \&cvs_get,
		  close => \&cvs_close
	      },
	      svn => {
		  # specify source http[s]://host/path as:
		  #   svn://http[s]/host/path[@build]/dirname/([\0-\377]*)
		  # datestamp (YYYYMMDD.hhmm) returned in $1 for version number
		  # get() returns a tar.gz
		  open  => \&svn_open,
		  dir   => sub { my @tm = localtime(time());
				 return sprintf("%04d%02d%02d.%02d%02d",
						$tm[5]+1900, $tm[4]+1,
						$tm[3], $tm[2], $tm[1]); },
		  size  => sub { return -1; },
		  get   => \&svn_get,
		  close => \&svn_close
	      },
	      uue => {
		  open  => sub { $uue_decoded = 0;
				 return defined($uuedata = ${$_[2]}{$_[0]}); },
		  dir   => sub { return (""); },
		  size  => sub { if (!$uue_decoded) {
				     $uuedata =~ s/(^|\n)begin.*\n/$1/;
				     $uuedata =~ s/(^|\n)end.*\n/$1/;
				     $uuedata = unpack("u",$uuedata);
				     $uue_decoded = 1;
				 }
				 return length($uuedata); },
		  get   => sub { if (!$uue_decoded) {
				     $uuedata =~ s/(^|\n)begin.*\n/$1/;
				     $uuedata =~ s/(^|\n)end.*\n/$1/;
				     $uuedata = unpack("u",$uuedata);
				     $uue_decoded = 1;
				 }
				 &{$_[1]}(length($uuedata));
				 return \$uuedata;
			       },
		  close => sub { $uuedata = undef; }
	      },
);

###########################################################################
###########################################################################

&init();
foreach $package (@packages) {
    if (!&handle_package($package, $mode)) {
	if ($allpkg) {
	    print STDERR "*** packager: Package $package failed\n";
	    push @failed, $package;
	} else {
	    exit 1;
	}
    }
}
if (@failed) {
    print STDERR "*** packager: Failed packages: ".join(" ",@failed)."\n";
    exit 1;
} else {
    exit 0;
}

END {
    my $exitcode = $?;
    &cleanup();
    $? = $exitcode;
}

sub quit_handler {
    $verbose = !$verbose;
    print "*** packager: Verbose mode " . ($verbose ? "on" : "off") . "\n";
}

sub signal_handler {
    my ($signal) = @_;
    my %signals = (HUP   => "Hangup",
		   INT   => "Interrupt",
		   QUIT  => "Quit",
		   ILL   => "Illegal instruction",
		   TRAP  => "IOT trap",
		   ABRT  => "Abort",
		   BUS   => "Bus error",
		   FPE   => "Floating point exception",
		   USR1  => "User signal 1",
		   SEGV  => "Segmentation fault",
		   USR2  => "User signal 2",
		   PIPE  => "Broken pipe",
		   ALRM  => "Alarm clock",
		   TERM  => "Terminated",
		   XCPU  => "Exceeded CPU limit",
		   XFSZ  => "Exceeded file size limit",
		   VTALRM=> "Virtual alarm clock",
		   IO    => "I/O error",
		   PWR   => "Power fault");
    my $newsignal = $signals{$signal} || "SIG$signal";
    print STDERR "\n*** packager: $newsignal\n";
    &cleanup();
    $SIG{$signal} = undef;
    kill $signal, $$;
}

###########################################################################

sub handle_package
{
    my ($package, $mode) = @_;
    my %pkgdata;
    &init_package($package, \%pkgdata) or return 0;
    return 1 if $allpkg && $pkgdata{'Explicit-Only'};
    if ($mode eq "c") {
	exit(&package_is_installed($package) ? 0 : 2);
    }
    if ($mode eq "d" || $mode eq "u") {
	&download($mode eq "u", $package, %pkgdata) or return 0;
    }
    if ($mode eq "i" || $mode eq "r" || $mode eq "u") {
	&install($mode eq "r", $mode eq "u", $package, %pkgdata) or return 0;
    }
    if ($mode eq "C") {
	&clean($package, %pkgdata) or return 0;
    }
    if ($mode eq "Z") {
	&zap($package, \%pkgdata) or return 0;
    }
    return 1;
}

###########################################################################

sub init
{
    $SIG{'HUP'}    = \&signal_handler;
    $SIG{'INT'}    = \&signal_handler;
    $SIG{'QUIT'}   = \&quit_handler;
    $SIG{'ILL'}    = \&signal_handler;
    $SIG{'TRAP'}   = \&signal_handler;
    $SIG{'IOT'}    = \&signal_handler;
    $SIG{'BUS'}    = \&signal_handler;
    $SIG{'FPE'}    = \&signal_handler;
    $SIG{'USR1'}   = \&signal_handler;
    $SIG{'SEGV'}   = \&signal_handler;
    $SIG{'USR2'}   = \&signal_handler;
    $SIG{'PIPE'}   = \&signal_handler;
    $SIG{'ALRM'}   = \&signal_handler;
    $SIG{'TERM'}   = \&signal_handler;
    $SIG{'XCPU'}   = \&signal_handler;
    $SIG{'XFSZ'}   = \&signal_handler;
    $SIG{'VTALRM'} = \&signal_handler;
    $SIG{'IO'}     = \&signal_handler;
    $SIG{'PWR'}    = \&signal_handler;
    while ($ARGV[0] =~ /^-/) {
	if ($ARGV[0] eq "-" || $ARGV[0] eq "--") {
	    shift @ARGV;
	    last;
	}
	$ARGV[0] =~ /^-(.)(.*)/;
	my ($opt,$rest) = ($1,$2);
	if ($opt eq "o" || $opt eq "O" || $opt eq "S") {  # handled later
	    my $type = $opt;
	    my $opt = $rest;
	    my $val = "1";
	    $val = $2 if $opt =~ s/(.*?)=(.*)/$1/;
	    push @ARGO, [$type,$opt,$val];
	    $rest = "";
	} elsif ($opt eq "a") {
	    $dont_ask = 1;
	} elsif ($opt eq "g") {
	    push @ARGO, ["o","debug","1"];
	} elsif ($opt eq "l") {
	    $my_logdir = $rest || (shift(@ARGV), $ARGV[0]);
	    $rest = "";
	} elsif ($opt eq "n") {
	    $dont_ask = -1;
	} elsif ($opt eq "s") {
	    push @ARGO, ["o","striplib","1"];
	} elsif ($opt eq "t") {
	    $my_tmpdir = $rest || (shift(@ARGV), $ARGV[0]);
	    $rest = "";
	} elsif ($opt eq "u") {
	    $always_upgrade = 1;
	} elsif ($opt eq "v") {
	    $verbose = 1;
	} elsif ($opt eq "w") {
	    $waittime = $rest || (shift(@ARGV), $ARGV[0]);
	    die "Timeout (-w) must be an integer >= 0\n" if $waittime =~ /\D/;
	    $waittime =~ s/^0+//;
	    $waittime = 0 + $waittime;  # make sure it's a number
	    $rest = "";
	} elsif ($opt eq "A") {
	    $dont_ask = 2;
	} elsif ($opt eq "D") {
	    $delete_old = 1;
	} elsif ($opt eq "P") {
	    $purge = 1;
	} elsif ($opt eq "T") {
	    $do_timing = 1;
	} elsif ($opt eq "Z") {
	    # zbi broken when dependencies are installed (see FIXME below)
	    $allow_zbi = 1;
	} elsif ($opt eq "3") {
	    $gen_i386 = 1;
	} else {
	    print STDERR "Unrecognized option -$opt\n" if $opt ne "h";
	    &usage;
	}
	if ($rest eq "") {
	    shift @ARGV;
	} else {
	    $ARGV[0] = "-$rest";
	}
    }
    &usage if $#ARGV < 1;
    if ("check" =~ /^$ARGV[0]/) {
	$mode = "c";
    } elsif ("download" =~ /^$ARGV[0]/) {
	$mode = "d";
    } elsif ("install" =~ /^$ARGV[0]/) {
	$mode = "i";
    } elsif ("reinstall" =~ /^$ARGV[0]/) {
	$mode = "r";
    } elsif ("upgrade" =~ /^$ARGV[0]/) {
	$mode = "u";
    } elsif ("Clean" =~ /^$ARGV[0]/) {
	$mode = "C";
    } elsif ("Zap" =~ /^$ARGV[0]/) {
	$mode = "Z";
    } else {
	&usage;
    }
    shift @ARGV;
    @packages = @ARGV;
    if (grep {$_ eq "ALL"} @packages) {
	if ($mode eq "c" || $mode eq "Z") {
	    print STDERR "Operations `check' and `Zap' invalid with ALL\n";
	    exit 1;
	}
	$allpkg = 1;
	my @skip;
	map {s/^-//} (@skip = grep {/^-/} @packages);
	@packages = sort {$a cmp $b} <$INSTDIR/*/.version>;
	map {s,.*/(.*)/\.version$,$1,} @packages;
	@packages = grep {my $this = $_; !grep {$this eq $_} @skip} @packages;
	# Sort packages by dependencies, putting base packages first
	print STDERR "Sorting packages... ";
	@packages = ("base", grep {$_ ne "base"} @packages);
	my %done = ();
	sub findnext { for (my $i=0; $i<@packages; $i++) {return $i if !${$_[0]}{$packages[$i]};} return -1; }
	while ((my $next = &findnext(\%done)) >= 0) {
	    my $s = "$next/".($#packages+1);
	    printf STDERR "%s\033[%dD", $s, length($s);
	    my @before = @packages[0..$next-1];
	    my @after = @packages[$next..$#packages];
	    my %pkgdata = &read_package_data($after[0]);
	    foreach $p (split(/\s+/, $pkgdata{'Requires'}), split(/\s+/, $pkgdata{'Compile-Requires'}), split(/\s+/, $pkgdata{'Suggests'}), split(/\s+/, $pkgdata{'Rebuild-For'})) {
		next if $p =~ /^\*/;
		foreach $q (split(/\|/, $p)) {
		    next if $q eq $after[0];
		    for (my $i = 1; $i < @after; $i++) {
			if ($after[$i] eq $q) {
			    if ($done{$q}) {
				print STDERR "WARNING: dependency loop ($after[0] -> $q)\n";
			    } else {
				push @before, $after[$i];
				@after = (@after[0..$i-1], @after[$i+1..$#after]);
			    }
			}
		    }
		}
	    }
	    @packages = (@before, @after);
	    $done{$after[0]} = 1;
	}
	print STDERR "done.\033[K\n";
    }
    if ($my_tmpdir) {
	if (! -d $my_tmpdir) {
	    print STDERR "$my_tmpdir does not exist or is not a directory, aborting.\n";
	    exit 1;
	}
	$tmpdir = `cd $my_tmpdir; /bin/pwd`;
	$tmpdir =~ s/\r?\n//g;
	if (!$tmpdir) {
	    print STDERR "$my_tmpdir: Unable to determine real path name, aborting.\n";
	    exit 1;
	}
    } else {
	my $dir = ($ENV{'TMPDIR'} || "/tmp") . "/packager$$." . int(rand(256));
	if (!mkdir($dir, 0700)) {
	    print STDERR "mkdir($dir): $!\n";
	    exit 1;
	}
	$tmpdir = $dir;
    }
    if ($my_logdir ne "") {
	$logfile = $my_logdir;
	if (! -d $logfile) {
	    print STDERR "Log directory $logfile does not exist, aborting.\n";
	    exit 1;
	}
	if (lstat("$logfile/log")) {
	    print STDERR "Log file $logfile/log already exists, aborting.\n";
	    exit 1;
	}
    } else {
	$logfile = ($ENV{'TMPDIR'} || "/tmp") . "/log.packager-$$" ;
	if (!mkdir($logfile, 0700)) {
	    print STDERR "mkdir($logfile): $!\n";
	    exit 1;
	}
    }
    $logfile = "$logfile/log";  # actual filename
}

###########################################################################

sub cleanup
{
    chdir("/"), system("/bin/rm", "-rf", $tmpdir) if $tmpdir && !$my_tmpdir;
    if (-f $logfile) {
	print STDERR "A make log can be found in $logfile.\n";
    } elsif ($my_logdir eq "") {
	$logfile =~ s,/log$,,;
	rmdir $logfile;
    }
}

###########################################################################

sub init_package
{
    my ($package, $pkgdata_ret) = @_;

    %pkgdata = &read_package_data($package, 1);
    return 0 if !$pkgdata{'Package-Name'};
    if ($pkgdata{'Dummy-Package'}) {
	%$pkgdata_ret = %pkgdata;
	return 1;
    }

    %sysoptions = %{&read_options("$INSTDIR/packager/.sysopt")};
    %pkgoptions = %{&read_options("$INSTDIR/$package/.pkgopt")};
    $got_sysopt = 0;
    $got_pkgopt = 0;
    %options = ();
    foreach (@ARGO) {
	my ($type,$opt,$val) = @$_;
	if ($type eq "o") {
	    if ($opt =~ s/^!//) {
		delete $options{$opt};
		$options{"!$opt"} = 1;
	    } else {
		delete $options{"!$opt"};
		$options{$opt} = $val;
	    }
	} elsif ($type eq "O") {
	    $got_pkgopt = 1;
	    if ($opt =~ s/^!//) {
		delete $options{$opt};
		delete $pkgoptions{$opt};
		$options{"!$opt"} = 1;
		$pkgoptions{"!$opt"} = 1;
	    } elsif ($opt =~ s/^-//) {
		delete $pkgoptions{$opt};
	    } else {
		delete $options{"!$opt"};
		delete $pkgoptions{"!$opt"};
		$options{$opt} = $val;
		$pkgoptions{$opt} = $val;
	    }
	} elsif ($type eq "S") {
	    $got_sysopt = 1;
	    if ($opt =~ s/^!//) {
		delete $options{$opt};
		delete $sysoptions{$opt};
		$options{"!$opt"} = 1;
		$sysoptions{"!$opt"} = 1;
	    } else {
		delete $options{"!$opt"};
		delete $sysoptions{"!$opt"};
		$options{$opt} = $val;
		$sysoptions{$opt} = $val;
	    }
	    delete $pkgoptions{$opt};  # make it use the systemwide one
	} else {
	    print STDERR "BUG: bad ARGO type $type\n";
	}
    }
    &write_options("$INSTDIR/packager/.sysopt", \%sysoptions) if $got_sysopt;
    &write_options("$INSTDIR/$package/.pkgopt", \%pkgoptions) if $got_pkgopt;
    # apply package options, then systemwide options, to undefined entries
    foreach (keys(%pkgoptions)) {
	my ($not,$opt) = /(!?)(.*)/;
	if (!defined($options{$opt}) && !defined($options{"!$opt"})) {
	    $options{$_} = $pkgoptions{$_};
	}
    }
    foreach (keys(%sysoptions)) {
	my ($not,$opt) = /(!?)(.*)/;
	if (!defined($options{$opt}) && !defined($options{"!$opt"})) {
	    $options{$_} = $sysoptions{$_};
	}
    }

    local *F;
    if (!open(F, ">$tmpdir/Makefile")) {
	print STDERR "open($tmpdir/Makefile): $!\n";
	return 0;
    }
    my $s = "PACKAGE=$package\n";
    $s .= "PREFIX=$INSTDIR/$package\n";
    $s .= "PACKAGER_TMPDIR=$tmpdir\n";
    $s .= "GCC_OPT_FLAGS=" . ($options{"debug"} ? "-g" : &gcc_opt_flags()) . "\n";
    $s .= "DEBUG=1\n" if $options{"debug"};
    $s .= "STRIPLIB=1\n" if $options{"striplib"};
    $s .= "PKGOPT_$_=$options{$_}\n" foreach grep {!/^!/} keys(%options);
    $s .= "PACKAGER_NO_CHOWN=1\n" if $pkgdata{'No-Chown'};
    $s .= <<'EOT';
.PHONY: configure instbin instsbin instlib addldso instpc instaclocal instinc
.PHONY: instinit instlocale instinfo instman instfont-ttf instfont-x11
.PHONY: preinstall Repack Compile Install Post-Install
configure:
	set -e ; \
	if sed 's/$(PACKAGE)/PACKAGE/g' < configure | grep -q 'wget\W' ; then \
		echo -n "Warning: configure script contains "\`"wget'.  Continue? [yN] " ; \
		read yn ; \
		test "x$$yn" = "xy" -o "x$$yn" = "xY" ; \
	fi
	if [ "$(PKGOPT_nonls)" ] ; then NLS=--disable-nls ; else NLS= ; fi ; \
	if [ "$(PKGOPT_shared)" = 0 ] ; then SHARED=--disable-shared ; elif [ "$(PKGOPT_shared)" = 1 ] ; then SHARED=--enable-shared ; else SHARED= ; fi ; \
	if [ "$(PKGOPT_static)" = 0 ] ; then STATIC=--disable-static ; elif [ "$(PKGOPT_static)" = 1 ] ; then STATIC=--enable-static ; else STATIC= ; fi ; \
	if [ 'x$(CONFIGURE)' != x ] ; then CONFIGURE='$(CONFIGURE)' ; else CONFIGURE=./configure ; fi ; \
	CC=gcc CXX=g++ OPT="-O3 $(GCC_OPT_FLAGS) $(CUSTOM_OPT)" CFLAGS="-O3 $(GCC_OPT_FLAGS) $(CUSTOM_OPT) $(CUSTOM_CFLAGS)" CXXFLAGS="-O3 $(GCC_OPT_FLAGS) $(CUSTOM_OPT) $(CUSTOM_CFLAGS) $(CUSTOM_CXXFLAGS)" LDFLAGS="$(CUSTOM_LDFLAGS)" $$CONFIGURE --prefix="$(PREFIX)" $$NLS $$SHARED $$STATIC $(CONFIGURE_OPTS)

instbin:
	set -e ; for i in `cd "$(PREFIX)/bin";echo $(BIN)` ; do ln -fsv "$(PREFIX)/bin/$$i" /usr/bin/ ; done

instsbin:
	set -e ; for i in `cd "$(PREFIX)/sbin";echo $(SBIN)` ; do ln -fsv "$(PREFIX)/sbin/$$i" /usr/sbin/ ; done

instlib:
	set -e ; for i in $(LIB) ; do for j in "$(PREFIX)/lib/lib$$i".{a,so,so.?,so.??} ; do \
		if [ -f "$$j" ] ; then ln -fsv "$$j" /usr/lib/ ; fi ; \
	done ; done

LDSOPATH=$(PREFIX)/lib
addldso:
	((grep -v '^$(LDSOPATH)$$' </etc/ld.so.conf ; echo "$(LDSOPATH)") >/etc/ld.so.conf~ && mv -fv /etc/ld.so.conf~ /etc/ld.so.conf) || (rm -fv /etc/ld.so.conf~ ; false)
	chmod a+r /etc/ld.so.conf
	ldconfig

instpc:
	set -e ; if test -d /usr/lib/pkgconfig ; then \
		for i in `cd "$(PREFIX)/lib/pkgconfig";echo $(PC)|sed -e 's/\.pc / /g' -e 's/\.pc$$//'` ; do \
			ln -fsv "$(PREFIX)/lib/pkgconfig/$$i.pc" "/usr/lib/pkgconfig/" ; \
		done ; \
	fi

instaclocal:
	set -e ; if test -d /usr/share/aclocal ; then \
		for i in `cd "$(PREFIX)/share/aclocal";echo $(M4)|sed -e 's/\.m4 / /g' -e 's/\.m4$$//'` ; do \
			ln -fsv "$(PREFIX)/share/aclocal/$$i.m4" "/usr/share/aclocal/" ; \
		done ; \
	fi

instetc:
	set -x -e ; \
	if test -z "$(DEST)" ; then DEST="" ; else DEST="/$(DEST)" ; fi ; \
	if test -z "$(DIR)"  ; then DIR=""  ; else DIR="$(DIR)/"   ; fi ; \
	umask 022 ; mkdir -p "/etc$$DEST/$$DIR" ; \
	if test -n "$(ETC)" ; then for i in `cd "$(PREFIX)/etc/$$DIR";echo $(ETC)` ; do \
		if test -d "$(PREFIX)/etc/$$DIR$$i" ; then \
			$(MAKE) instetc DEST="$(DEST)" DIR="$$DIR$$i" ETC="*" ; \
		else \
			if test -f "/etc$$DEST/$$DIR$$i" ; then \
				rm -fv "$(PREFIX)/etc/$$DIR$$i" ; \
			elif test -f "$(PREFIX)/etc/$$DIR$$i" ; then \
				mv -fv "$(PREFIX)/etc/$$DIR$$i" "/etc$$DEST/$$DIR" ; \
			fi ; \
			ln -fsv "/etc$$DEST/$$DIR$$i" "$(PREFIX)/etc/$$DIR" ; \
		fi ; \
	done ; fi ; \
	if test -n "$(LINK)" ; then for i in `cd "$(PREFIX)/etc/$$DIR";echo $(LINK)` ; do \
		ln -fsv "$(PREFIX)/etc/$$DIR$$i" "/etc$$DEST/$$DIR" ; \
	done ; fi

instinc:
	set -e ; \
	if [ "x$(DIR)" != x ] ; then DIR="/$(DIR)" ; else DIR= ; fi ; \
	umask 022 ; mkdir -p "/usr/include/$$DIR" ; \
	set -e ; for i in `cd "$(PREFIX)/include$$DIR";echo $(INC)` ; do \
		ln -fsv "$(PREFIX)/include$$DIR/$$i" "/usr/include$$DIR/" ; \
	done

instinit:
	set -e ; \
	if [ -n "$(SRC)" ] ; then \
		ln -fsv "$(PREFIX)/$(SRC)" "/etc/rc.d/init.d/$(DEST)" ; \
	else \
		ln -fsv generic-daemon "/etc/rc.d/init.d/$(DEST)" ; \
	fi ; \
	if [ "$(S)" ] ; then \
		rl=`echo "$(S)" | cut -d/ -f1` ; \
		pri=`echo "$(S)" | cut -d/ -f2` ; \
		if [ ! -f "/etc/rc.d//rc$$rl.d/S$${pri}$(DEST)" -a ! -f "/etc/rc.d/rc$$rl.d/s$${pri}$(DEST)" ] ; then \
			ln -fsv "../init.d/$(DEST)" "/etc/rc.d/rc$$rl.d/S$${pri}$(DEST)" ; \
		fi ; \
	fi ; \
	if [ "$(K)" ] ; then \
		rl=`echo "$(K)" | cut -d/ -f1` ; \
		pri=`echo "$(K)" | cut -d/ -f2` ; \
		if [ ! -f "/etc/rc.d/rc$$rl.d/K$${pri}$(DEST)" -a ! -f "/etc/rc.d/rc$$rl.d/k$${pri}$(DEST)" ] ; then \
			ln -fsv "../init.d/$(DEST)" "/etc/rc.d/rc$$rl.d/K$${pri}$(DEST)" ; \
		fi ; \
	fi ; \

instlocale:
	set -e ; \
	if [ ! "$(PKGOPT_nonls)" ] ; then \
		for i in `cd "$(PREFIX)" ; echo share/locale/*/LC_MESSAGES/"$(FILE)".mo` ; do if test -f "$(PREFIX)/$$i" ; then \
			dir=`dirname "/usr/$$i"` ; \
			if [ ! -d "$$dir" ] ; then umask 022 ; mkdir -p "$$dir" ; fi ; \
			ln -fsv "$(PREFIX)/$$i" "/usr/$$i" ; \
		fi ; done ; \
	else \
		for i in {"$(PREFIX)",/usr}/share/locale/*/LC_MESSAGES/"$(FILE)".mo ; do \
			j=`dirname "$$i"` ; \
			k=`dirname "$$j"` ; \
			rm -fv "$$i" ; \
			if rmdir "$$j" &>/dev/null ; then \
				rmdir "$$k" &>/dev/null || true ; \
			fi ; \
		done ; \
		rmdir "$(PREFIX)"/share/locale "$(PREFIX)"/share /usr/share/locale || true ; \
	fi

instinfo:
	umask 022 ; mkdir -p /usr/share/info
	set -e ; \
	for i in $(INFO) ; do if [ -f "$(PREFIX)/info/$$i.info.gz" ] ; then \
		ln -fsv "$(PREFIX)/info/$$i.info"*.gz /usr/share/info/ ; \
		install-info /usr/share/info/$$i.info.gz /usr/share/info/dir.gz || true ; \
	fi ; done

instman:
	umask 022 ; mkdir -p "/usr/share/man/man$(SECTION)"
	set -e ; for i in `cd "$(PREFIX)/man/man$(SECTION)";echo $(MAN)|sed -e 's/\.$(SECTION)$(SUFFIX)\.gz//g'` ; do \
		ln -fsv "$(PREFIX)/man/man$(SECTION)/$$i.$(SECTION)$(SUFFIX).gz" "/usr/share/man/man$(SECTION)/" ; \
	done

instfont-ttf:
	umask 022 ; mkdir -p "/usr/share/fonts/TTF"
	set -e ; for i in `cd /usr/share/fonts/TTF;echo $(FONTS)` ; do \
		rm -fv "/usr/share/fonts/TTF/$$i" ; \
	done
	set -e ; \
	if test -z "$(DIR)" ; then DIR="" ; else DIR="/$(DIR)"  ; fi ; \
	for i in `cd "$(PREFIX)$$DIR";echo $(FONTS)` ; do \
		ln -fsv "$(PREFIX)$$DIR/$$i" /usr/share/fonts/TTF/ ; \
	done

instfont-x11:
	umask 022 ; mkdir -p "/usr/share/fonts/X11"
	set -e ; for i in `cd /usr/share/fonts/X11;echo $(FONTS)` ; do \
		rm -fv "/usr/share/fonts/X11/$$i" ; \
	done
	set -e ; \
	if test -z "$(DIR)" ; then DIR="" ; else DIR="/$(DIR)"  ; fi ; \
	for i in `cd "$(PREFIX)$$DIR";echo $(FONTS)` ; do \
		ln -fsv "$(PREFIX)$$DIR/$$i" /usr/share/fonts/X11/ ; \
	done
	if test -x /usr/X11R6/bin/mkfontdir ; then \
		/usr/X11R6/bin/mkfontdir /usr/share/fonts/X11 ; \
	fi

repack-finish:
	test -d "$(PACKAGE)-$(VERSION)" || (echo >&2 "Directory $(PACKAGE)-$(VERSION) not found"; exit 1)
	chown -R 0.0 "$(PACKAGE)-$(VERSION)"
	chmod -R a+rX "$(PACKAGE)-$(VERSION)"
	chmod -R go-w "$(PACKAGE)-$(VERSION)"
	find "$(PACKAGE)-$(VERSION)" \( -type f -o -type l \) -print | sort | (tar cvfT - - | gzip -9 > "$(DEST)") 2>&1 | sed 's/^/[] /'

preinstall:
	if [ -z "$(PACKAGER_NO_CHOWN)" ] ; then \
		chown -R 0.0 "$(PREFIX)" ; \
		chmod -R a+rX "$(PREFIX)" ; \
	fi
	if [ -z "$(DEBUG)" -a -n "$(STRIPLIB)" ] ; then rm -fv "$(PREFIX)"/lib/*_g.a ; fi
	if [ -z "$(DEBUG)" -a -x /usr/bin/strip ] ; then \
		for i in "$(PREFIX)"/{bin,libexec,sbin}/* ; do \
			if [ \( ! -h "$$i" \) -a -f "$$i" -a -x "$$i" ] ; then \
				a=`head -4c "$$i"` ; \
				if [ "x$$a" = "xELF" ] ; then \
					echo strip "$$i" ; \
					strip "$$i" ; \
				fi ; \
			fi ; \
		done ; \
		if [ -z "$(DEBUG)" -a -n "$(STRIPLIB)" ] ; then for i in "$(PREFIX)"/lib/*.{a,so,so.*} ; do \
			if [ \( ! -h "$$i" \) -a -f "$$i" ] ; then \
				a=`head -4c "$$i"` ; \
				if [ "x$$a" = "xELF" -o "x$$a" = "x!<ar" ] ; then \
					echo strip -x "$$i" ; \
					strip -x "$$i" ; \
				fi ; \
			fi ; \
		done ; fi ; \
	fi
	rm -fv "$(PREFIX)/info/dir" "$(PREFIX)/info/dir.gz"
	set -e ; if [ -d "$(PREFIX)/info" ] ; then \
		find "$(PREFIX)/info" -name \*.gz -exec rm -fv {} \; ; \
		find "$(PREFIX)/info" -type f -printf '%p -> %p.gz\n' -exec gzip -9f {} \; ; \
	fi
	set -e ; if [ -d "$(PREFIX)/man" ] ; then \
		find "$(PREFIX)/man" -name \*.gz -exec rm -fv {} \; ; \
		find "$(PREFIX)/man" -type f | perl -nle 'open F,"<$$_" or next; $$a=<F>; if ($$a =~ /^\.so (man.\/)?(\S+)$$/) {$$b=""; $$b="../$$1" if $$1; print "$$b$$2 $$_"}' | while read a b ; do ln -fsv $$a $$b ; done ; \
		for f in `find "$(PREFIX)/man" -type l` ; do \
			link=`ls -l "$$f" | sed 's/.* //'` ; \
			rm -fv "$$f" ; \
			ln -sv "$$link.gz" "$$f.gz" ; \
		done ; \
		find "$(PREFIX)/man" -type f \! -name \*.gz -exec gzip -9f {} \; ; \
	fi
	-chmod a-x "$(PREFIX)"/info/*.gz "$(PREFIX)"/man/*/*.gz

doinstall:
	$(MAKE) -C$(PACKAGER_TMPDIR) Install

EOT
    $s .= "Repack:\n$pkgdata{'Repack'}\n" .
	  "Compile:\n$pkgdata{'Compile'}\n" .
	  "Install:\n$pkgdata{'Install'}\n" .
	  "Post-Install:\n$pkgdata{'Post-Install'}\n" .
	  "Clean:\n$pkgdata{'Clean'}\n";
    if (!(print F $s and close F)) {
	print STDERR "write($tmpdir/Makefile): $!\n";
	return 0;
    }
    %$pkgdata_ret = %pkgdata;
    return 1;
}

###########################################################################

sub usage
{
    print STDERR <<EOT;
Usage: packager [options] <mode> {<package-name>... | ALL [-<package-name>...]}
  options: [-a] [-g] [-l logdir] [-o option[=value]] [-s] [-t tmpdir] [-u]
           [-v] [-w timeout] [-D] [-P] [-T] [-3]
  mode: c[heck] | d[ownload] | i[nstall] | r[einstall] | u[pgrade] | 
        C[lean] | Z[ap]
EOT
    exit 1;
}

###########################################################################
###########################################################################

# Return values:
#    0 -> download failed
#    1 -> download succeeded
#    2 -> already up to date, or $check_installed nonzero and pkg installed
#    3 -> $allpkg and package missing Source: or Repack:
#    4 -> package skipped from -n

sub download
{
    my ($check_installed, $package, %pkgdata) = @_;

    return 2 if $pkgdata{'Dummy-Package'};
    if (!$pkgdata{'Source'}) {
	return 3 if $allpkg;
	print STDERR "Package $package has no source information!\n";
	return 0;
    }
    if (!defined($pkgdata{'Repack'})) {
	return 3 if $allpkg;
	print STDERR "Package $package does not have a Repack rule!\n";
	return 0;
    }
    my ($protocol,$host,$port,$dir,$filepat,$replace,$newname) =
	($pkgdata{'Source'} =~ m,^(\w+)://(.*?)(?::(\d+))?((?:/[^/]*)*)/(\S*)\s+(\S+)(?:\s+(.+))?,);
    if ($replace eq "") {
	print STDERR "Source information in package $package is invalid!\n";
	return 0;
    }
    $filepat = ".*" if $filepat eq "";
    my $handler = $protocols{lc($protocol)};
    if (!$handler) {
	print STDERR "Protocol `$protocol' for package $package not supported!\n";
	return 0;
    }
    if (!chdir($tmpdir)) {
	print STDERR "chdir($tmpdir): $!\n";
	return 0;
    }
    my $cur_version = &get_downloaded_version($package);
    if (!$cur_version && $check_installed) {
	$cur_version = &get_installed_version($package);
    }
    &do_open($handler, $host, $port, \%pkgdata) or return 0;
    return 0 if $@;
    if ($filepat !~ /^\(*\.\*\)*$/) {  # leave .* or (.*) alone
	$filepat =~ s/\./\\./g;
    }
    my @files = &{$$handler{'dir'}}($dir, $filepat) or return 0;
    my $eflag = 0;
    if ($replace =~ s/^e://) {
	$eflag = 1;
    }
    $replace =~ s/,/\\,/g;
    local $p = $filepat;  # with "my" the evals break
    @files = sort {
	my ($x,$y) = ($a,$b);
	foreach ($x,$y) {
	    if ($eflag) {
		eval "s,\$p,$replace,e";
	    } else {
		eval "s,\$p,$replace,";
	    }
	}
	&compare_versions($y,$x);
    } @files;
    my @versions = @files;
    foreach (@versions) {
	if ($eflag) {
	    eval "s,\$p,$replace,e";
	} else {
	    eval "s,\$p,$replace,";
	}
    }
    if ($versions[0] =~ /-/) {
	print STDERR "Invalid remote version `$versions[0]'.\n";
	print STDERR "Version numbers must not contain a dash.\n";
	return 0;
    }
    print STDERR "Package $package: local ".($cur_version||"(none)").
		 ", remote $versions[0]\n";
    if (&compare_versions($cur_version, $versions[0]) >= 0) {
	&{$$handler{'close'}};
	return 2;
    }
    if (!$dont_ask) {
	my $yn;
	do {
	    print STDERR "Download new version? [Yn] ";
	    $yn = <STDIN>;
	    chop $yn;
	} while ($yn ne "" && $yn !~ /^[yn]/i);
	if ($yn ne "" && $yn !~ /^y/i) {
	    &{$$handler{'close'}};
	    return 2;
	}
    } elsif ($dont_ask < 0) {
	return 4;
    }
    my $r0 = $versions[0];
    my ($r1,$r2,$r3,$r4,$r5,$r6,$r7,$r8,$r9) = ($files[0] =~ m,$filepat,);
    if ($newname ne "") {
	$newname =~ s,\$([0-9]),\${r$1},g;
	$newname =~ s/,/\\,/g;
	if ($newname =~ s/^e://) {
	    eval "\$files[0] =~ s,\$filepat,$newname,e";
	} else {
	    eval "\$files[0] =~ s,\$filepat,$newname,";
	}
    }
    my @to_download;
    my @to_download_protocol;
    my @to_download_host;
    if ($files[0] =~ s,^(\w+)://([^/]*),,) {
	$to_download[0] = $files[0];
	$to_download_protocol[0] = $1;
	$to_download_host[0] = $2;
    } else {
	$to_download[0] = "$dir/$files[0]";
	$to_download_protocol[0] = $protocol;
	$to_download_host[0] = $host;
    }
    foreach (keys(%pkgdata)) {
	if (/^More-Source-([1-9]\d*)$/) {
	    my $n = $1;
	    $to_download[$n] = $pkgdata{$_};
	    $to_download[$n] =~ s,\$([0-9]),eval '${r'.$1.'}',eg;
	    if ($to_download[$n] =~ s,^(\w+)://([^/]*),,) {
		$to_download_protocol[$n] = $1;
		$to_download_host[$n] = $2;
	    } else {
		$to_download_protocol[$n] = undef;
		$to_download_host[$n] = undef;
	    }
	    $to_download[$n] = "$dir/$to_download[$n]" if $to_download[$n] !~ m#^/#;
	}
    }
    print "Downloading $package-$versions[0]...\n";
    my $total = 0;
    my $n;
    for ($n = 0; $n <= $#to_download; $n++) {
	next if $to_download[$n] eq "";
	if (defined($to_download_protocol[$n])
	 && defined($to_download_host[$n])
	 && ($to_download_protocol[$n] ne $protocol
	     || $to_download_host[$n] ne $host)
	) {
	    &{$$handler{'close'}};
	    $protocol = $to_download_protocol[$n];
	    $host = $to_download_host[$n];
	    if ($host =~ s/^(.*?):(\d+)/$1/) {
		$port = $2;
	    }
	    $handler = $protocols{lc($protocol)};
	    if (!$handler) {
		printf STDERR "Protocol `%s' for package %s (%s) not".
		       " supported!\n", $protocol, $package,
		       $n ? "additional file $n" : "main file";
		return 0;
	    }
	    &do_open($handler, $host, $port, \%pkgdata) or return 0;
	}
	my $size = &{$$handler{'size'}}($to_download[$n]);
	if (!defined($size) || $size < 0) {
	    if (!defined($size)) {
		print STDERR "Warning: get size failed for $to_download[$n]\n";
	    }
	    $total = -1;
	    last;
	}
	$total += $size;
    }
    &status_init($total);
    my @more = ();
    my $current_protocol = $protocol;
    my $current_host = $host;
    for ($n = 0; $n <= $#to_download; $n++) {
	next if $to_download[$n] eq "";
	if (defined($to_download_protocol[$n])
	 && defined($to_download_host[$n])
	 && ($to_download_protocol[$n] ne $protocol
	     || $to_download_host[$n] ne $host)
	) {
	    &{$$handler{'close'}};
	    $protocol = $to_download_protocol[$n];
	    $host = $to_download_host[$n];
	    if ($host =~ s/^(.*?):(\d+)/$1/) {
		$port = $2;
	    }
	    $handler = $protocols{lc($protocol)};
	    if (!$handler) {
		printf STDERR "Protocol `%s' for package %s (%s) not".
		       " supported!\n", $protocol, $package,
		       $n ? "additional file $n" : "main file";
		return 0;
	    }
	    &do_open($handler, $host, $port, \%pkgdata) or return 0;
	}
	my $data;
	return 0 if !($dataref = &{$$handler{'get'}}($to_download[$n], \&status));
	local *DATA;
	if (!open(DATA, ">data$n")) {
	    print STDERR "create($tmpdir/data$n): $!\n";
	    return 0;
	}
	if (syswrite(DATA, $$dataref) != length($$dataref)) {
	    print STDERR "write($tmpdir/data$n): $!\n";
	    close DATA;
	    return 0;
	}
	close DATA;
	push @more, "SOURCE$n=$tmpdir/data$n" if $n > 0;
    }
    &status_end();
    &{$$handler{'close'}}();
    $newname = "$package-$versions[0].tar.gz";
    print "Repacking...\n";
    local *F;
    if (!open(F, ">>Makefile")) {
	print STDERR "append(Makefile): $!\n";
	return 0;
    }
    print F "\nSOURCE=$tmpdir/data0\n";
    print F join("\n",@more)."\n";
    print F "VERSION=$versions[0]\n";
    close F;
    if (!&make($package, $versions[0], $pkgdata{'timing_Repack'}, "Repack", "repack-finish", "DEST=$tmpdir/$newname")
     || ! -f "$package-$versions[0].tar.gz"
    ) {
	print STDERR "Repacking failed!\n";
	return 0;
    }
    # Can't use rename() here--tmpdir and SRCDIR might be on different devices
    if (system("mv", $newname, "$SRCDIR/$newname") != 0) {
	print STDERR "Unable to install source tarball!\n";
	return 0;
    }
    if ($delete_old && $cur_version ne "") {
	unlink "$SRCDIR/$package-$cur_version.tar.gz";
    }
    return 1;
}

###########################################################################
###########################################################################

sub install
{
    my ($reinstall, $upgrade, $package, %pkgdata) = @_;

    return 1 if $pkgdata{'Dummy-Package'};
    if (!defined($pkgdata{'Compile'})) {
	return 3 if $allpkg;
	print STDERR "Package $package does not have a Compile rule!\n";
	return 0;
    }
    if (!defined($pkgdata{'Install'})) {
	return 3 if $allpkg;
	print STDERR "Package $package does not have an Install rule!\n";
	return 0;
    }
    if (! -d "$INSTDIR/$package") {
	if (!mkdir("$INSTDIR/$package", 0755)) {
	    print STDERR "mkdir($INSTDIR/$package,0755): $!\n";
	    return 0;
	}
	chmod 0755, "$INSTDIR/$package";
    }
    if (!chdir($tmpdir)) {
	print STDERR "chdir($tmpdir): $!\n";
	return 0;
    }
    my $version = &get_downloaded_version($package);
    if (!$version && $upgrade) {
	$version = &get_installed_version($package);
    }
    if (!$version) {
	if ($allpkg && (!$pkgdata{'Source'} || !$pkgdata{'Repack'})) {
	    return 3;
	}
	print STDERR "Package $package has not been downloaded!\n";
	if (!$dont_ask) {
	    my $yn;
	    do {
		print STDERR "Download it now? [Yn] ";
		$yn = <STDIN>;
		chop $yn;
	    } while ($yn ne "" && $yn !~ /^[yn]/i);
	    if ($yn =~ /^n/i) {
		return 0;
	    }
	} elsif ($dont_ask < 0) {
	    return 4;
	}
	my $old_dont_ask = $dont_ask;
	$dont_ask = 1;
	&handle_package($package, "d");
	$dont_ask = $old_dont_ask;
	$version = &get_downloaded_version($package);
	if (!$version) {
	    print STDERR "Downloading failed, installation aborted.\n";
	    return 0;
	}
    }
    local *F;
    if (!open(F, ">>Makefile")) {
	print STDERR "append(Makefile): $!\n";
	return 0;
    }
    print F "VERSION=$version\n";
    close F;
    my $instver = &get_installed_version($package);
    my $compare = &compare_versions($version, $instver);
    if ($compare == 0) {
	# See if it depends on a package modified previously
	# FIXME: record install times so this can be applied even without ALL
	REIN_CHECK: foreach $p (split(/\s+/, $pkgdata{'Requires'}), split(/\s+/, $pkgdata{'Compile-Requires'}), split(/\s+/, $pkgdata{'Suggests'}), split(/\s+/, $pkgdata{'Rebuild-For'})) {
	    $p =~ s/^\*//;
	    foreach $q (split(/\|/, $p)) {
		if ($modified{$q}) {
		    $reinstall = 1;
		    last REIN_CHECK;
		}
	    }
	}
	if (!$reinstall) {
	    print STDERR "Package $package is up to date.\n";
	    return 2;
	}
    } elsif ($compare < 0) {
	# FIXME: handle -a
	print STDERR "Warning: downloaded source is version $version, but\n";
	print STDERR "         newer version $instver is already installed!\n";
	my $yn;
	do {
	    print STDERR "Continue? [yN] ";
	    $yn = <STDIN>;
	    chop $yn;
	} while ($yn ne "" && $yn !~ /^[yn]/i);
	if ($yn !~ /^y/i) {
	    return 0;
	}
    }
    &install_check_dependencies($package, %pkgdata) or return $dont_ask<0 ? 4 : 0;
    open F, ">$INSTDIR/$package/.version";  # make it an empty file
    close F;
    print STDERR "Installing package $package (version $version)...\n";
    my $tarball = "$SRCDIR/$package-$version.tar.gz";
    my $into;
    my $extracted = 0;
    if ($pkgdata{'Compile-In-Place'}) {
	$into = "/usr/src/$package-$version";
	if (! -d $into) {
	    if (system("tar", "Cxzfp", "/usr/src", $tarball) != 0) {
		print STDERR "Unable to unpack sources for package $package!\n";
		return 0;
	    }
	    $extracted = 1;
	}
    } else {
	$into = "$tmpdir/$package-$version";
	if (system("tar", "Cxzfp", $tmpdir, $tarball) != 0) {
	    print STDERR "Unable to unpack sources for package $package!\n";
	    return 0;
	}
	$extracted = 1;
    }
    if (!chdir($into)) {
	print STDERR "chdir($into): $!\n";
	return 0;
    }
    if ($pkgdata{'Patch'} && $extracted) {
	local $SIG{'PIPE'} = "IGNORE";
	my $v_option = $verbose ? " --verbose" : " &>/dev/null";
	if (!open(PATCH, "|patch -p0$v_option")) {
	    print STDERR "open(|patch -p0$v_option): $!\n";
	    return 0;
	}
	if (!print PATCH $pkgdata{'Patch'}) {
	    close PATCH;
	    print STDERR "Source patch failed!\n";
	    return 0;
	}
	if (!close(PATCH)) {
	    print STDERR "Source patch failed!\n";
	    return 0;
	}
    }
    # If we've gotten this far, go ahead and zap the old package data if needed
    # FIXME: can delete a just-installed prerequisite package! oops
    if ($allow_zbi && $pkgdata{'Zap-Before-Install'}) {
	&zap($package, \%pkgdata, 1);
    }
    system "sync";  # to avoid random disk writes messing up timing
    {
	local $verbose = $verbose;
	$verbose = 1 if $pkgdata{'Interactive-Compile'};
	if (!&make($package, $version, $pkgdata{'timing_Compile'},
		   "Compile", "preinstall", "doinstall", "-f$tmpdir/Makefile")
	) {
	    print STDERR "Installation failed!\n";
	    return 0;
	}
    }
    if ($pkg{'Post-Install'}) {
	if (system("make -C$tmpdir Post-Install") != 0) {
	    print STDERR "Post-installation script failed!\n";
	    return 0;
	}
    }
    if (!open(F, ">$INSTDIR/$package/.version")) {
	print STDERR "Unable to record installed version number!  This may\n";
	print STDERR "cause problems in the future.\n";
    } else {
	print F "$version\n";
	close F;
    }
    chdir $tmpdir;
    if (!$pkgdata{'Compile-In-Place'} && !$my_tmpdir) {
	system "/bin/rm", "-rf", "$tmpdir/$package-$version";
    }
    $modified{$package} = 1;
    return 1;
}

###########################################################################

sub install_check_dependencies
{
    my ($package, %pkgdata) = @_;
    my $install_mode = $always_upgrade ? "u" : "i";

    foreach $p (split(/\s+/, $pkgdata{'Conflicts'})) {
	if (&package_is_installed($p)) {
	    print STDERR "Cannot install package $package: conflicts with $p.\n";
	    return 0;
	}
    }
    foreach $p (split(/\s+/, $pkgdata{'Requires'}), split(/\s+/, $pkgdata{'Compile-Requires'})) {
	$p =~ s/^\*//;
	next if $p eq $package;
	my $found = 0;
	foreach $q (split(/\|/, $p)) {
	    if (&package_is_installed($q)) {
		$found = 1;
		last;
	    }
	}
	if (!$found) {
	    my $s = ($p =~ /\|/) ? "one of the packages" : "package";
	    print STDERR "Package $package requires $s $p to be installed.\n";
	    if (!$dont_ask) {
		my $yn;
		do {
		    print STDERR "Install it now? [Yn] ";
		    $yn = <STDIN>;
		    chop $yn;
		} while ($yn ne "" && $yn !~ /^[yn]/i);
		if ($yn ne "" && $yn !~ /^y/i) {
		    print STDERR "Cannot install package $package: requires $p.\n";
		    return 0;
		}
	    } elsif ($dont_ask < 0) {
		return 0;
	    }
	    if ($p =~ /\|/) {
		my $ok = 0;
		foreach $q (split(/\|/,$p)) {
		    print STDERR "Trying to install package $q...\n";
		    if (!rename("$tmpdir/Makefile", "$tmpdir/Makefile.$package")) {
			print STDERR "rename($tmpdir/Makefile,$tmpdir/Makefile.$package): $!\n";
			return 0;
		    }
		    my $result = &handle_package($q, $install_mode);
		    if (!rename("$tmpdir/Makefile.$package", "$tmpdir/Makefile")) {
			print STDERR "rename($tmpdir/Makefile.$package,$tmpdir/Makefile): $!\n";
			return 0;
		    }
		    if ($result == 1 || $result == 2) {
			$ok = 1;
			last;
		    }
		}
		if (!$ok) {
		    print STDERR "Unable to install any of packages $p, aborting.\n";
		    return 0;
		}
	    } else {
		if (!rename("$tmpdir/Makefile", "$tmpdir/Makefile.$package")) {
		    print STDERR "rename($tmpdir/Makefile,$tmpdir/Makefile.$package): $!\n";
		    return 0;
		}
		my $result = &handle_package($p, $install_mode);
		if (!rename("$tmpdir/Makefile.$package", "$tmpdir/Makefile")) {
		    print STDERR "rename($tmpdir/Makefile.$package,$tmpdir/Makefile): $!\n";
		    return 0;
		}
		if ($result != 1 && $result != 2) {
		    print STDERR "Unable to install required package $p, aborting.\n";
		    return 0;
		}
	    }
	}  # if (!$found)
    }  # for each required package
    return 1 if $dont_ask == 1;  # -a: skip suggestions
    return 1 if $dont_ask < 0;   # -n: skip everything
    foreach $p (split(/\s+/, $pkgdata{'Suggests'})) {
	$p =~ s/^\*//;
	my $found = 0;
	foreach $q (split(/\|/, $p)) {
	    if (&package_is_installed($q)) {
		$found = 1;
		last;
	    }
	}
	if (!$found) {
	    my $s = ($p =~ /\|/) ? "one of the packages" : "package";
	    print STDERR "Package $package suggests $s $p to be installed.\n";
	    if (!$dont_ask) {
		my $yn;
		do {
		    print STDERR "Install it now? [yN] ";
		    $yn = <STDIN>;
		    chop $yn;
		} while ($yn ne "" && $yn !~ /^[yn]/i);
		next if $yn !~ /^y/i;
	    }
	    my $ok = 0;
	    foreach $q (split(/\|/,$p)) {
		print STDERR "Trying to install package $q...\n";
		if (!rename("$tmpdir/Makefile", "$tmpdir/Makefile.$package")) {
		    print STDERR "rename($tmpdir/Makefile,$tmpdir/Makefile.$package): $!\n";
		    return 0;
		}
		my $result = &handle_package($q, $install_mode);
		if (!rename("$tmpdir/Makefile.$package", "$tmpdir/Makefile")) {
		    print STDERR "rename($tmpdir/Makefile.$package,$tmpdir/Makefile): $!\n";
		    return 0;
		}
		if ($result) {
		    $ok = 1;
		    last;
		}
	    }
	    if (!$ok && !$dont_ask) {
		my $s = ($p =~ /\|/) ? "None of the packages" : "Package";
		my $t = ($p =~ /\|/) ? "could " : "could not";
		my $yn;
		do {
		    print STDERR "$s $p $t be installed.  Continue anyway? [Yn] ";
		    $yn = <STDIN>;
		    chop $yn;
		} while ($yn ne "" && $yn !~ /^[yn]/i);
		return 0 if $yn =~ /^n/i;
	    }
	}  # if (!$found)
    }  # for each suggested package
    return 1;
}

###########################################################################
###########################################################################

sub clean
{
    my ($package, %pkgdata) = @_;

    return 1 if $pkgdata{'Dummy-Package'} || !defined($pkgdata{'Clean'});
    my $version = &get_installed_version($package);
    if (!$version) {
	print STDERR "Package $package is not installed!\n";
	return 0;
    }
    if (!chdir($tmpdir)) {
	print STDERR "chdir($tmpdir): $!\n";
	return 0;
    }
    local *F;
    if (!open(F, ">>Makefile")) {
	print STDERR "append(Makefile): $!\n";
	return 0;
    }
    print F "VERSION=$version\n";
    close F;
    system "sync";  # to avoid random disk writes messing up timing
    if (system("make -C$tmpdir Clean") != 0) {
	print STDERR "Clean script failed!\n";
	return 0;
    }
    return 1;
}

###########################################################################
###########################################################################

sub zap
{
    # autozap: for calling from install (suppresses dep check and output)
    my ($package, $pkgdata_ref, $autozap) = @_;

    if (!$autozap) {
	if (!&package_is_installed($package)) {
	    print STDERR "Package $package is not installed.\n";
	    return 0;
	}
	my $p2 = $package;
	$p2 =~ s/(\W)/\\$1/g;
	if ($pkgdata{'Requires'} =~ /(^| )$p2( |$)/) {
	    print STDERR "Package $package may not be removed.\n";
	    return 0;
	}
	print "Checking dependencies...\n";
	return 0 if !&zap_check_dependencies($package);
	print "Removing package $package.\n";
    }
    my @files_to_zap = map {glob($_)} grep {m,^/,} @{$pkgdata{'_FILES'}};
    my @files_to_keep = ();
    if (!$purge) {
	@files_to_keep = grep {/^-/} @{$pkgdata{'_FILES'}};
    }
    s/^-// foreach @files_to_keep;
    push @files_to_keep, "$INSTDIR/$package/.pkgopt", "$INSTDIR/$package/.version" if $autozap;
    my $result = 1;
    foreach (@files_to_zap) {
	next if &zap_check_keep($_, \@files_to_keep);
	if (m,([\0-\377]*)/$,) {
	    my $dir = $1;
	    next if &zap_check_keep($dir, \@files_to_keep);  # just in case
	    &zapdir($dir, \@files_to_keep) or $result = 0;
	} else {
	    &zapfile($_) or $result = 0;
	}
    }
    return $result;
}

###########################################################################

sub zap_check_dependencies
{
    my ($package) = @_;
    my $abort = 0;

    my $package_regex = $package;
    $package_regex =~ s/(\W)/\\$1/g;
    foreach $p2 (&get_installed_packages()) {
	my %p2data = read_package_data($p2);
	foreach (split(/\s+/, $p2data{'Requires'})) {
	    if (/(^|\|)$package_regex(\||$)/) {
		my $ok = 0;
		foreach (split(/\|/)) {
		    if ($_ ne $package && &package_is_installed($_)) {
			$ok = 1;
			last;
		    }
		}
		if (!$ok) {
		    print STDERR "Package $package is required by package $p2.\n";
		    $abort = 1;
		}
	    }
	}
	foreach (split(/\s+/, $p2data{'Suggests'})) {
	    if (/(^|\|)$package_regex(\||$)/) {
		my $ok = 0;
		foreach (split(/\|/)) {
		    if ($_ ne $package && &package_is_installed($_)) {
			$ok = 1;
			last;
		    }
		}
		if (!$ok) {
		    if ($dont_ask < 0) {
			print STDERR "Package $package is suggested by package $p2, skipping.\n";
			$abort = 1;
		    } elsif ($dont_ask) {
			print STDERR "Warning: package $package is suggested by package $p2, removing anyway.\n";
		    } else {
			print STDERR "Package $package is suggested by package $p2.\n";
			if (!$abort) {
			    my $yn;
			    do {
				print STDERR "Remove anyway? [Yn] ";
				$yn = <STDIN>;
				chop $yn;
			    } while ($yn ne "" && $yn !~ /^[yn]/i);
			    $abort = 1 if $yn =~ /^n/i;
			}
		    }
		}
	    }
	}
    }
    print STDERR "Dependency check failed, aborting.\n" if $abort;
    return !$abort;
}

###########################################################################

# check whether a file/directory should be kept; returns 1 to keep, 0 to zap
sub zap_check_keep
{
    my ($path, $keep_ref) = @_;
    local $_;

    foreach (@$keep_ref) {
	my $pattern = $_;
	$pattern =~ s/(\W)/\\$1/g;
	$pattern =~ s/\\\?/./g;
	$pattern =~ s/\\\*/.*/g;
	return 1 if $path =~ /^$pattern$/;
    }
    return 0;
}

###########################################################################

# delete a file/directory; if successful (or file doesn't exist), return 1,
# else print an error message and return 0
sub zapfile
{
    my ($zap) = @_;
#DEBUG#print "zapping $zap";my $zzz=<STDIN>;
    return 1 if !lstat($zap);
    if (-d _) {
	if (!rmdir($zap)) {
	    print STDERR "rmdir($zap): $!\n";
	    return 0;
	}
    } else {
	if (!unlink($zap)) {
	    print STDERR "unlink($zap): $!\n";
	    return 0;
	}
    }
    return 1;
}

###########################################################################

# delete a directory (recursively), except for given files; returns 1 if
# the directory was deleted, -1 if files to be kept are left but no errors
# occurred, or 0 if an error occurred
sub zapdir
{
    my ($zap, $keep_ref) = @_;
    my $result = 1;
    local *DIR;

    return 1 if !lstat($zap);
    if (!opendir(DIR, $zap)) {
	print STDERR "opendir($zap): $!\n";
	return 0;
    }
    foreach (readdir(DIR)) {
	next if $_ eq "." || $_ eq "..";
	if (&zap_check_keep("$zap/$_", $keep_ref)) {
	    $result = -1 if $result == 1;
	    next;
	}
	if (-d "$zap/$_") {
	    if (&zap_check_keep("$zap/$_/", $keep_ref)) {
		$result = -1 if $result == 1;
		next;
	    }
	    my $res2 = &zapdir("$zap/$_", $keep_ref);
	    $result = 0 if $res2 == 0;
	    $result = -1 if $res2 == -1 && $result == 1;
	} else {
	    &zapfile("$zap/$_") or $result = 0;
	}
    }
    closedir DIR;
    if ($result > 0) {
	&zapfile($zap) or $result = 0;
    }
    return $result;
}

###########################################################################
###########################################################################

sub do_open
{
    local ($handler, $host, $port, $pkgref) = @_;

    eval {
	local $SIG{ALRM} = sub {die "alarm clock"};
	alarm $waittime;
	&{$$handler{'open'}}($host, $port, $pkgref) or return 0;
	alarm 0;
	return 1;
    } or return 0;
    return $@ ? 0 : 1;
}

###########################################################################
