#!/bin/bash
#
# pkgmeek - a drop-in replacement for pkgmk
#     based on upkgmk (c) 2018 therealfun
#    and pkgmk (c) 2004--2022 Per Liden et al.
# Distributed under the same license, 
# https://crux.nu/gitweb/?p=tools/pkgutils.git;a=blob_plain;f=COPYING;hb=HEAD

main() {
######################## main routine ################################
local N pkg_dir src_dir work _local_ here url u pkg_utd f DIR TARGET
local errDL=0; local errUZ=0; local BSDTAR="/usr/bin/bsdtar --format=gnutar"
declare -A ZFLAG=([xz]="-J" [bz2]="-j" [gz]="-z" [lz]="--lzip" [lz4]="--lz4" [zst]="--zstd")
declare -A CMD_FLAGS
parse_options "$@"

# Exit early if refreshing an existing sha256 manifest was requested
[ "$PKGMK_REFRESH_SIG" = "yes" ] && { make_signature refresh; exit $?; }

# Read the Pkgfile to determine what to do next. But first ensure that
# it came from a trusted source (FS#1851)

[ "${CMD_FLAGS[is]}${CMD_FLAGS[us]}" ] && PKGMK_IGNORE_SIG="yes"
validate_pkgfile || exit $E_PKGFILE
[ -f .32bit ] && PKGMK_ARCH=32 || PKGMK_ARCH=64
. "Pkgfile"; . "$PKGMK_CONF"
if [ -z "$LIBC" ]; then
  LIBC=$(pkginfo -o libc.so | awk '/libc.so/ {if ($1 == "glibc"){print "gnu"} else {print $1}}')
fi

# Ensure that command-line flags take precedence
[ "${CMD_FLAGS[i]}" ] && PKGMK_INSTALL="yes"
[ "${CMD_FLAGS[u]}" ] && PKGMK_INSTALL="update"
[ "${CMD_FLAGS[d]}" ] && PKGMK_DOWNLOAD="yes"
[ "${CMD_FLAGS[do]}" ] && PKGMK_DOWNLOAD_ONLY="yes"
[ "${CMD_FLAGS[eo]}" ] && PKGMK_EXTRACT_ONLY="yes"
[ "${CMD_FLAGS[utd]}" ] && PKGMK_MTIME_ONLY="yes"
[ "${CMD_FLAGS[uf]}" ] && PKGMK_UPDATE_FOOTPRINT="yes"
[ "${CMD_FLAGS[if]}" ] && PKGMK_IGNORE_FOOTPRINT="yes"
[ "${CMD_FLAGS[in]}" ] && PKGMK_IGNORE_NEW="yes"
[ "${CMD_FLAGS[f]}" ] && PKGMK_FORCE="yes"
[ "${CMD_FLAGS[kw]}" ] && PKGMK_KEEP_WORK="yes"
[ "${CMD_FLAGS[us]}" ] && PKGMK_UPDATE_SIG="yes"
[ "${CMD_FLAGS[is]}" ] && PKGMK_IGNORE_SIG="yes"
[ "${CMD_FLAGS[cs]}" ] && PKGMK_CHECK_SIG="yes"
[ "${CMD_FLAGS[rs]}" ] && PKGMK_REFRESH_SIG="yes"
[ "${CMD_FLAGS[ns]}" ] && PKGMK_NO_STRIP="yes"
[ "${KEY_FILES[pk]}" ] && PKGMK_PUBLICKEY="${KEY_FILES[pk]}"
[ "${KEY_FILES[sk]}" ] && PKGMK_PRIVATEKEY="${KEY_FILES[sk]}"

# respect the settings for centralized source and package directories ...
[ -v pkg_dir  ] || pkg_dir="$PKGMK_PACKAGE_DIR"/
[ -w "$pkg_dir" ] || pkg_dir="$(pwd)"/
[ -v src_dir  ] || src_dir="$PKGMK_SOURCE_DIR"
[ -w "$src_dir" ] || src_dir=

# ... and for WORK_DIR in case the user wants to build in RAM
[ -v PKGMK_WORK_DIR ] && work="$PKGMK_WORK_DIR"

# Let the main routine and subsequent subroutines know what filenames
# they should expect to see on disk.
package="${name}#${version}-${release}.pkg.tar.${PKGMK_COMPRESSION_MODE}"
declare -a _local_
for (( s=0; s<${#source[@]}; s++ )); do
    case "${source[s]}" in
    http://*|https://*|ftp://*|__git__*)
        _local_[s]="${source[s]##*/}"    # strip the leading path
        # and for git sources, extract the project name:
        [[ "${source[s]}" =~ ^__git__ ]] && { _local_[s]="${_local_[s]%%\#*}";
        _local_[s]="${_local_[s]%.git}"; } ;;
    *)
        _local_[s]="${source[s]}" ;;
    esac
    [ -z "${renames[s]}" ] || [ "${renames[s]}" = "SKIP" ] || \
        _local_[s]="${renames[s]}"
done
# Example: source = ( __git__https://gitlab.com/demo-user/cool-project.git#0.4.9
#                     https://dev.big-corp.com/src/needed-library.tgz
#                     random.patch )
#         _local_ = ( cool-project
#                     needed-library.zgt  <-- renamed to prevent automatic unpacking
#                     random.patch )

# The effective user should at least have write permissions on $PWD
[ -w "$(dirname "$work")" ] || work="$(pwd)"/work
[ "$PKGMK_KEEP_WORK" = yes ] || rm -rf "$work"
mkdir -p "$work"/{src,pkg} && cd "$work"

# Skip the retrieval of sources if the user asked for '-utd' or '-uf'.
[[ "$PKGMK_MTIME_ONLY $PKGMK_UPDATE_FOOTPRINT" =~ yes ]] || \
    [ "$(type -t download_source)" = "function" ] || \
    { for (( u=0; u<${#_local_[@]}; u++ )); do
here="${_local_[u]}"; url="${source[u]}";
# at least one of the following commands should put a file of the
# appropriate name in the current directory
[ -e "$here" ] || [ "$src_dir" = "" ] || [ ! -e "$src_dir/$here" ] || ln -sf "$src_dir/$here" . ;
[ -e "$here" ] || [ ! -e "$PKGMK_ROOT/$here" ] || ln -sf "$PKGMK_ROOT/$here" . ;
[ -e "$here" ] || fetch_url "$url" "$here"; # <-- should also be able to resume partial downloads
[ -e "$here" ] || { error "failed to download $here. Check connection and try again."; errDL+=1; }
done ; [ $errDL -gt 0 ] && error "failed to download one or more sources." && exit $E_DOWNLOAD; }

# accommodate the ports that redefine download_source()
if [[ ! "$PKGMK_MTIME_ONLY $PKGMK_UPDATE_FOOTPRINT" =~ yes ]] && [ "$(type -t download_source)" = "function" ]
then
    download_source && info "Successfully obtained all needed source files." \
       || { error "failed to download sources."; exit $E_DOWNLOAD; }
fi

# Can stop here if the user asked for '-do', but honor any requests for '-um', '-uf',
# '-us', '-eo', or '-utd' by proceeding to those steps
[ "$PKGMK_DOWNLOAD_ONLY" = "no" ] || [ "$PKGMK_UPDATE_FOOTPRINT" = "yes" ] \
    || [ "$PKGMK_UPDATE_SIG" = "yes" ] || [ "$PKGMK_EXTRACT_ONLY" = "yes" ] \
    || [ "$PKGMK_MTIME_ONLY" = "yes" ] || { cleanup_work; exit 0; }

# If '-utd' was requested, check the modification times and then exit.
check_pkg_mtime; pkg_utd=$?
[ "$PKGMK_MTIME_ONLY" = "no" ] || { cleanup_work; exit $pkg_utd; }

# Take into account all the actions that can be done without extracting
# the downloaded sources
[ "$pkg_utd" = 0 ] || [ "$PKGMK_FORCE" = "yes" ] || [ "$PKGMK_EXTRACT_ONLY" = "yes" ] \
    || [[ "$PKGMK_UPDATE_FOOTPRINT $PKGMK_CHECK_SIG $PKGMK_UPDATE_SIG" =~ yes ]] \
    || [ "$PKGMK_INSTALL" != "no" ] \
    || { info "Package '$pkg_dir$package' is up to date. Use '-f' to force a rebuild."; cleanup_work; exit 0; }

# Silence the progress report if the user never intended to proceed with unpacking
[ "$pkg_utd" = 1 ] || [[ "$PKGMK_CHECK_SIG $PKGMK_UPDATE_SIG" =~ yes ]] || \
    [[ "$PKGMK_IGNORE_SIG $PKGMK_UPDATE_FOOTPRINT" =~ yes ]] \
    || echo "Checking signatures before unpacking..."

# The option -uf is meant to be used *after* a previous invocation of pkgmeek has
# alerted the user to a footprint mismatch.
if [ "$PKGMK_UPDATE_FOOTPRINT" = "yes" ]; then
    [ -f "$pkg_dir$package" ] || \
        { error "unable to update footprint. File '$package' not found.";
        cleanup_work; exit "$E_FOOTPRINT"; }
    [ "$pkg_utd" = 1 ] || [ "$PKGMK_FORCE" = "yes" ] || \
        { error "outdated package. Use '-f' to force the footprint update."; 
        cleanup_work; exit "$E_FOOTPRINT"; }
    make_footprint > "$PKGMK_ROOT/.footprint" && { info "footprint created."; exit 0; } \
    || { error "Failed to write the footprint."; cleanup_work; exit "$E_DIRPERM"; }
fi
# Updating signatures (option -us) requires only sources and footprint, not a built package.
# As with -uf, exit after fulfilling the explicit request for a manifest.
[ "$PKGMK_UPDATE_SIG" = "yes" ] && { make_signature new; cleanup_work; exit $?; }

# All the sources should be here by now, let's verify that we can trust them.
readonly cs_fail_msg="Use '--ignore-signature' to override, if you have determined integrity by other means."
[ "$PKGMK_IGNORE_SIG" = "yes" ] || { check_signature "pre-build" | parse_signify_output;
case $? in
    0) info "Sources successfully authenticated." ;;
    1) error "Signature file missing or corrupted."
        echo "$cs_fail_msg" ; cleanup_work; exit $E_SIGNATURE ;;
    2) error "Failed to authenticate remote sources using signify."
        echo "$cs_fail_msg" ; cleanup_work; exit $E_SIGNATURE ;;
esac; }
[ "$PKGMK_CHECK_SIG" = "no" ] || { cleanup_work; exit 0; } # no need to continue if the user only requested -cs

if [ "$pkg_utd" = 0 ] || [[ "$PKGMK_FORCE $PKGMK_EXTRACT_ONLY" =~ yes ]]; then
    if [ "$(type -t unpack_source)" = "function" ]; then
        SRC=$PWD/src unpack_source || errUZ+=1
    else
        for (( u=0; u<${#_local_[@]}; u++ )) ; do
        here="${_local_[u]}"
        case "$here" in
        *.tar|*.tar.gz|*.tar.Z|*.tgz|*.tar.bz2|*.tbz2|*.tar.xz|*.txz|*.tar.lzma|*.tar.lz|*.7z|*.zip|*.rpm)
            bsdtar -p -o -C src -xf "$here" || errUZ+=1 ;;
        *)
            cp -r -L "$here" src/ ;;
        esac
        done
    fi
    [ $errUZ = 0 ] && info "Sources successfully unpacked." || \
        { error "Failed to unpack all sources."; cleanup_work; exit "$E_UNPACK"; }
    [ "$PKGMK_EXTRACT_ONLY" = "no" ] || exit 0

# The actual build step! (use fakeroot when building daemon ports as an ordinary user,
#  otherwise the owner and group might not be correct)
    (SRC=$(pwd)/src; PKG=$(pwd)/pkg; cd src; set -e -x; build)
    [ $? = 0 ] && echo "Build successful. Moving on to compression." \
        || { error "Unsuccessful build!"; cleanup_work; exit "$E_BUILD"; }

    # Strip binaries
    N=$(nproc)
    [ -n "$ns_filter" ] || [ ! -f "$PKGMK_ROOT/.nostrip" ] \
            || ns_filter="| grep -v -f $PKGMK_ROOT/.nostrip"
    ns_filter+="| xargs -r -L10 -P$N file --no-buffer --separator '>'"
    ns_filter+=" -e apptype -e text -e encoding -e cdf -e compress -e tar"

    < <(eval find pkg -type f -printf "%P\n" $ns_filter) awk 'BEGIN { FS=">[ ]+" }
        $0 ~ /ELF.*executable.*not stripped/ { print "--strip-all \"" $1 "\"" }
        $0 ~ /ELF.*shared object.*not stripped/ { print "--strip-unneeded \"" $1 "\"" }
        $2 == "current ar archive" { print "--strip-debug \"" $1 "\"" }' \
        | xargs -r -L1 -P$N strip

    # Compress anything under /man that does not appear to be gzipped
    find pkg -type f -path "*/man/man*/*" \! -iname '*.gz' \
            -exec gzip -9 '{}' +

    # Ensure that symlinks point to the (now-compressed) manpages
    { while IFS="!" read -r f DIR; do
        TARGET="$(readlink -n "$DIR/$f")"
        TARGET="${TARGET##*/}"; TARGET="${TARGET%.gz}.gz";
        rm -f "$DIR/$f"
        [ -e "$DIR/$TARGET" ] && ln -sf "$TARGET" "$DIR/${f%.gz}.gz"
    done } < <(find pkg -type l -path "*/man/man*/*" -printf '%f!%h\n')

    # Create the archive
    BSDTAR+=" ${ZFLAG[$PKGMK_COMPRESSION_MODE]}"
    [ $UID = 0 ] || BSDTAR+=" --uid 0 --gid 0"
    [ -w "$pkg_dir$package" ] || rm -f "$pkg_dir$package" 2>/dev/null \
        || { error "$pkg_dir$package not writable.";
            cleanup_work; exit "$E_BUILD"; }
    if (cd pkg; $BSDTAR -cf "$pkg_dir$package" -- *); then
        info "Building '$pkg_dir$package' succeeded."
    else
        error "Unable to create the compressed package $package."
        cleanup_work; exit "$E_BUILD"
    fi

    # Check the footprint of the built package, unless '-if' was given
    [ "$PKGMK_IGNORE_FOOTPRINT" = "yes" ] || check_footprint

    # Create signature if it doesn't already exist
    [ -e "$PKGMK_ROOT/.signature" ] || make_signature new

fi  # Continue from here if extract and build were skipped

# Clean up the work directory.
find . -maxdepth 1 -mindepth 1 -type l -delete; cleanup_work

# Proceed to install/upgrade if requested.
if [ "$PKGMK_INSTALL" != "no" ]; then
    PKGMK_INSTALL_COMMAND="/usr/bin/pkgadd"
    [ "${CMD_FLAGS[pkgadd]}" ] && PKGMK_INSTALL_COMMAND="${CMD_FLAGS[pkgadd]}"
    [ "${CMD_FLAGS[instroot]}" ] && PKGMK_INSTALL_COMMAND+=" -r ${CMD_FLAGS[instroot]}"
    [ "$PKGMK_INSTALL" = "update" ] && PKGMK_INSTALL_COMMAND+=" -u"
    $PKGMK_INSTALL_COMMAND "$pkg_dir$package" \
        && info "pkgadd $package succeeded." || \
        { error "pkgadd $package failed."; exit "$E_INSTALL"; }
fi

# Done!
}

#######################  error codes  ##############################
E_PKGFILE=2    # invalid Pkgfile
E_DIRPERM=3    # (source/build) directory missing or missing read/write permission
E_DOWNLOAD=4   # error during download
E_UNPACK=5     # error during unpacking of source file(s)
E_FOOTPRINT=6  # footprint mismatch
E_BUILD=7      # error while running 'build()'
E_INSTALL=8    # error while installing the package via 'pkgadd'
E_SIGNATURE=9  # error verifying the signature

########################  settings  ################################
readonly PKGMK_VERSION="#VERSION#"
readonly PKGMK_COMMAND="$0"
readonly PKGMK_ROOT="$PWD"
PKGMK_DOWNLOAD_PROG="/usr/bin/curl"
PKGMK_GIT_COMMAND="/usr/bin/git"
PKGMK_CONF="/etc/pkgmk.conf";     PKGMK_SU="/usr/bin/sudo"
PKGMK_SOURCE_DIR="$PWD";          PKGMK_WORK_DIR="$PWD/work"
PKGMK_PACKAGE_DIR="$PWD";         PKGMK_COMPRESSION_MODE="gz"
PKGMK_INSTALL="no";               PKGMK_FORCE="no"
PKGMK_DOWNLOAD_ONLY="no";         PKGMK_KEEP_WORK="no"
PKGMK_EXTRACT_ONLY="no";          PKGMK_UPDATE_SIG="no"
PKGMK_MTIME_ONLY="no";            PKGMK_IGNORE_SIG="no"
PKGMK_UPDATE_FOOTPRINT="no";      PKGMK_REFRESH_SIG="no"
PKGMK_IGNORE_FOOTPRINT="no";      PKGMK_CHECK_SIG="no"
PKGMK_IGNORE_NEW="no";            PKGMK_PRIVATEKEY=""

######################## subroutines ################################
parse_options() {
    local needHelp && declare -a needHelp
    while [ "$1" ]; do
        case "$1" in
	    -h|--help) needHelp+=("help") ;;
	    -v|--version) needHelp+=("version") ;;
            -uf|-us|-rs|-cs|-um|-do|-eo|-if|-is|-in|-ns|-f|-kw|-i|-u) CMD_FLAGS["${1#-}"]=1 ;;
            --update-signature) CMD_FLAGS["us"]=1 ;;
            --refresh-signature) CMD_FLAGS["rs"]=1 ;;
            --check-signature) CMD_FLAGS["cs"]=1 ;;
            -d|--download) ;; # deprecated flags but don't throw an error for them
            -utd|--up-to-date) CMD_FLAGS["utd"]=1 ;;
            --download-only) CMD_FLAGS["do"]=1 ;;
            --extract-only) CMD_FLAGS["eo"]=1 ;;
            --ignore-footprint) CMD_FLAGS["if"]=1 ;;
            --ignore-new) CMD_FLAGS["in"]=1 ;;
            --ignore-signature) CMD_FLAGS["is"]=1 ;;
            --no-strip) ns_filter="-false" ;;
            --force) CMD_FLAGS["f"]=1 ;;
            --keep-work) CMD_FLAGS["kw"]=1 ;;
            --install) CMD_FLAGS["i"]=1 ;;
            --upgrade) CMD_FLAGS["u"]=1 ;;
            -pk|--public-key)
                [ -r "$2" ] && KEY_FILES["pk"]="$2" || { echo "$(basename "$PKGMK_COMMAND"): option $1 requires a filename as argument";
                    exit 1; }
                shift ;;
            -sk|--secret-key)
                [ -r "$2" ] && KEY_FILES["sk"]="$2" || { echo "$(basename "$PKGMK_COMMAND"): option $1 requires a filename as argument";
                    exit 1; }
                shift ;;
            -cf|--config-file)
                [ -r "$2" ] && PKGMK_CONF="$2" || { echo "$(basename "$PKGMK_COMMAND"): option $1 requires a filename as argument";
                    exit 1; }
                shift ;;
            --install-root)
                [ -d "$2" ] && CMD_FLAGS["rootfs"]="$2" || { echo "$(basename "$PKGMK_COMMAND"): option $1 requires a mountpoint as argument";
                    exit 1; }
                shift ;;
            --addcommand)
                [ "$2" ] && CMD_FLAGS["pkgadd"]="$2" || { echo "$(basename "$PKGMK_COMMAND"): option $1 requires a path to pkgadd";
                    exit 1; }
                shift ;;
            -um|--update-md5sum) warn "updating md5sums is obsolete, ignoring option '$1'." ;;
            -r|--recursive) { warn "option '$1' no longer supported. Use a wrapper script for recursive operation."; exit 1; } ;;
            -c|--clean) { error "option '$1' no longer implemented in $(basename "$PKGMK_COMMAND"). Use prtwash instead."; exit 1; } ;;
            *)
                echo "$(basename "$PKGMK_COMMAND"): invalid option $1"; exit 1 ;;
        esac
        shift
    done
    if [ ${#needHelp[@]} != 0 ]; then
        for ((i=0; i<${#needHelp[@]}; i++)); do
          case "${needHelp[i]}" in
	    help) print_help ;;
	    version) echo "$(basename "$PKGMK_COMMAND") (pkgutils) $PKGMK_VERSION" ;;
          esac
        done
	exit 0
    fi
}

print_help() {
    echo "usage: $(basename "$PKGMK_COMMAND") [options]"
    echo "options: (highest to lowest precedence)"
    echo "  -h,   --help                print help and exit"
    echo "  -v,   --version             print version and exit"
    echo "  -cf,  --config-file <file>  use alternative configuration file"
    echo "  -sk,  --secret-key <file>   use <file> to sign the port"
    echo "  -pk,  --public-key <file>   check the port signature using public-key <file>"
    echo "  -rs,  --refresh-signature   create new signature based on existing sha256 checksums"
    echo "  -utd, --up-to-date          report whether the built package is up to date, then exit"
    echo "  -f,   --force               override timestamp check, build package or update footprint anyway"
    echo "  -uf,  --update-footprint    update footprint using result from last build"
    echo "  -do,  --download-only       stop after downloading all the necessary source file(s)"
    echo "  -cs,  --check-signature     stop after verifying the signatures of all downloaded sources"
    echo "  -us,  --update-signature    update signature of Pkgfile and sources, do not build"
    echo "  -is,  --ignore-signature    build package without checking signature"
    echo "  -eo,  --extract-only        stop after downloading and extracting source file(s)"
    echo "  -ns,  --no-strip            do not strip executable binaries or libraries"
    echo "  -kw,  --keep-work           keep temporary working directory"
    echo "  -if,  --ignore-footprint    build package without checking footprint"
    echo "  -in,  --ignore-new          build package, ignoring new files in a footprint mismatch"
    echo "  -i,   --install             build and install package"
    echo "  -u,   --upgrade             build and upgrade package"
    echo "  --install-root <dir>        install package to the CRUX system mounted at <dir>"
    echo "  --addcommand <path>         specify an alternative pkgadd command"
}

validate_pkgfile() { # called from within PKGMK_ROOT
    local errcode kv
    [ -f Pkgfile ] || { error "no Pkgfile found. $PKGMK_ROOT is not a valid ports directory."; return "$E_PKGFILE"; }
    [ "$PKGMK_UPDATE_SIG" = "yes" ] && return 0
    check_signature "pre-Pkgfile" | parse_signify_output
    errcode=$?
    [ "$errcode" = 0 ] || info "Use '-is' if you have independent confirmation of the port's integrity."
    [ "$errcode" = 0 ] || [ "$PKGMK_IGNORE_SIG" = "yes" ] || return $E_SIGNATURE

    # the environment should not be affected by sourcing the Pkgfile both here
    # and in the later build, but to be safe we use a nested subshell
    ( . Pkgfile; kv=0; [ -n "$name" ] || kv+=1; [ -n "$version" ] || kv+=2; \
    [ -n "$release" ] || kv+=4; [ "$(type -t build)" = "function" ] || kv+=8; \
    echo $kv ) | check_reqvars
}

check_reqvars () {
    local checksum nullvars;
    read -r checksum
    [ $((checksum & 1)) = 1 ] && nullvars=" 'name'"
    [ $((checksum & 2)) = 2 ] && nullvars+=" 'version'"
    [ $((checksum & 4)) = 4 ] && nullvars+=" 'release'"
    [ $((checksum & 8)) = 8 ] && nullvars+=" 'build()'"
    [ "$checksum" = 0 ] || { error "Pkgfile does not specify these required variables:"; \
    info "$nullvars"; return "$E_PKGFILE"; }
}

check_pkg_mtime() { # can be called even if some sources are missing
    local li=0; local utd=0; local msg="Package '$pkg_dir$package' is not up to date."

    if [ -f "$pkg_dir$package" ]; then
        utd=1
        while (( li < ${#_local_[@]} )) && [ "$utd" = 1 ]; do
            [ ! -e "${_local_[li]}" ] || \
            [ "$pkg_dir$package" -nt "$(realpath "${_local_[$li]}")" ] || utd=0
            li=$(( li+1 ))
        done
        [ ! -e "$PKGMK_ROOT/Pkgfile" ] || [ "$pkg_dir$package" -nt "$PKGMK_ROOT/Pkgfile" ] || utd=0
    else
        msg="$package is not yet built."
        utd=0
    fi

    [ $utd = 0 ] || msg="Package '$pkg_dir$package' is up to date."
    [ "$PKGMK_MTIME_ONLY" = "yes" ] && info "$msg"; return $utd
}

get_basename() {
    echo "${1##*/}"
}

get_filename() {
    local ABSOLUTE=""
    [ "$1" = "-a" ] && { ABSOLUTE=1; shift; }

    if [[ $1 =~ ^(http|https|ftp|file)://.*/(.+) ]]; then
        echo "$PKGMK_SOURCE_DIR/${BASH_REMATCH[2]}"
    else
        [ "$ABSOLUTE" ] && echo "$PKGMK_ROOT/$1" || echo "$1"
    fi
}

fetch_url() {
    local u="$1"; local h="$2"; local finished=0; local giturl tag CLONE_ARGS
    local SAVE_AS REPO OCONTINUE OOUT; local m=0

    if [[ "$u" =~ ^__git__ ]]; then
            # git must be installed in order to obtain such a source
            [ -x "$PKGMK_GIT_COMMAND" ] || return 1;
            #
            giturl="${u#__git__}"
            # Did the port maintainer specify a branch other than 'main'?
            tag="${giturl##*\#}"
            [ "$tag" = "$giturl" ] || { giturl="${giturl%\#*}";
                CLONE_ARGS=(-c advice.detachedHead=false --branch "$tag"); }
            #
            # Has this project been downloaded before?
            if [ -d "$src_dir/$h.partial" ]; then
                ln -s "$src_dir/$h.partial" "$h"; cd "$h"
                "$PKGMK_GIT_COMMAND" pull "$giturl" "$tag"
                finished=$?; cd ..
            else
                "$PKGMK_GIT_COMMAND" clone "$giturl" "${CLONE_ARGS[@]}" "$h.partial"
                finished=$?; [ "$src_dir" = "" ] || { mv "$h.partial" "$src_dir";
                        ln -s "$src_dir/$h.partial" "$h"; }
            fi
            return $finished;
    else
            case "$PKGMK_DOWNLOAD_PROG" in
                *wget)  OCONTINUE="-c"
                        OOUT="--compression=none --passive-ftp
                        --no-directories --tries=3 --waitretry=3
                        $PKGMK_WGET_OPTIONS -O" ;;
                *curl)  OCONTINUE="-C -"
                        OOUT="-L -# --fail --ftp-pasv
                                $PKGMK_CURL_OPTIONS -o" ;;
                *) SAVE_AS="false" ;;
            esac
            #
            # start with the mirrors defined in pkgmk.conf, then go to the url found in the Pkgfile
            while (( m <= ${#PKGMK_SOURCE_MIRRORS[@]} )) && [ $finished = 0 ] \
                    && [ "$SAVE_AS" != "false" ]; do
                [ "${PKGMK_SOURCE_MIRRORS[m]}" = "" ] && um=$u || \
                { REPO=${PKGMK_SOURCE_MIRRORS[m]%/}; um=$REPO/${u##*/}; }
                m=$(( m+1 ))
            # interrupted downloads from a previous run should be put where wget or curl will find them
                [ -f "$src_dir/$h.partial" ] && { ln -s "$src_dir/$h.partial" . ;
                SAVE_AS="$PKGMK_DOWNLOAD_PROG $um $OCONTINUE $OOUT"; } \
                        || SAVE_AS="$PKGMK_DOWNLOAD_PROG $um $OOUT"
                if $SAVE_AS "$h.partial"; then
                    finished=1
                    [ "$src_dir" = "" ] || [ ! -w "$src_dir"/ ] || \
                    { mv "$h.partial" "$src_dir/$h"; ln -sf "$src_dir/$h" . ; }
                else # an interrupted download should not have its efforts destroyed by cleanup_work()
                    [ ! -s "$h.partial" ] || [ "$src_dir" = "" ] || \
                            [ ! -w "$src_dir"/ ] || mv "$h.partial" "$src_dir"
                fi
            done
    fi
}

make_footprint() {
    pkginfo --footprint "$pkg_dir$package" \
    | sed -e "s|\tlib/modules/$(uname -r)/|\tlib/modules/<kernel-version>/|g" \
    | sort -k 3
}

check_footprint() {
    local CN CM
    local TRUTH="$PKGMK_ROOT/.footprint"; local diffs=0; local severity=error;
    [ -f "$pkg_dir$package" ] || \
        { error "$package not found. Cannot check footprint."; exit "$E_FOOTPRINT"; }

    if [ -f "$TRUTH" ]; then
	    diff -w -t -U 0 <(sed "s;x86_64-linux-\(gnu\|musl\);x86_64-linux-LIBC;" "$TRUTH" | sort) \
		    <(make_footprint | sed "s;x86_64-linux-\(gnu\|musl\);x86_64-linux-LIBC;" | sort) | \
        sed '/^@@/d; /^+++/d; /^---/d; s/^+/NEW    /g; s/^-/MISSING /g' > ".footprint.diff"
        if [ -s ".footprint.diff" ]; then
            CN=$(grep -c ^NEW ".footprint.diff"); CM=$(grep -c ^MISSING ".footprint.diff")
            [ "$PKGMK_IGNORE_NEW" = "yes" ] || diffs=$CN
            [ "$PKGMK_IGNORE_FOOTPRINT" = "yes" ] && diffs=0 || diffs=$(( diffs+CM ))
            (( diffs == 0 )) && severity=warning
            $severity "footprint mismatch found:"; cat ".footprint.diff" >&2
        fi
        rm ".footprint.diff"
    else
        warning "footprint not found, creating new."; make_footprint > "$TRUTH"
    fi
    (( diffs == 0 )) || { cleanup_work; exit $E_FOOTPRINT; }
}

parse_signify_output() { # chomps the output of check_signature()
    local signout signerr
    [ "$PKGMK_IGNORE_SIG" = "yes" ] && return 0
    while read -r signout; do
    case "$signout" in 
        *"Pkgfile verification failed")
            signerr=-1; error "Signature missing! Unable to authenticate the Pkgfile."
            ;;
        *"verification failed"*)
            signerr=1; error "Signature file corrupted or unreadable."
            ;;
        *"FAIL")
            signerr=2; error "Signature mismatch found:"
            echo "$signout" | awk -F: '/FAIL/ {printf "MISMATCH   %s\n", $1}'
            ;;
        *"OK") signerr=0 ;;
    esac
    done
    return $signerr
}

check_signature() { # called from $PKGMK_ROOT in the case "when"="pre-Pkgfile",
                    # otherwise called from within $work.
    local reqfiles=(Pkgfile); local s=0; local when="$1";
    local SIGNIFY_ARGS=(-C -x "$PKGMK_ROOT/.signature")

    [ "$PKGMK_PUBLICKEY" ] || PKGMK_PUBLICKEY="$(get_repo_key public)"
    if [ -f "$PKGMK_ROOT/.signature" ]; then
        [ "$when" = "pre-Pkgfile" ] || reqfiles=(.footprint)
        while [ "$when" = "pre-build" ] && (( s < ${#_local_[@]} )); do 
            [[ "${source[s]}" =~ ^__git__ ]] || reqfiles+=("${_local_[s]}")
            s=$(( s+1 ))
        done
        for FILE in "${reqfiles[@]}"; do
            [ -e "$FILE" ] || ln -sf "$PKGMK_ROOT/$FILE" .
        done
        [ -r "$PKGMK_PUBLICKEY" ] && SIGNIFY_ARGS+=(-p "$PKGMK_PUBLICKEY")
        /usr/bin/signify "${SIGNIFY_ARGS[@]}" "${reqfiles[@]}" 2>&1
    else
        [ "$when" = "pre-Pkgfile" ] && echo "Pkgfile verification failed"
        [ "$when" != "pre-Pkgfile" ] && echo "signature verification failed"
    fi
}

get_repo_key() {
    local typ="${1:0:3}"; local REPO;
    REPO=$(dirname "$PKGMK_ROOT"); REPO=$(basename -s .git "$REPO");
    ls "/etc/ports/$REPO.$typ" 2>/dev/null \
            || ls "$HOME/.ssh/$REPO.$typ" 2>/dev/null
}

make_signature() {
    local ordered si pub
    [ ! -e "$PKGMK_ROOT/.signature" ] || [ -w "$PKGMK_ROOT/.signature" ] \
        || { error ".signature not writable."; return $E_DIRPERM; }

    [ -n "$PKGMK_PRIVATEKEY" ] || PKGMK_PRIVATEKEY="$(get_repo_key secret)"
    if [ -n "$PKGMK_PRIVATEKEY" ]; then
        pub="/etc/ports/$(basename -s .sec "$PKGMK_PRIVATEKEY").pub"
    elif [[ "$PKGMK_UPDATE_SIG$PKGMK_REFRESH_SIG" =~ yes ]]; then
        error "No suitable secret key found. Specify one explicitly with '-sk'."
        return $E_SIGNATURE
    else
        return 0
    fi

    # create a new .signature, or refresh an existing manifest?
    case "$1" in
        new)
            [ -e "$PKGMK_ROOT/.footprint" ] || \
                    warning "Footprint not found, signature will be incomplete."
            for f in "$PKGMK_ROOT/Pkgfile" "$PKGMK_ROOT/.footprint"; do
                    [ -e "$f" ] && ordered+=( "$f" )
            done
            for ((si=0; si < ${#source[@]}; si++)); do
                # ignore git directories when writing the signature
                [[ "${source[si]}" =~ ^__git__ ]] || ordered+=("${_local_[si]}")
            done
            sha256sum --tag "${ordered[@]}" \
                | sed 's|^SHA256 (.*/\(.*\))\(.* = .*\)|SHA256 (\1)\2|' \
                | /usr/bin/signify -S -e -x - -q -s "$PKGMK_PRIVATEKEY" -m - \
                | sed "s|${PKGMK_PRIVATEKEY%.sec}.pub|$pub|" \
                > "$PKGMK_ROOT/.signature"
        ;;
        refresh)
            [ -e ".signature" ] || \
                { error "missing .signature, cannot refresh."; return $E_SIGNATURE; }
            if tail -n +3 ".signature" | /usr/bin/signify -S -e -x - -q \
                -s "$PKGMK_PRIVATEKEY" -m - > .signature.tmp; then
                mv .signature.tmp .signature
            else
                rm .signature.tmp; return $E_SIGNATURE
            fi
        ;;
    esac
    [ "$1" = "new" ] && info "signature created." || info "signature updated."
}

interrupted() {
    error "Interrupted."; cleanup_work; exit 1
}

cleanup_work() {
    [ "$PKGMK_KEEP_WORK" = "yes" ] || { cd "$PKGMK_ROOT"; rm -rf "$work"; }
}

info() {
    echo "=======> $1"
}

warning() {
    info "WARNING: $1" >&2
}

error() {
    info "ERROR: $1" >&2
}

######################## end of subroutines ###########################
## Now ensure that some of them cannot be overwritten when sourcing Pkgfile  ##
readonly -f main info warning error print_help parse_options validate_pkgfile \
    check_reqvars check_pkg_mtime fetch_url get_repo_key \
    check_signature parse_signify_output cleanup_work
trap "interrupted" SIGHUP SIGINT SIGQUIT SIGTERM

export LC_ALL=C.UTF-8
main "$@"
