#!/bin/sh -e
#
# This is a simple package manager written in POSIX 'sh' for
# KISS Linux utilizing the core UNIX utilities where needed.
#
# The script runs with 'set -e' enabled. It will exit on any
# non-zero return code. This ensures that no function continues
# if it fails at any point.
#
# Keep in mind that this involves extra code in the case where
# an error is optional or required.
#
# Where possible the package manager should "error first".
# Check things first, die if necessary and continue if all is well.
#
# The code below conforms to shellcheck's rules. However, some
# lint errors *are* disabled as they relate to unexpected
# behavior (which we do expect).
#
# KISS is available under the MIT license.
#
# - Dylan Araps.

die() {
    # Print a message and exit with '1' (error).
    printf '\033[31m!>\033[m %s\n' "$@" >&2
    exit 1
}

log() {
    # Print a message with a colorful arrow to distinguish
    # from other output.
    printf '\033[32m=>\033[m %s\n' "$@"
}

pkg_lint() {
    # Check that each mandatory file in the package entry exists.
    log "[$1]: Checking repository files..."

    # Figure out *where* the repository entry for the package
    # is located.
    repo_dir=$(pkg_search "$1")

    cd "$repo_dir" || die "'$repo_dir' not accessible"

    [ -f sources ]   || die "[$1]: Sources file not found."
    [ -x build ]     || die "[$1]: Build file not found or not executable."
    [ -s version ]   || die "[$1]: Version file not found or empty."

    # Ensure that the release field in the version file is set
    # to something. The above test checks for the version field inclusively.
    read -r _ rel < version
    [ "$rel" ] || die "Release field not found in version file."

    # Unset this variable so it isn't used again on a failed
    # source. There's no 'local' keyword in POSIX sh.
    rel=
}

pkg_search() {
    # Figure out which repository a package belongs to by
    # searching for directories matching the package name
    # in $KISS_PATH/*.
    [ "$KISS_PATH" ] || \
        die "\$KISS_PATH needs to be set." \
            "Example: KISS_PATH=/packages/core:/packages/extra:/packages/xorg" \
            "Repositories will be searched in the configured order." \
            "The variable should work just like \$PATH."

    # Disable globbing with 'set -f' to ensure that the unquoted
    # variable doesn't expand into anything nasty.
    # shellcheck disable=2086,2046
    {
        set -f
        set -- "$1" $(IFS=:; find $KISS_PATH -maxdepth 1 -name "$1")
        set +f
    }

    # A package may also not be found due to a repository not being
    # readable by the current user. Either way, we need to die here.
    [ -z "$2" ] && die "Package '$1' not in any repository."

    printf '%s\n' "$2"
}

pkg_list() {
    # List installed packages. As the format is files and
    # directories, this just involves a simple for loop and
    # file read.

    # Change directories to the database. This allows us to
    # avoid having to 'basename' each path. If this fails,
    # set '$1' to mimic a failed glob which indicates that
    # nothing is installed.
    cd "$KISS_ROOT/var/db/kiss/" 2>/dev/null ||
        set -- "$KISS_ROOT/var/db/kiss/"\*

    # Optional arguments can be passed to check for specific
    # packages. If no arguments are passed, list all. As we
    # loop over '$@', if there aren't any arguments we can
    # just set the directory contents to the argument list.
    [ "$1" ] || set -- *

    # If the 'glob' above failed, exit early as there are no
    # packages installed.
    [ "$1" = "$KISS_ROOT/var/db/kiss/"\* ] && return 1

    # Loop over each version file and warn if one doesn't exist.
    # Also warn if a package is missing its version file.
    for pkg; do
        [ -d "$pkg" ] || {
            log "Package '$pkg' is not installed."
            return 1
        }

        [ -f "$pkg/version" ] || {
            log "Warning: Package '$pkg' has no version file."
            continue
        }

        read -r version release < "$pkg/version" &&
            printf '%s\n' "$pkg $version-$release"
    done
}

pkg_sources() {
    # Download any remote package sources. The existence of local
    # files is also checked.
    log "[$1]: Downloading sources..."

    # Store each downloaded source in named after the package it
    # belongs to. This avoid conflicts between two packages having a
    # source of the same name.
    mkdir -p "$src_dir/$1" && cd "$src_dir/$1"

    # Find the package's repository files. This needs to keep
    # happening as we can't store this data in any kind of data
    # structure.
    repo_dir=$(pkg_search "$1")

    while read -r src _; do
        case $src in
            # Git repository.
            git:*)
                git clone "${src##git:}" "$mak_dir"
            ;;

            # Remote source.
            *://*)
                [ -f "${src##*/}" ] && {
                    log "[$1]: Found cached source '${src##*/}'."
                    continue
                }

                wget "$src" || {
                    rm -f "${src##*/}"
                    die "[$1]: Failed to download $src."
                }
            ;;

            # Local files (Any source that is non-remote is assumed to be local).
            *)
                [ -f "$repo_dir/$src" ] ||
                    die "[$1]: No local file '$src'."

                log "[$1]: Found local file '$src'."
            ;;
        esac
    done < "$repo_dir/sources"
}

pkg_extract() {
    # Extract all source archives to the build directory and copy over
    # any local repository files.
    log "[$1]: Extracting sources..."

    # Store each downloaded source in named after the package it
    # belongs to. This avoid conflicts between two packages having a
    # source of the same name.
    mkdir -p "$mak_dir/$1" && cd "$mak_dir/$1"

    # Find the package's repository files. This needs to keep
    # happening as we can't store this data in any kind of data
    # structure.
    repo_dir=$(pkg_search "$1")

    while read -r src dest; do
        mkdir -p "./$dest"

        case $src in
            # Do nothing as git repository was downloaded to the build
            # directory directly.
            git:*) ;;

            # Only 'tar' archives are currently supported for extraction.
            # Any other file-types are simply copied to '$mak_dir' which
            # allows you to extract them manually.
            *://*.tar*|*://*.tgz)
                tar xf "$src_dir/$1/${src##*/}" -C "./$dest" \
                    --strip-components 1 \
                || die "[$1]: Couldn't extract ${src##*/}."
            ;;

            # Local files (Any source that is non-remote is assumed to be local).
            *)
                [ -f "$repo_dir/$src" ] ||
                    die "[$1]: Local file $src not found."

                cp -f "$repo_dir/$src" "./$dest"
            ;;
        esac
    done < "$repo_dir/sources"
}

pkg_depends() {
    # Resolve all dependencies and install them in the right order.

    # Find the package's repository files. This needs to keep
    # happening as we can't store this data in any kind of data
    # structure.
    repo_dir=$(pkg_search "$1")

    # This does a depth-first search. The deepest dependencies are
    # listed first and then the parents in reverse order.
    case $missing_deps in
        # Dependency is already in list, skip it.
        *" $1 "*) ;;

        *)
            # Recurse through the dependencies of the child
            # packages. Keep doing this.
            [ -f "$repo_dir/depends" ] &&
                while read -r dep _; do
                    pkg_depends "$dep" ||:
                done < "$repo_dir/depends"

            # After child dependencies are added to the list,
            # add the package which depends on them.
            missing_deps="$missing_deps $1 "
        ;;
    esac
}

pkg_verify() {
    # Verify all package checksums. This is achieved by generating
    # a new set of checksums and then comparing those with the old
    # set.

    # Find the package's repository files. This needs to keep
    # happening as we can't store this data in any kind of data
    # structure.
    repo_dir=$(pkg_search "$1")

    # Generate a second set of checksums to compare against the
    # repository's checksums for the package.
    pkg_checksums .checksums "$1"

    # Compare the checksums using 'cmp'.
    cmp -s "$repo_dir/.checksums" "$repo_dir/checksums" || {
        log "[$1]: Checksum mismatch."

        # Instead of dying above, log it to the terminal. Also define a
        # variable so we *can* die after all checksum files have been
        # checked.
        mismatch="$mismatch$1 "
    }

    # The second set of checksums use a temporary file, we need to
    # delete it.
    rm -f "$repo_dir/.checksums"
}

pkg_strip() {
    # Strip package binaries and libraries. This saves space on the
    # system as well as on the tar-balls we ship for installation.

    # Find the package's repository files. This needs to keep
    # happening as we can't store this data in any kind of data
    # structure.
    repo_dir=$(pkg_search "$1")

    # Package has stripping disabled, stop here.
    [ -f "$repo_dir/nostrip" ] && return

    log "[$1]: Stripping binaries and libraries..."

    find "$pkg_dir/$1" -type f | while read -r binary; do
        case $(file -bi "$binary") in
            application/x-sharedlib*|application/x-pie-executable*)
                strip_opts=--strip-unneeded
            ;;

            application/x-archive*)    strip_opts=--strip-debug ;;
            application/x-executable*) strip_opts=--strip-all ;;

            *) continue ;;
        esac

        # Suppress errors here as some binaries and libraries may
        # fail to strip. This is OK.
        strip "$strip_opts" "$binary" 2>/dev/null ||:
    done
}

pkg_manifest() (
    # Generate the package's manifest file. This is a list of each file
    # and directory inside the package. The file is used when uninstalling
    # packages, checking for package conflicts and for general debugging.
    log "[$1]: Generating manifest..."

    # This funcion runs as a sub-shell to avoid having to 'cd' back to the
    # prior directory before being able to continue.
    cd "$pkg_dir/$1"

    # Find all files and directories in the package. Directories are printed
    # with a trailing forward slash '/'. The list is then reversed with
    # directories appearing *after* their contents.
    find . -mindepth 1 -type d -exec printf '%s/\n' {} + -or -print |
    sort -r | sed -e ss.ss > "$pkg_dir/$1/var/db/kiss/$1/manifest"

    log "[$1]: Generated manifest."
)

pkg_tar() {
    # Create a tar-ball from the built package's files.
    # This tar-ball also contains the package's database entry.
    log "[$1]: Creating tar-ball..."

    # Find the package's repository files. This needs to keep
    # happening as we can't store this data in any kind of data
    # structure.
    repo_dir=$(pkg_search "$1")

    # Read the version information to name the package.
    read -r version release < "$repo_dir/version"

    # Create a tar-ball from the contents of the built package.
    tar zpcf "$bin_dir/$1#$version-$release.tar.gz" -C "$pkg_dir/$1" . ||
        die "[$1]: Failed to create tar-ball."

    log "[$1]: Successfully created tar-ball."
}

pkg_build() {
    # Build packages and turn them into packaged tar-balls. This function
    # also checks checksums, downloads sources and ensure all dependencies
    # are installed.

    # Resolve dependencies and generate a list.
    # Send 'force' to 'pkg_depends' to always include the explicitly
    # requested packages.
    log "Resolving dependencies..."
    for pkg; do pkg_depends "$pkg"; done

    # Store the explicit packages so we can handle them differently
    # below. Dependencies are automatically installed but packages
    # passed to KISS aren't.
    explicit_packages=" $* "

    # Disable globbing with 'set -f' to ensure that the unquoted
    # variable doesn't expand into anything nasty.
    # shellcheck disable=2086,2046
    {
        # Set the resolved dependency list as the function's arguments.
        set -f
        set -- $missing_deps
        set +f
    }

    for pkg; do
        case $explicit_packages in
            *" $pkg "*)
                build_packages="$build_packages$pkg "
            ;;

            *)
                pkg_list "$pkg" >/dev/null ||
                    build_packages="$build_packages$pkg "
            ;;
        esac
    done

    # Disable globbing with 'set -f' to ensure that the unquoted
    # variable doesn't expand into anything nasty.
    # shellcheck disable=2086,2046
    {
        # Set the resolved dependency list as the function's arguments.
        set -f
        set -- $build_packages
        set +f
    }

    log "Building: $*."

    # Only ask for confirmation if more than one package needs to be built.
    [ $# -gt 1 ] || [ "$build_prompt" ] && {
        log "Continue?: Press Enter to continue or Ctrl+C to abort here."

        # POSIX 'read' has none of the "nice" options like '-n', '-p'
        # etc etc. This is the most basic usage of 'read'.
        read -r REPLY || exit
    }

    log "Checking to see if any dependencies have already been built..."
    log "Installing any pre-built dependencies..."

    # Install any pre-built dependencies if they exist in the binary
    # directory and are up to date.
    for pkg; do
        # Don't check for pre-built package if it was passed to KISS
        # directly.
        case $explicit_packages in
            *" $pkg "*)
                shift
                set -- "$@" "$pkg"
                continue
            ;;
        esac

        # Find the package's repository files. This needs to keep
        # happening as we can't store this data in any kind of data
        # structure.
        repo_dir=$(pkg_search "$pkg")

        # Figure out the version and release.
        read -r version release < "$repo_dir/version"

        # Remove the current package from the package list.
        shift

        # Install any pre-built binaries if they exist.
        [ -f "$bin_dir/$pkg#$version-$release.tar.gz" ] && {
            log "[$pkg]: Found pre-built binary."
            pkg_install "$bin_dir/$pkg#$version-$release.tar.gz"
            continue
        }

        # Add the removed package back to the list if it doesn't
        # have a pre-built binary.
        set -- "$@" "$pkg"
    done

    for pkg; do pkg_lint "$pkg"; done
    for pkg; do
        # Find the package's repository files. This needs to keep
        # happening as we can't store this data in any kind of data
        # structure.
        repo_dir=$(pkg_search "$pkg")

        # Ensure that checksums exist prior to building the package.
        [ -f "$repo_dir/checksums" ] || {
            log "[$pkg]: Checksums are missing."

            # Instead of dying above, log it to the terminal. Also define a
            # variable so we *can* die after all checksum files have been
            # checked.
            no_checkums="$no_checkums$pkg "
        }
    done

    # Die here as packages without checksums were found above.
    [ "$no_checkums" ] &&
        die "Run '$kiss checksum ${no_checkums% }' to generate checksums."

    for pkg; do pkg_sources "$pkg"; done
    for pkg; do pkg_verify  "$pkg"; done

    # Die here as packages with differing checksums were found above.
    [ "$mismatch" ] &&
        die "Checksum mismatch with: ${mismatch% }"

    # Finally build and create tarballs for all passed packages and
    # dependencies.
    for pkg; do
        pkg_extract "$pkg"

        # Find the package's repository files. This needs to keep
        # happening as we can't store this data in any kind of data
        # structure.
        repo_dir=$(pkg_search "$pkg")

        # Install built packages to a directory under the package name
        # to avoid collisions with other packages.
        mkdir -p "$pkg_dir/$pkg/var/db/kiss"

        # Move to the build directory and call the build script.
        (cd "$mak_dir/$pkg"; "$repo_dir/build" "$pkg_dir/$pkg") ||
            die "[$pkg]: Build failed."

        # Copy the repository files to the package directory.
        # This acts as the database entry.
        cp -Rf "$repo_dir" "$pkg_dir/$pkg/var/db/kiss/"

        log "[$pkg]: Successfully built package."

        # Create the manifest file early and make it empty.
        # This ensure that the manifest is added to the manifest...
        : > "$pkg_dir/$pkg/var/db/kiss/$pkg/manifest"

        pkg_strip    "$pkg"
        pkg_manifest "$pkg"
        pkg_tar      "$pkg"

        # Install only dependencies of passed packages.
        case $explicit_packages in
            *" $pkg "*) continue ;;
            *)          pkg_install "$pkg" ;;
        esac
    done

    log "Successfully built package(s)."
    log "Run '$kiss i${explicit_packages% }' to install the built package(s)."
}

pkg_checksums() {
    # Generate checksums for packages.
    # This also downloads any remote sources.
    checksum_file=$1
    shift

    for pkg; do
        # Find the package's repository files. This needs to keep
        # happening as we can't store this data in any kind of data
        # structure.
        repo_dir=$(pkg_search "$pkg")

        while read -r src _; do
            case $src in
                # Git repository.
                # Skip checksums on git repositories.
                git:*) ;;

                *)
                    # File is local to the package and is stored in the
                    # repository.
                    [ -f "$repo_dir/$src" ] &&
                        src_path=$repo_dir/${src%/*}

                    # File is remote and was downloaded.
                    [ -f "$src_dir/$pkg/${src##*/}" ] &&
                        src_path=$src_dir/$pkg

                    # Die here if source for some reason, doesn't exist.
                    [ "$src_path" ] ||
                        die "[$pkg]: Couldn't find source '$src'."

                    # An easy way to get 'sha256sum' to print with the 'basename'
                    # of files is to 'cd' to the file's directory beforehand.
                    (cd "$src_path" && sha256sum "${src##*/}") ||
                        die "[$pkg]: Failed to generate checksums."

                    # Unset this variable so it isn't used again on a failed
                    # source. There's no 'local' keyword in POSIX sh.
                    src_path=
                ;;
            esac
        done < "$repo_dir/sources" > "$repo_dir/$checksum_file"

        log "[$pkg]: Generated/Verified checksums."
    done
}

pkg_conflicts() {
    # Check to see if a package conflicts with another.
    # This function takes a path to a KISS tar-ball as an argument.
    log "[$2]: Checking for package conflicts."

    # Extract manifest from the tar-ball and only extract files entries.
    tar xf "$1" -O "./var/db/kiss/$2/manifest" |
    while read -r line; do
        [ "${line%%*/}" ] && printf '%s\n' "$line" >> "$cac_dir/manifest-$pid"
    done ||:

    # Compare extracted manifest to all installed manifests.
    # If there are matching lines (files) there is a package conflict.
    for db in "$KISS_ROOT/var/db/kiss/"*; do
        [ "$2" = "${db##*/}" ] && continue

        grep -Fxf "$cac_dir/manifest-$pid" "$db/manifest" 2>/dev/null &&
            die "Package '$2' conflicts with '${db##*/}'."
    done

    # Remove this temporary file as we no longer need it.
    rm -f "$cac_dir/manifest-$pid"
}

pkg_remove() {
    # Remove a package and all of its files. The '/etc' directory
    # is handled differently and configuration files are *not*
    # overwritten.

    # Create a backup of 'rm' and 'rmdir' so they aren't removed
    # during package removal. This ensures that an upgrade to 'busybox'
    # or your core utilities of choice doesn't break the package manager.
    cp "$(command -v rm)"    "$cac_dir"
    cp "$(command -v rmdir)" "$cac_dir"

    # The package is not installed, don't do anything.
    pkg_list "$1" >/dev/null || {
        log "[$1]: Not installed."
        return
    }

    # Make sure that nothing depends on this package.
    [ "$2" = check ] && for file in "$KISS_ROOT/var/db/kiss/"*; do
        # Check each depends file for the package and if it's
        # a run-time dependency, append to the $required_by string.
        grep -q "^$1$" "$file/depends" 2>/dev/null &&
            required_by="$required_by'${file##*/}', "
    done

    [ "$required_by" ] &&
        die "[$1]: Package is required by ${required_by%, }." \
            "[$1]: Aborting here..."

    # Block being able to abort the script with 'Ctrl+C' during installation.
    # Removes all risk of the user aborting a package installation leaving
    # an incomplete package installed.
    trap '' INT

    while read -r file; do
        # The file is in '/etc' skip it. This prevents the package
        # manager from removing user edited configuration files.
        [ "${file##/etc/*}" ] || continue

        if [ -d "$KISS_ROOT/$file" ]; then
            "$cac_dir/rmdir" "$KISS_ROOT/$file" 2>/dev/null || continue
        else
            "$cac_dir/rm" -f -- "$KISS_ROOT/$file" ||
                log "[$1]: Failed to remove '$file'."
        fi
    done < "$KISS_ROOT/var/db/kiss/$1/manifest"

    # Reset 'trap' to its original value. Installation is done so
    # we no longer need to block 'Ctrl+C'.
    trap pkg_clean EXIT INT

    log "[$1]: Removed successfully."
}

pkg_install() {
    # Install a built package tar-ball.

    for pkg; do
        # Install can also take the full path to a tar-ball.
        # We don't need to check the repository if this is the case.
        if [ -f "$pkg" ] && [ -z "${pkg%%*.tar.gz}" ] ; then
            tar_file=$pkg

        else
            # Find the package's repository files. This needs to keep
            # happening as we can't store this data in any kind of data
            # structure.
            repo_dir=$(pkg_search "$pkg")

            # Read the version information to name the package.
            read -r version release < "$repo_dir/version"

            # Construct the name of the package tarball.
            tar_name=$pkg\#$version-$release.tar.gz

            [ -f "$bin_dir/$tar_name" ] ||
                die "Package '$pkg' has not been built." \
                    "Run '$kiss build $pkg'."

            tar_file=$bin_dir/$tar_name
        fi

        # Figure out which package the tar-ball installs by checking for
        # a database entry inside the tar-ball. If no database entry exists,
        # exit here as the tar-ball is *most likely* not a KISS package.
        pkg_name=$(tar tf "$tar_file" | grep -x "\./var/db/kiss/.*/version") ||
            die "'${tar_file##*/}' is not a valid KISS package."

        pkg_name=${pkg_name%/*}
        pkg_name=${pkg_name##*/}

        pkg_conflicts "$tar_file" "$pkg_name"

        # Extract the tar-ball early to catch any errors before installation
        # begins. The package manager uninstalls the previous package during
        # an upgrade so any errors need to be caught ASAP.
        tar pxf "$tar_file" -C "$tar_dir/" ||
            die "[$pkg_name]: Failed to extract tar-ball."

        log "[$pkg_name]: Checking that all dependencies are installed..."

        # Make sure that all run-time dependencies are installed prior to
        # installing the package.
        [ -f "$tar_dir/var/db/kiss/$pkg_name/depends" ] &&
            while read -r dep dep_type; do
                [ "$dep_type" ] || pkg_list "$dep" >/dev/null ||
                    required_install="$required_install'$dep', "
            done < "$tar_dir/var/db/kiss/$pkg_name/depends"

        [ "$required_install" ] &&
            die "[$1]: Package requires ${required_install%, }." \
                "[$1]: Aborting here..."

        # Create a backup of 'mv', 'rsync' and 'find' so they aren't removed
        # during package removal. This ensures that an upgrade to 'busybox' or
        # your core utilities of choice doesn't break the package manager.
        cp "$(command -v mv)"    "$cac_dir"
        cp "$(command -v rsync)" "$cac_dir"
        cp "$(command -v find)"  "$cac_dir"

        log "[$pkg_name]: Removing previous version of package if it exists."
        pkg_remove "$pkg_name"
        log "[$pkg_name]: Installing package..."

        # Block being able to abort the script with 'Ctrl+C' during installation.
        # Removes all risk of the user aborting a package installation leaving
        # an incomplete package installed.
        trap '' INT

        # "Install" the package using 'rsync'. None of the coreutils could
        # correctly accomplish this task. The preservation of permissions,
        # proper handling of existing files etc.
        "$cac_dir/rsync" --ignore-existing -a "$tar_dir/" "$KISS_ROOT/"

        # Reset 'trap' to its original value. Installation is done so
        # we no longer need to block 'Ctrl+C'.
        trap pkg_clean EXIT INT

        # Run the post install script and suppress errors. If it exists,
        # it will run, else nothing will happen.
        "$KISS_ROOT/var/db/kiss/$pkg_name/post-install" 2>/dev/null ||:

        log "[$pkg_name]: Installed successfully."
    done
}

pkg_updates() {
    # Check all installed packages for updates. So long as the installed
    # version and the version in the repositories differ, it's considered
    # an update.

    # Disable globbing with 'set -f' to ensure that the unquoted
    # variable doesn't expand into anything nasty.
    # shellcheck disable=2086,2046
    {
        set -f
        IFS=:
        set -- $KISS_PATH
        IFS=$old_ifs
        set +f
    }

    log "Updating repositories..."

    # Update each repository in '$KISS_PATH'. It is assumed that
    # each repository is 'git' tracked.
    for repo; do
        log "[${repo##*/}]: Updating repository."
        (cd "$repo"; git rev-parse --git-dir >/dev/null && git pull)
    done

    log "Checking for new package versions..."

    for pkg in "$KISS_ROOT/var/db/kiss/"*; do
        # Find the package's repository files. This needs to keep
        # happening as we can't store this data in any kind of data
        # structure.
        repo_dir=$(pkg_search "${pkg##*/}")

        # Read version and release information from the installed packages
        # and repository.
        read -r db_ver db_rel < "$pkg/version"
        read -r re_ver re_rel < "$repo_dir/version"

        # Compare installed packages to repository packages.
        [ "$db_ver-$db_rel" != "$re_ver-$re_rel" ] && {
            printf '%s\n' "${pkg##*/} $db_ver-$db_rel ==> $re_ver-$re_rel"
            outdated="$outdated${pkg##*/} "
        }
    done

    # End here if no packages have an update.
    [ "$outdated" ] || {
        log "Everything is up to date."
        return
    }

    # Disable globbing with 'set -f' to ensure that the unquoted
    # variable doesn't expand into anything nasty.
    # shellcheck disable=2086,2046
    {
        set -f
        set -- $outdated
        set +f
    }

    log "Packages to update: ${outdated% }."

    # Tell 'pkg_build' to always prompt before build.
    build_prompt=1

    # POSIX 'read' has none of the "nice" options like '-n', '-p'
    # etc etc. This is the most basic usage of 'read'.
    pkg_build "$@"
}

setup_caching() {
    # Setup the host machine for the package manager. Create any
    # directories which need to exist and set variables for easy
    # access to them.

    # Main cache directory (~/.cache/kiss/) typically.
    mkdir -p "${cac_dir:=${XDG_CACHE_HOME:=$HOME/.cache}/kiss}" ||
        die "Couldn't create cache directory ($cac_dir)."

    # Build directory.
    mkdir -p "${mak_dir:=$cac_dir/build-$pid}" ||
        die "Couldn't create build directory ($mak_dir)."

    # Package directory.
    mkdir -p "${pkg_dir:=$cac_dir/pkg-$pid}" ||
        die "Couldn't create package directory ($pkg_dir)."

    # Tar directory.
    mkdir -p "${tar_dir:=$cac_dir/extract-$pid}" ||
        die "Couldn't create tar directory ($tar_dir)."

    # Source directory.
    mkdir -p "${src_dir:=$cac_dir/sources}" ||
        die "Couldn't create source directory ($src_dir)."

    # Binary directory.
    mkdir -p "${bin_dir:=$cac_dir/bin}" ||
        die "Couldn't create binary directory ($bin_dir)."
}

pkg_clean() {
    # Clean up on exit or error. This removes everything related
    # to the build.

    # Remove temporary directories.
    rm -rf -- "$mak_dir" "$pkg_dir" "$tar_dir"

    # Remove cached commands.
    rm -f  -- "$cac_dir/find" "$cac_dir/mv" "$cac_dir/rsync" \
              "$cac_dir/rm" "$cac_dir/rmdir"

    # Remove temporary files.
    rm -f "$repo_dir/.checksums"
}

root_check() {
    # Ensure that the user has write permissions to '$KISS_ROOT'.
    # When this variable is empty, a value of '/' is assumed.
    [ -w "$KISS_ROOT/" ] || \
        die "No write permissions to '${KISS_ROOT:-/}'." \
            "You may need to run '$kiss' as root."
}

args() {
    # Parse script arguments manually. POSIX 'sh' has no 'getopts'
    # or equivalent built in. This is rather easy to do in our case
    # since the first argument is always an "action" and the arguments
    # that follow are all package names.

    # Actions can be abbreviated to their first letter. This saves
    # keystrokes once you memorize the commands and it also has the
    # side-effect of "correcting" spelling mistakes (assuming the first
    # letter is right).
    case $1 in
        # Build the list of packages.
        b*)
            shift

            # If no arguments were passed, rebuild all packages.
            [ "$1" ] || {
                cd "$KISS_ROOT/var/db/kiss" || die "Failed to find package db."

                # Use a glob after 'cd' to generate a list of all installed
                # packages based on directory names.
                set -- *

                # Undo the above 'cd' to ensure we stay in the same location.
                cd - >/dev/null
            }

            pkg_build "$@"
        ;;

        # Generate checksums for packages.
        c*)
            shift
            [ "$1" ] || die "'kiss checksum' requires an argument."

            for pkg; do pkg_lint    "$pkg"; done
            for pkg; do pkg_sources "$pkg"; done

            pkg_checksums checksums "$@"
        ;;

        # Install packages.
        i*)
            shift
            [ "$1" ] || die "'kiss install' requires an argument."
            root_check
            pkg_install "$@"
        ;;

        # Remove packages.
        r*)
            shift
            [ "$1" ] || die "'kiss remove' requires an argument."
            root_check
            log "Removing packages..."
            for pkg; do pkg_remove "$pkg" check; done
        ;;

        # List installed packages.
        l*)
            shift
            pkg_list "$@"
        ;;

        # Upgrade packages.
        u*)
            pkg_updates
        ;;

        # Search packages
        s*)
            [ "$2" ] || die "'kiss search' requires an argument."
            pkg_search "$2"
        ;;

        # Print version and exit.
        v*)
            log "$kiss 0.2.7"
        ;;

        # Catch all invalid arguments as well as
        # any help related flags (-h, --help, help).
        *)
            log "$kiss [b|c|i|l|r|s|u] [pkg]" \
                "build:     Build a package." \
                "checksum:  Generate checksums." \
                "install:   Install a package (Runs build if needed)." \
                "list:      List packages." \
                "remove:    Remove a package." \
                "search:    Search for a package." \
                "update:    Check for updates."
        ;;
    esac
}

main() {
    # Store the script name in a variable and use it everywhere
    # in place of 'kiss'. This allows the script name to be changed
    # easily.
    kiss=${0##*/}

    # The PID of the current shell process is used to isolate directories
    # to each specific KISS instance. This allows multiple package manager
    # instances to be run at once. Store the value in another variable so
    # that it doesn't change beneath us.
    pid=$$

    # Store the original value of IFS so we can revert back to it if the
    # variable is ever changed.
    old_ifs=$IFS

    # Catch errors and ensure that build files and directories are cleaned
    # up before we die. This occurs on 'Ctrl+C' as well as success and error.
    trap pkg_clean EXIT INT

    # Create the required temporary directories and set the variables
    # which point to them.
    setup_caching

    args "$@"
}

main "$@"