#! /bin/sh -
# Copyright (C) 2016-2020 Matias Fonzo <selk@dragora.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

# EXIT STATUS
# 0  = Successful completion
# 1  = Minor common errors (e.g: help usage, support not available)
# 2  = Command execution error
# 3  = Integrity check error for compressed files
# 4  = File empty, not regular, or expected
# 5  = Empty or not defined variable
# 6  = Package already installed
# 10 = Network manager error

PROGRAM="${0##*/}"

LC_ALL=C;	# Override locale settings.
umask 022;	# Remove write permission for group and other.
export LC_ALL

### Functions

usage()
{
    printf '%s' \
"Usage: $PROGRAM COMMAND [OPTIONS] [FILE]...
"'A simple but well-integrated package manager.

List of commands:
  warn                     Warn about files that will be installed
  install                  Install packages
  remove                   Remove packages
  upgrade                  Upgrade packages
  extract                  Extract packages for debugging purposes
  create                   Create a .tlz package from directory
  build                    Build packages using recipe names
  order                    Resolve build order through .order files

Options when installing, removing, or upgrading software packages:
  -f, --force              Force upgrade of pre-existing packages
  -k, --keep               Keep package directory when remove/upgrade
  -p, --prune              Prune conflicts
  -P, --packagedir=<dir>   Set directory for package installations
  -t, --targetdir=<dir>    Set target directory for symbolic links
  -r, --rootdir=<dir>      Use the fully qualified named directory as
                           the root directory for all qi operations
                           Note: the target directory and the package
                           directory will be relative to the specified
                           directory, excepting the graft log file

Options when building software packages using recipes:
  -a, --architecture       Set architecture name for the package
  -j, --jobs               Parallel jobs for the compiler
  -k, --keep               Keep ${srcdir} or ${destdir} when build
  -S, --skip-questions     Skip questions on completed recipes
  -1, --increment          Increment release number (${release} + 1)
  -n, --no-package         Do not create a .tlz package
  -i, --install            Install package after the build
  -u, --upgrade            Upgrade package after the build
  -o, --outdir=<dir>       Where the packages produced will be written
  -w, --worktree=<dir>     Where archives, patches, recipes are expected
  -s, --sourcedir=<dir>    Where compressed sources will be found

Other options:
  -N, --no-rc              Do not read the configuration file
  -v, --verbose            Be verbose (an extra -v gives more)
  -L, --show-location      Print default directory locations and exit
  -h, --help               Display this help and exit
  -V, --version            Output version information and exit

Some influential environment variables:
  TMPDIR                   Temporary directory for sources
  QICFLAGS                 C compiler flags
  QICXXFLAGS               C++ compiler flags
  QILDFLAGS                Linker flags
  SOURCE_DATE_EPOCH        Last modification time for created packages

When FILE is -, read standard input.

Exit status: 0 for a normal exit, 1 for minor common errors (help usage,
support not available, etc), 2 to indicate a command execution error;
3 for integrity check error on compressed files, 4 for empty, not
regular, or expected files, 5 for empty or not defined variables,
6 when a package already exist, 10 for network manager errors.

Qi home page: https://www.dragora.org
'
}

warn()
{
    printf '%s\n' "$@" 1>&2
}

is_readable()
{
    if test -e "$1"
    then
        if ! test -r "$1"
        then
            echo "${PROGRAM}: cannot read ${1}: Permission denied" 1>&2
            return 1
        fi
    else
        echo "${PROGRAM}: cannot access ${1}: No such file or directory" 1>&2
        return 1
    fi
}

# Portable alternative to the file operator -nt (among shells)
is_newer()
{
    if test -n "$(find "$1" -prune -newer "$2" -print)"
    then
        return 0
    fi
    return 1
}

# Determine whether $2 matches pattern $1
fnmatch()
{
    case $2 in
    $1)
        return 0
        ;;
    *)
        return 1
        ;;
    esac
}

chkstatus_or_exit()
{
    status=$?

    if test $status -ne 0
    then
        echo "^ Return status = $status" 1>&2
        exit "${1-2}";	# If not given, defaults to 2
    fi

    unset -v status
}

readconfig()
{
    if test "$readconfig" = readconfig
    then
        is_readable "$HOME/.qirc" 2> /dev/null && _rcfile="$HOME/.qirc";

        echo "Importing configuration file \`${_rcfile}' ..."

        test -f "$_rcfile" || {
            warn "$_rcfile is not a regular file."
            return 1;
        }

        . "$_rcfile"
    fi
}

### Mode functions

mode_build()
{
    recipe=$1

    echo ""
    echo "{@} Building \"${recipe}\" ..."

    # A recipe is any valid regular file.  Qi sets priorities for reading
    # a recipe, the order in which qi looks for a recipe is:
    #
    # 1. Current working directory.
    #
    # 2. If the specified path name does not contain "recipe" as the last
    # component.  Qi will complete it by adding "recipe" to the path
    # name.
    #
    # 3. If the recipe is not in the current working directory, it will be
    # searched under '${worktree}/recipes'.  The last component will be
    # completed adding "recipe" to the specified path name.

    if test ! -f "$recipe"
    then
        if test -f "${recipe}/recipe"
        then
            recipe="${recipe}/recipe"
        elif test -f "${worktree}/recipes/${recipe}/recipe"
        then
            recipe="${worktree}/recipes/${recipe}/recipe"
        fi
    fi

    test -f "$recipe" || {
        warn "\`${recipe}' is not a regular file."
        return 4
    }

    # Complain if the file name is not "recipe"

    if test "${recipe##*/}" != recipe
    then
        warn "\`${recipe}' is not a valid recipe name."
        return 4
    fi

    # Start preparations to import the recipe

    # Get working directory and base name of the recipe

    CWD="$(dirname -- "$recipe")"
    recipe="$(basename -- "$recipe")"

    # Check readability for load the recipe on success

    is_readable "${CWD}/$recipe" || exit 4

    # Re-create external directories
    mkdir -p -- "${worktree}/archive" \
                "${worktree}/patches" \
                "${worktree}/recipes" \
                "$tardir"

    # Variables treatment for the current and the next recipe.
    #
    # Unset special variables that can only be predefined in
    # the recipe and does not come from '${sysconfdir}/qirc'

    unset -v \
     srcdir destdir pkgname pkgversion pkgcategory program version release \
     fetch description homepage license replace full_pkgname \
     CFLAGS CXXFLAGS LDFLAGS

    # The following variables must be restored, later
    save_arch="${save_arch:=$arch}"
    save_jobs="${save_jobs:=$jobs}"
    save_outdir="${save_outdir:=$outdir}"
    save_opt_nopkg="${save_opt_nopkg:=$opt_nopkg}"

    # Reset variable values in case of return
    arch="$save_arch"
    jobs="$save_jobs"
    outdir="$save_outdir"
    opt_nopkg="$save_opt_nopkg"

    # The following variables cannot be redefined on the recipe
    readonly worktree netget rsync

    # Import the recipe

    . "${CWD}/$recipe"
    chkstatus_or_exit

    # Check for required variables
    if test -z "$program"
    then
        warn "${recipe}: The variable 'program' is not defined."
        exit 5
    fi
    if test -z "$version"
    then
        warn "${recipe}: The variable 'version' is not defined."
        exit 5
    fi
    if test -z "$release"
    then
        warn "${recipe}: The variable 'release' is not defined."
        exit 5
    fi

    # Pre-settings before to start building

    # Increment the release number if the option was given
    if test "$opt_incr_release" = opt_incr_release
    then
        release=$(( release + 1 ))
    fi

    # Allow the dot as definition for 'tardir'
    if test "$tardir" = .
    then
        tardir="$CWD"
    fi

    # Set default values for the following special variables

    pkgname="${pkgname:=$program}"
    pkgversion="${pkgversion:=$version}"
    srcdir="${srcdir:=${program}-$version}"
    destdir="${destdir:=${TMPDIR}/package-$pkgname}"

    # If 'pkgcategory' has been defined, prefix it using the "at" symbol
    if test -n "$pkgcategory"
    then
        pkgcategory="@${pkgcategory}"
    fi

    # Compose the full package name
    full_pkgname="${full_pkgname:=${pkgname}_${pkgversion}_${arch}-${release}${pkgcategory}}"

    # Use 'arch' as suffix for 'outdir' to have a well-organized package output
    outdir="${outdir}/${arch}"

    # If a package is going to be created the existence of a
    # previous build will be detected and reported.  Under normal
    # conditions the recipe is built as long as it is newer than
    # the produced package, if not, we warn to the user about it.
    # Rebuilding the package is possible (through the force ;-)

    if test "$opt_nopkg" != opt_nopkg && \
     { test "$opt_force" != opt_force && \
       test -e "${outdir}/${full_pkgname}.tlz" ; }
    then
        if is_newer "${CWD}/$recipe" "${outdir}/${full_pkgname}.tlz"
        then
            warn \
             ""                                                          \
             "The recipe is more RECENT than the detected package:"      \
             ""                                                          \
             "$( stat -c "%y %n" "${CWD}/$recipe" )"                     \
             "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )"      \
             ""                                                          \
             "    This recipe will be processed ..."                     \
             ""
        elif test -e "${CWD}/post-install" && \
             is_newer "${CWD}/post-install" "${CWD}/$recipe"
        then
            warn \
             ""                                                          \
             "The post-install script is more RECENT than the recipe:"   \
             ""                                                          \
             "$( stat -c "%y %n" "${CWD}/post-install" )"                \
             "$( stat -c "%y %n" "${CWD}/$recipe" )"                     \
             ""                                                          \
             "    The recipe will be re-processed ..."                   \
             ""
             touch "${CWD}/$recipe"
        elif test "$opt_skipqsts" = opt_skipqsts
        then
            warn "Recipe for '${full_pkgname}.tlz':  Ignored." ""
            return 0
        else
            warn \
             ""                                                          \
             "This recipe ALREADY produced a package:"                   \
             "$( stat -c "%y %n" "${outdir}/${full_pkgname}.tlz" )"      \
             ""                                                          \
             "The recipe is still OLDER than the produced package:"      \
             "$( stat -c "%y %n" "${CWD}/$recipe" )"                     \
             ""                                                          \
             "    Probably nothing has changed."                         \
             ""

            # In non-interactive mode, the user is asked about
            # rebuilding the package.  In interactive mode,
            # the user need to pass the option explicitly
            if test ! -t 0
            then
                printf '%s\n' \
                 "Do you want to rebuild this package?"   \
                 "1) Yes, built it"                       \
                 "2) No, skip it [default]"               \
                 "3) Resume, skipping completed recipes"  \
                 "Enter an option number:" > /dev/tty
                IFS= read -r ANSWER < /dev/tty || exit 2;
                case $ANSWER in
                1*)
                    echo "$ANSWER" > /dev/tty
                    unset -v ANSWER
                    ;;
                3*)
                    unset -v ANSWER
                    echo "=== Building UNPROCESSED/MODIFIED recipes ..." > /dev/tty
                    echo ""
                    opt_skipqsts=opt_skipqsts
                    readonly opt_skipqsts
                    return 0
                    ;;
                *)
                    unset -v ANSWER
                    echo "Recipe for '${full_pkgname}.tlz':  Cancelled." > /dev/tty
                    echo ""
                    return 0
                    ;;
                esac
            else
                warn "Use the --force option to reprocess ${CWD}/$recipe." ""
                return 6;
            fi
        fi
    fi

    # Fetch remote sources

    echo "=== Fetching remote sources if needed ..."
    if test -n "$fetch"
    then
        for origin in $fetch
        do
            _source="${origin##*/}"

            echo "=== Looking for \"$_source\" ..."

            echo "=== Verifying checksum file \`${tardir}/${_source}.sha256'"
            if test -e "${tardir}/${_source}.sha256"
            then
                ( cd -- "$tardir" && sha256sum - ) < "${tardir}/${_source}.sha256"
                chkstatus_or_exit
                continue;
            else
                warn "${_source}.sha256: Checksum file does not exist, yet."
            fi

            # Download source or resume if allowed

            if test ! -e "${tardir}/$_source"
            then
                warn "    Can't find \"${_source}\" at \"${tardir}\";" \
                     "attempting to get it from ${origin%/*} ..."
            fi

            case $origin in
            rsync://*)
                    (
                        cd -- "$tardir" && $rsync "$origin" || exit $?
                        sha256sum "$_source" > "${_source}.sha256"
                    );  chkstatus_or_exit 10
                ;;
            *://*)
                    (
                        cd -- "$tardir" && $netget "$origin" || exit $?
                        sha256sum "$_source" > "${_source}.sha256"
                    );  chkstatus_or_exit 10
                ;;
            *)
                warn "${PROGRAM}: Unrecognized protocol for ${origin}."
                exit 4
            esac
        done
        unset -v origin _source
    else
        warn "${recipe}: The variable 'fetch' is empty."
    fi

    # Prepare special directories for build the source,
    # the destination and the output of the package

    echo "=== Preparing directories ..."

    if test -z "$keep_srcdir"
    then
        if test -e "${TMPDIR}/$srcdir"
        then
            rm -r -- "${TMPDIR:?}/$srcdir" || chkstatus_or_exit
            echo "removed directory: '${TMPDIR}/$srcdir'"
        fi
    else
        warn \
         "WARNING: The variable 'keep_srcdir' has been set (${keep_srcdir})."
    fi

    if test -z "$keep_destdir"
    then
        if test -e "$destdir"
        then
            rm -r -- "$destdir" || chkstatus_or_exit
            echo "removed directory: '$destdir'"
        fi
        mkdir -p -- "$destdir" || chkstatus_or_exit
        echo "mkdir: created directory '$destdir'"
    fi

    if test ! -e "$outdir"
    then
        mkdir -p -- "$outdir" || chkstatus_or_exit
        echo "mkdir: created directory '$outdir'"
    fi

    echo "=== Changing to '${TMPDIR}' ..."
    cd -- "$TMPDIR" || chkstatus_or_exit

    # Set trap before to run the build() function in order
    # to catch the return status, exit code 2 if fails

    trap 'chkstatus_or_exit 2' EXIT HUP INT QUIT ABRT TERM

    # Determine if the debugging indicators of the shell should be
    # retained, assuming that it has been previously passed
    case $- in *x*)
        _xtrace_flag=_xtrace_flag_is_set ;;
    esac

    echo "=== Running the 'build' function ..."
    build
    unset -f build

    # Turn off possible shell flags coming from the recipe

    set +e
    if test "${_xtrace_flag:+$_xtrace_flag}" != _xtrace_flag_is_set
    then
        set +x
    fi

    # Reset given signals
    trap - EXIT HUP INT QUIT ABRT TERM

    # If 'destdir' is empty, the package won't be created
    if rmdir -- "$destdir" 2> /dev/null
    then
        warn "The package \"${full_pkgname}.tlz\" won't be created.  'destdir' is empty."
        opt_nopkg=opt_nopkg
    fi

    # Create (make) the package

    if test "$opt_nopkg" != opt_nopkg
    then
        # Edit the recipe when 'release' is incremented
        if test "$opt_incr_release" = opt_incr_release
        then
            echo ",s/^\\(release\\)=.*/\\1=${release}/"$'\nw' | \
             ed "${CWD}/$recipe" || chkstatus_or_exit
        fi

        mkdir -p -- "${destdir}/var/lib/qi" || chkstatus_or_exit

        # Include a copy of the recipe into the package
        cp -p "${CWD}/$recipe" \
              "${destdir}/var/lib/qi/${full_pkgname}.recipe" && \
         chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.recipe"
        chkstatus_or_exit

        # Detect post-install script for inclusion

        if test -f "${CWD}/post-install"
        then
            echo "${CWD}/post-install:  Detected."
            cp -p "${CWD}/post-install" \
                  "${destdir}/var/lib/qi/${full_pkgname}.sh" && \
             chmod 644 "${destdir}/var/lib/qi/${full_pkgname}.sh"
            chkstatus_or_exit
        fi

        # Detect declared package name(s) for later replacement

        if test -n "$replace"
        then
            warn \
             "=== The following package names has been declared for replacement:" \
             "$replace"

            : > "${destdir}/var/lib/qi/${full_pkgname}.replace"
            for item in $replace
            do
                printf '%s\n' "$replace" >> \
                 "${destdir}/var/lib/qi/${full_pkgname}.replace"
            done
            unset -v item
        fi

        # Create (external) meta file for package information,
        # make a copy of it for the package database
        echo "=== Creating meta file ${full_pkgname}.tlz.txt ..."
        do_meta > "${outdir}/${full_pkgname}.tlz.txt" || chkstatus_or_exit
        cp -p "${outdir}/${full_pkgname}.tlz.txt" \
          "${destdir}/var/lib/qi/${full_pkgname}.txt" || chkstatus_or_exit

        # Produce the package
        cd -- "$destdir" && mode_create "${outdir}/${full_pkgname}.tlz"
    fi

    # Back to the current working directory
    cd -- "$CWD" || chkstatus_or_exit

    echo "=== Deleting 'srcdir', 'destdir' ..."
    if test "$opt_keep" != opt_keep
    then
        if test -z "$keep_srcdir"
        then
            srcdir="${srcdir%%/*}";	# Directory name without parents.
            if test -e "${TMPDIR}/$srcdir"
            then
                rm -r -- "${TMPDIR:?}/$srcdir" || chkstatus_or_exit
                echo "removed directory: '${TMPDIR}/$srcdir'"
            fi
        fi

        if test -z "$keep_destdir"
        then
            if test -e "$destdir"
            then
                rm -r -- "$destdir" || chkstatus_or_exit
                echo "removed directory: '$destdir'"
            fi
        fi
    else
        warn \
         "    Request via --keep to preserve 'srcdir' or 'destdir' has been made:" \
         "${TMPDIR}/$srcdir" \
         "$destdir"
    fi

    # Install or upgrade the package after build
    if test "$opt_nopkg" != opt_nopkg
    then
        if test "$opt_install" = opt_install
        then
            mode_install "${outdir}/${full_pkgname}.tlz"
        elif test "$opt_upgrade" = opt_upgrade
        then
            mode_upgrade "${outdir}/${full_pkgname}.tlz"
        fi
    fi

    echo "{@} All done for \"${CWD}/${recipe}\"."
    echo ""
}

mode_create()
{
    directory="$(dirname -- "$1")"

    # Perform sanity checks

    if ! fnmatch '/?*' "$directory"
    then
        warn "${PROGRAM}: Output directory \`${directory}' is not fully qualified"
        exit 4
    fi
    is_readable "$directory" || exit 4

    name="$(basename -- "$1")"

    echo ""
    echo "{#} Creating package name \`${name}' ..."

    if test "$name" = "${name%.tlz}"
    then
        warn "Package format '$name' not supported." \
             "It should be \"name_version_architecture-release[@pkgcategory].tlz\""
        exit 4
    fi

    # Pass extra options for tarlz(1)
    if test -n "$SOURCE_DATE_EPOCH"
    then
        tarlz_opts="--mtime=@${SOURCE_DATE_EPOCH}"
    fi

    ( umask 022 ; tarlz --solid -9 $tarlz_opts -cvf - -- * ) \
     > "${directory}/$name"
    chkstatus_or_exit 3
    unset -v tarlz_opts

    ( cd -- "$directory" && sha256sum "$name" > "${name}.sha256" )
    chkstatus_or_exit 4

    echo "{#} Package \"${name}\" created on ${directory}."

    # Remove used variables
    unset -v directory name

    echo ""
}

mode_remove()
{
    expunge="${packagedir}/$(basename -- "$1" .tlz)"

    echo ""
    echo "<<< Removing package \`$rootdir${expunge}' ..."

    # Complain if the package directory does not exist

    test -e "$rootdir${expunge}" || {
        warn "Package directory '$rootdir${expunge}' does not exist."
        return 4
    }

    # Complain if the package directory cannot be well-read

    is_readable "$rootdir${expunge}" || exit 4

    # Remove package from Graft control

    # Scan for possible conflicts, stop if arise
    if test "$opt_prune" != opt_prune
    then
        echo "=== Checking for possible conflicts ..."
        if graft -d -n $graft_r -t "$targetdir" "$expunge" 2>&1 | \
         grep "^CONFLICT"
        then
            warn "" \
             "    A conflict occurred during uninstallation;"  \
             "Unless the --prune option is given, this package will be PRESERVED."
            return 6;
        fi
    fi

    # Remove objects (files, links or directories) from the target
    # directory that are in conflict with the package directory

    echo "=== Pruning any conflict ..."
    graft -p -D -u $graft_r -t "$targetdir" "$expunge"
    chkstatus_or_exit 2

    echo "=== Disabling links ..."
    graft -d -D -u $graft_v $graft_r -t "$targetdir" "$expunge"
    chkstatus_or_exit 2

    # Delete package directory
    if test "$opt_keep" != opt_keep
    then
        echo "=== Deleting package directory, if exists as such ..."
        if test -e "${rootdir}$expunge"
        then
            rm -r -- "${rootdir}$expunge" || chkstatus_or_exit
            echo "removed directory: '${rootdir}$expunge'"
        fi
    fi

    # Remove used variables
    unset -v expunge

    echo ""
}

mode_install()
{
    # Complain if the package cannot be well-read

    is_readable "$1" || exit 4

    # Complain if the package does not end in .tlz

    if ! fnmatch '*.tlz' "$1"
    then
        warn "\`${1}' does not end in .tlz"
        return 4
    fi

    # Make preparations to install the package

    # Get the filename
    name="$(basename -- "$1" .tlz)"

    echo ""
    echo ">>> Installing package \`${name}' ..."

    echo "=== Checking tarball integrity ..."
    tarlz --missing-crc -tf "$1" > /dev/null
    chkstatus_or_exit 3

    # To accept random directory from the upgrade mode
    _packagedir="$2"
    _packagedir="${_packagedir:=$packagedir}"

    # Create package directory using 'name'
    if ! test -d "$rootdir${_packagedir}/$name"
    then
        mkdir -p -- "$rootdir${_packagedir}/$name" || chkstatus_or_exit
        echo "mkdir: created directory '$rootdir${_packagedir}/$name'"
    fi

    # Scan for possible conflicts, stop if arise
    if test "$opt_prune" != opt_prune
    then
        echo "=== Checking for possible conflicts ..."
        if graft -i -n $graft_r -t "$targetdir" "${_packagedir}/$name" 2>&1 | \
         grep "^CONFLICT"
        then
            warn "" \
             "    A conflict occurred during installation;"  \
             "Unless the --prune option is given, this package won't be LINKED."
             return 6;
        fi
    fi

    echo "=== Decompressing package ..."
    ( cd -- "$rootdir${_packagedir}/$name" && tarlz -xpf - ) < "$1"
    chkstatus_or_exit 3

    # Transite package to Graft control

    # Remove objects (files, links or directories) from the target
    # directory that are in conflict with the package directory
    echo "=== Pruning any conflict ..."
    graft -p -D -u $graft_r -t "$targetdir" "${_packagedir}/$name"
    chkstatus_or_exit 2

    echo "=== Enabling symbolic links ..."
    graft -i -P $graft_v $graft_r -t "$targetdir" "${_packagedir}/$name"
    chkstatus_or_exit 2

    # Avoid unnecessary runs coming from the upgrade mode,
    # this is when the incoming package is **pre-installed**

    if test "$_isUpgrade" != _isUpgrade.on
    then
        # Show package description
        if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.txt"
        then
            awk '/^#/' "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.txt"
        elif test -r "${1}.txt"
        then
            # From external meta file (current directory)
            awk '/^#/' "${1}.txt"
        else
            warn "Description file not found for '$name'."
        fi

        # Check and run the post-install script if exist
        if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.sh"
        then
            echo "=== Running post-install script for \`${name}' ..."
            (
                # Rely on 'targetdir' if 'rootdir' is empty
                cd -- "${rootdir:=$targetdir}"/ && \
                 . "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.sh"
            )
        fi

        # Check if there are declared packages for replacement
        if test -r "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.replace"
        then
            while read -r line
            do
                for replace in "$rootdir${_packagedir}/${line%%_*}"_*
                do
                    if ! test -e "$replace"
                    then
                        warn "<^> Declared package \`${replace}' does not exist. (ignoring)"
                        continue;
                    fi

                    replace="${replace##*/}"

                    # The search for the package to be replaced cannot
                    # be the same to the incoming package, even to the
                    # temporary location coming from the upgrade mode
                    if test "$replace" = "$name" || \
                       test "_x_${replace}" = "_x_${PRVLOC##*/}"
                    then
                        continue;
                    fi

                    warn "WARNING: Replacing package \`${replace}' ..."

                    # Since the links belongs to the new package, only
                    # those which are not in conflict can be deleted.
                    # To complete, we will remove the package directory

                    graft -d -D -u $graft_r \
                     -t "$targetdir" "$replace" > /dev/null 2>&1

                    rm -rf -- "$rootdir${_packagedir}/$replace"
                done
            done < "$rootdir${_packagedir}/${name}/var/lib/qi/${name}.replace"
        fi
    fi

    # Remove used variables
    unset -v name _packagedir

    echo ""
}

mode_order()
{
    # Complain if the file cannot be well-read

    is_readable "$1" || exit 4

    # Complain if the file does not end in .order

    if ! fnmatch '*.order' "$1"
    then
        warn "\`${1}' does not end in .order"
        return 4
    fi

    # Get a clean list of the file while printing its contents in reverse
    # order. The last `awk 'in the pipeline eliminates the non-consecutive
    # lines, the duplicates. Blank lines, colons and parentheses are
    # simply ignored, comment lines beginning with '#' are allowed

    awk \
    '{ gsub( /:|^#(.*)$|\([^)]*)|^$/,"" ); for( i=NF; i > 0; i-- ) print $i }' \
     "$1" | awk '!s[$0]++'
}

mode_upgrade()
{
    # Complain if the package does not end in .tlz

    if ! fnmatch '*.tlz' "$1"
    then
        warn "\`${1}' does not end in .tlz"
        return 4
    fi

    # Get the filename
    incoming="$(basename -- "$1" .tlz)"

    echo ""
    echo "{%} Upgrading package \`${incoming}' ..."

    # Check package pre-existence
    if test "$opt_force" != opt_force && \
       test -e "$rootdir${packagedir}/$incoming"
    then
        warn \
         ""                                                                \
         "    The package to be upgraded already exist;"                   \
         "Unless the --force option is given, this package won't be UPGRADED."
        return 6;
    fi

    # Ignore some signals until the upgrade process is complete
    trap "" HUP INT QUIT ABRT TERM

    # Check blacklisted packages

    echo "=== Checking blacklist ..."
    for item in $blacklist
    do
        case $item in
        ${incoming}*)
            warn \
             "Package name declared on the blacklist \"${incoming}\"."      \
             ""                                                             \
             "  This package will be INSTALLED instead of being upgraded."
             opt_prune=opt_prune mode_install "$1"
             return 0
            ;;
        esac
    done
    unset -v item

    # Prepare the package to install it in a temporary but custom location

    # Set random directory under 'rootdir/packagedir' using 'incoming' as name
    PRVLOC=$(mktemp -dp "$rootdir${packagedir}" ${incoming}.XXXXXXXXXXXX) || exit 2

    echo "=== Pre-installing package in temporary location ..."
    opt_prune=opt_prune		# Turn on prune operation.
    _isUpgrade=_isUpgrade.on mode_install "$1" "$PRVLOC" > /dev/null
    _isUpgrade=_isUpgrade.off

    echo "=== Looking for installations under the same name ..."
    for long_name in "$rootdir${packagedir}/${incoming%%_*}"_*
    do
        # Check if it is a valid directory and if the incoming
        # package is not equivalent to a previous installation;
        # also check if the package to be deleted is equivalent
        # to the temporary, private location.

        if ! test -d "$long_name"
        then
            continue;
        fi
        if test "${long_name##*/}" = "$incoming"
        then
            continue;
        fi
        if test "$long_name" = "$PRVLOC"
        then
            continue;
        fi

        warn "Proceeding to the removal of ${long_name} ..."
        # A package directory will be preserved if --keep is given
        mode_remove "${long_name##*/}" > /dev/null
    done
    unset -v long_name

    # Re-install the package removing the temporary location

    mode_install "$1"
    opt_prune=opt_prune.off	# Turn off prune operation.

    echo "=== Deleting temporary location ..."
    rm -rf -- "$PRVLOC" || chkstatus_or_exit
    echo "removed directory: '$PRVLOC'"

    echo "{%} Upgraded from \"${1}\"."

    # Remove remaining variables
    unset -v incoming PRVLOC

    echo ""

    # Reset given signals
    trap - HUP INT QUIT ABRT TERM
}

mode_warn()
{
    # Complain if the package cannot be well-read

    is_readable "$1" || exit 4

    # Complain if the package does not end in .tlz

    if ! fnmatch '*.tlz' "$1"
    then
        warn "\`${1}' does not end in .tlz"
        return 4
    fi

    # List content of files excluding directories
    while test -f "$1"
    do
        tarlz -tvvf "$1" | awk '!/^drwx/'
        chkstatus_or_exit 3
        shift;
    done
    return 0
}

mode_extract()
{
    # Perform sanity checks before package extraction

    is_readable "$1" || exit 4

    test -f "$1" || {
        warn "\`${1}' is not a regular file."
        return 4
    }

    # Preparations to extract the package

    name="$(basename -- "$1" .tlz)"

    # Set random directory under 'TMPDIR' using 'name'
    PRVDIR=$(mktemp -dp "$TMPDIR" ${name}.XXXXXXXXXXXX) || exit 2

    # Trap to remove 'PRVDIR' on disruptions
    trap 'rm -rf -- "$PRVDIR"' HUP INT ABRT TERM

    # Create 'PRVDIR' removing access for all but user
    ( umask 077 ; mkdir -- "$PRVDIR" )
    mkdir -p -- "$PRVDIR"
    chmod 700 -- "$PRVDIR"

    echo ""
    echo "--- Extracting package \`${name}' ..."

    ( umask 000 ; cd -- "$PRVDIR" && tarlz -xvf - ) < "$1"
    if test $? -ne 0
    then
        # Try to remove (empty) 'PRVDIR' on failure
        rmdir -- "$PRVDIR"
        exit 3;
    fi

    echo ""
    echo "\"${name}\" has been extracted on \`${PRVDIR}'"

    # Reset given signals
    trap - HUP INT ABRT TERM

    # Remove used variables
    unset -v name PRVDIR
}

### Extra functions to be used during the modes

unpack()
{
    for file in "$@"
    do
        case $file in
        *.tar)
            tar -tf "$file" > /dev/null && \
            tar -xpf "$file"
            chkstatus_or_exit 3
            ;;
        *.tar.gz | *.tgz | *.tar.Z )
            gzip -cd "$file" | tar -tf - > /dev/null && \
            gzip -cd "$file" | tar -xpf -
            chkstatus_or_exit 3
            ;;
        *.tar.bz2 | *.tbz2 | *.tbz )
            bzip2 -cd "$file" | tar -tf - > /dev/null && \
            bzip2 -cd "$file" | tar -xpf -
            chkstatus_or_exit 3
            ;;
        *.tar.lz | *.tlz )
            lzip -cd "$file" | tar -tf - > /dev/null && \
            lzip -cd "$file" | tar -xpf -
            chkstatus_or_exit 3
            ;;
        *.tar.xz | *.txz )
            xz -cd "$file" | tar -tf - > /dev/null && \
            xz -cd "$file" | tar -xpf -
            chkstatus_or_exit 3
            ;;
        *.zip | *.ZIP )
            unzip -t "$file" > /dev/null && \
            unzip "$file" > /dev/null
            chkstatus_or_exit 3
            ;;
        *.gz)
            gzip -t "$file" && \
            gzip -cd "$file" > "$(basename -- "$file" .gz)"
            chkstatus_or_exit 3
            ;;
        *.Z)
            gzip -t "$file" && \
            gzip -cd "$file" > "$(basename -- "$file" .Z)"
            chkstatus_or_exit 3
            ;;
        *.bz2)
            bzip2 -t "$file" && \
            bzip2 -cd "$file" > "$(basename -- "$file" .bz2)"
            chkstatus_or_exit 3
            ;;
        *.lz)
            lzip -t "$file" && \
            lzip -cd "$file" > "$(basename -- "$file" .lz)"
            chkstatus_or_exit 3
            ;;
        *.xz)
            xz -t "$file" && \
            xz -cd "$file" > "$(basename -- "$file" .xz)"
            chkstatus_or_exit 3
            ;;
        *)
            warn "${PROGRAM}: cannot unpack ${file}: Unsupported extension"
            exit 1
        esac
    done
    unset -v file
}

do_meta()
{
    # Extract information from the recipe to create the meta file.
    #
    # The package description is pre-formatted in 78 columns,
    # the '#' character and a space is added as prefix to conform
    # the 80 columns in total

    printf '%s\n' "$description" | fold -w 78 | awk '$0="# " $0'
    printf '%s' \
"
QICFLAGS=\"$QICFLAGS\"
QICXXFLAGS=\"$QICXXFLAGS\"
QILDFLAGS=\"$QILDFLAGS\"
pkgname=$pkgname
pkgversion=$pkgversion
arch=$arch
release=$release
pkgcategory=\"${pkgcategory#@*}\"
full_pkgname=$full_pkgname
blurb=\"$(printf '%s\n' "$description" | sed -e '/^$/d;2q')\"
homepage=\"$homepage\"
license=\"$license\"
fetch=\"$fetch\"
replace=\"$replace\"
"
}

### Default values

packagedir=@PACKAGEDIR@
targetdir=@TARGETDIR@
blacklist="perl5 graft tarlz plzip musl glibc coreutils bash mksh"
readconfig=readconfig
_rcfile=@SYSCONFDIR@/qirc
opt_install=opt_install.off
opt_upgrade=opt_upgrade.off
opt_force=opt_force.off
opt_keep=opt_keep.off
opt_incr_release=opt_incr_release.off
opt_skipqsts=opt_skipqsts.off
opt_nopkg=opt_nopkg.off
opt_prune=opt_prune.off
verbose=0
rootdir=""
arch=@ARCH@
jobs=1
mode=""
readstdin=""
graft_v=""
graft_r=""
_isUpgrade=_isUpgrade.off
keep_srcdir=""
keep_destdir=""
TMPDIR="${TMPDIR:-/usr/src/qi/build}"
QICFLAGS="${QICFLAGS:--g0 -O2}"
QICXXFLAGS="${QICXXFLAGS:--g0 -O2}"
QILDFLAGS="${QILDFLAGS:--s}"
worktree=/usr/src/qi
tardir=${worktree}/sources
outdir=/var/cache/qi/packages
netget="wget -c -w1 -t3 --no-check-certificate"
rsync="rsync -v -a -L -z -i --progress"
configure_args="--prefix=@PREFIX@ --libexecdir=@LIBEXECDIR@ --bindir=@BINDIR@ --sbindir=@SBINDIR@ --sysconfdir=@SYSCONFDIR@ --localstatedir=@LOCALSTATEDIR@"
infodir=@INFODIR@
mandir=@MANDIR@
docdir=@DOCDIR@

# Store (default) directory locations
QI_TARGETDIR=$targetdir
QI_PACKAGEDIR=$packagedir
QI_WORKTREE=$worktree
QI_TARDIR=$tardir
QI_OUTDIR=$outdir

### Parse commands and options

_validate_mode()
{
    if test -n "$mode"
    then
        warn "${PROGRAM}: First defined command: ${mode#*_}" \
             "Switching to another command is not allowed (${1})."
        exit 1
    fi
}

_validate_option()
{
    if test -z "$2"
    then
        warn "${PROGRAM}: The '${1}' option requires an argument" \
             "Try '${PROGRAM} --help' for more information."
        exit 1
    fi
}

_validate_directory()
{
    if test ! -d "$2"
    then
        warn "${PROGRAM}: ${1} \"${2}\" must be a valid directory name"
        exit 1
    fi
}

_validate_digit()
{
    name="$1"
    shift

    # Taken from https://mywiki.wooledge.org/BashFAQ/054
    case ${1#[-+]} in
    '')
        warn "${PROGRAM}: The '${name}' option has no defined value"
        exit 1
        ;;
    *.*.*)
        warn "${PROGRAM}: The '${name}' option has more than one decimal point on it \"${1}\""
        exit 1
        ;;
    *[!0-9]*)
        warn "${PROGRAM}: The '${name}' option contains a non-valid digit on it \"${1}\""
        exit 1
        ;;
    esac

    unset -v name
}

while test $# -gt 0
do
    case $1 in
    warn)
        _validate_mode warn
        mode=mode_warn
        ;;
    install)
        _validate_mode install
        readconfig
        mode=mode_install
        ;;
    remove)
        _validate_mode remove
        readconfig
        mode=mode_remove
        ;;
    upgrade)
        _validate_mode upgrade
        readconfig
        mode=mode_upgrade
        ;;
    extract)
        _validate_mode extract
        readconfig
        mode=mode_extract
        ;;
    create)
        _validate_mode create
        readconfig
        mode=mode_create
        ;;
    order)
        _validate_mode order
        readconfig
        mode=mode_order
        ;;
    build)
        _validate_mode build
        readconfig
        mode=mode_build
        ;;
    --no-rc | -N )
        readconfig=readconfig.off
        ;;
    --install | -i )
        opt_install=opt_install
        ;;
    --upgrade | -u )
        opt_upgrade=opt_upgrade
        ;;
    --force | -f )
        opt_force=opt_force
        ;;
    --keep | -k )
        opt_keep=opt_keep
        ;;
    --prune | -p )
        opt_prune=opt_prune
        ;;
    --packagedir | -P )
        _validate_option "$1" "$2"
        packagedir="$2"
        _validate_directory "$1" "$packagedir"
        shift
        ;;
    --packagedir=*)
        _validate_option "$1" "$2"
        packagedir="${1#*=}"
        _validate_directory "$1" "$packagedir"
        ;;
    --targetdir | -t )
        _validate_option "$1" "$2"
        targetdir="$2"
        _validate_directory "$1" "$targetdir"
        shift
        ;;
    --targetdir=*)
        _validate_option "$1" "$2"
        targetdir="${1#*=}"
        _validate_directory "$1" "$targetdir"
        ;;
    --rootdir | -r )
        _validate_option "$1" "$2"
        rootdir="$2"
        _validate_directory "$1" "$rootdir"
        shift
        ;;
    --rootdir=*)
        _validate_option "$1" "$2"
        rootdir="${1#*=}"
        _validate_directory "$1" "$rootdir"
        ;;
    --outdir | -o )
        _validate_option "$1" "$2"
        outdir="$2"
        _validate_directory "$1" "$outdir"
        shift
        ;;
    --outdir=*)
        _validate_option "$1" "$2"
        outdir="${1#*=}"
        _validate_directory "$1" "$outdir"
        ;;
    --worktree | -w )
        _validate_option "$1" "$2"
        worktree="$2"
        _validate_directory "$1" "$worktree"
        shift
        ;;
    --worktree=*)
        _validate_option "$1" "$2"
        worktree="${1#*=}"
        _validate_directory "$1" "$worktree"
        ;;
    --sourcedir | -s )
        _validate_option "$1" "$2"
        tardir="$2"
        _validate_directory "$1" "$tardir"
        shift
        ;;
    --sourcedir=*)
        _validate_option "$1" "$2"
        tardir="${1#*=}"
        _validate_directory "$1" "$tardir"
        ;;
    --architecture | -a )
        _validate_option "$1" "$2"
        arch="$2"
        shift
        ;;
    --arch=*)
        _validate_option "$1" "$2"
        arch="${1#*=}"
        ;;
    --jobs | -j )
        jobs="$2"
        _validate_digit "$1" "$jobs"
        shift
        ;;
    -j[0-9]*)
        jobs="${1#-j*}"
        _validate_digit '-j' "$jobs"
        ;;
    --jobs=*)
        jobs="${1#*=}"
        _validate_digit '--jobs=' "$jobs"
        ;;
    --no-package | -n )
        opt_nopkg=opt_nopkg
        ;;
    --increment | -1 )
        opt_incr_release=opt_incr_release
        ;;
    --skip-questions | -S )
        opt_skipqsts=opt_skipqsts
        ;;
    --verbose | -v )
        verbose=$(( verbose + 1 ))
        ;;
    -vv)
        # ^ Trick for a second -v.
        verbose=2
        ;;
    --show-location | -L )
        printf '%s\n' \
         "QI_TARGETDIR=$QI_TARGETDIR"   \
         "QI_PACKAGEDIR=$QI_PACKAGEDIR" \
         "QI_WORKTREE=$QI_WORKTREE"     \
         "QI_TARDIR=$QI_TARDIR"         \
         "QI_OUTDIR=$QI_OUTDIR"
        exit 0
        ;;
    --help | --hel | --he | --h | '--?' | -help | -hel | -he | -h | '-?' | \
     help )
        usage
        exit 0
        ;;
    --version | --versio | --versi | --vers | \
     -version |  -versio |  -versi |  -vers | -V | version )
        echo "$PROGRAM version @VERSION@"
        exit 0
        ;;
    '-')
        readstdin=readstdin
        break
        ;;
    --)
        shift
        break;	# End of options.
        ;;
    -*)
        warn "qi: Unrecognized option: $1" \
             "Try '${PROGRAM} --help' for more information."
        exit 1
        ;;
    *)
        break;	# No more options.
        ;;
    esac
    shift
done
unset -f _validate_mode _validate_option _validate_directory _validate_digit

# When there are no arguments, show the help
if test $# -eq 0
then
    usage
    exit 1
fi
unset -f usage

# Program sanity check

for need in awk basename chmod cp dirname find fold graft grep \
            mkdir mktemp rm rmdir sed sha256sum stat tarlz ; \
do
    if ! \command -v $need > /dev/null
    then
        warn "${PROGRAM}: Prerequisite \`${need}' not found in PATH"
        exit 2
    fi
done
unset -v need

# Set verbosity level/flag

if test "$verbose" -gt 0
then
    if test "$verbose" -eq 1
    then
        graft_v=-v
    else
        graft_v=-V
    fi
fi

# Read standard input if FILE is -, or when
# FILE is not connected to a terminal

if test "$readstdin" = readstdin
then
    if test -t 0
    then
        warn "qi: I won't read from a connected terminal." \
             "Try '${PROGRAM} --help' for more information."
        exit 1
    fi

    # Unset positional parameters setting $# to zero
    set --

    # Assign remaining arguments to the positional parameters
    while read -r input
    do
        set -- "$@" "$input"
    done
fi
unset -v readstdin

if test -z "$mode"
then
    warn "qi: We need at least one (valid) command." \
         "Try '${PROGRAM} --help' for more information."
    exit 4
fi

# Validate 'packagedir' and 'targetdir' as canonical directories

# The single slash '/' does not qualify here
if ! fnmatch '/?*' "$packagedir"
then
    warn "${PROGRAM}: Package directory \`${packagedir}' is not fully qualified"
    exit 4
fi
if test ! -d "$packagedir"
then
    warn "${PROGRAM}: Package directory \`${packagedir}' does not exist"
    exit 4
fi

# The single slash '/' is valid here
if ! fnmatch '/*' "$targetdir"
then
    warn "${PROGRAM}: Target directory \`${targetdir}' is not fully qualified"
    exit 4
fi
if test ! -d "$targetdir"
then
    warn "${PROGRAM}: Target directory \`${targetdir}' does not exist"
    exit 4
fi

# Validate 'rootdir' directory

if test -n "$rootdir"
then
    if test -d "$rootdir" && test "$rootdir" != /
    then
        rootdir="${rootdir%/}"	# Remove slash from the end.

        # A workaround for graft-2.13+.  The specified directory is
        # relative to the log file, we prepend it inside 'rootdir'

        eval "$(graft -L)" ; GRAFT_LOGFILE="${GRAFT_LOGFILE:=/var/log/graft}"
        mkdir -p -- "$rootdir$(dirname -- "$GRAFT_LOGFILE")" || chkstatus_or_exit

        # Compose 'rootdir' and log file option to be used with graft(1)
        graft_r="-r $rootdir -l $GRAFT_LOGFILE"

        # Unset variables coming from eval
        unset -v GRAFT_PERL GRAFT_LOGFILE GRAFT_TARGETDIR GRAFT_PACKAGEDIR
    else
        warn "${PROGRAM}: Root directory \`${rootdir}' is not fully qualified"
        exit 4
    fi
    readonly rootdir
    export rootdir
fi

# Ensure 'TMPDIR' creation to prefix temporary files

if test ! -d "$TMPDIR"
then
    mkdir -p -- "$TMPDIR" || chkstatus_or_exit
fi
readonly TMPDIR packagedir targetdir

# Process each package or recipe provided on the command-line

for package in "$@"
do
    $mode "$package"
done

