#!/bin/sh -ef
#
# Disable warnings against word-splitting and globbing.
# They are used *safely* throughout this script as globbing
# is globally disabled and assumptions can be made about the input.
# shellcheck disable=2046,2086
#
# 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 and/or required.
#
# Where possible the package manager should 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=/var/db/kiss/repo/core:/var/db/kiss/repo/extra" \
            "Repositories will be searched in the configured order." \
            "The variable should work just like \$PATH."

    # Find the repository containing a package.
    set -- "$1" $(IFS=:; find $KISS_PATH -maxdepth 1 -name "$1")

    # 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/$pkg_db" 2>/dev/null ||
        set -- "$KISS_ROOT/$pkg_db/"\*

    # 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 +f; set -f -- *; }

    # If the 'glob' above failed, exit early as there are no
    # packages installed.
    [ "$1" = "$KISS_ROOT/$pkg_db/"\* ] && 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"
    chmod 1777 "$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
                    [ "${dep##\#*}" ] || continue
                    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 "$1" > "$cac_dir/c-$1"

    # Compare the checksums using 'cmp'.
    cmp -s "$cac_dir/c-$1" "$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 "
    }
}

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..."

    # Strip only files matching the below mime-types from the package
    # directory. No alternative to 'file' here sadly.
    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/$pkg_db/$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. `fakeroot`
    # is used here to correct issues with file ownership.
    fakeroot \
        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.

    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=" $* "

    # Set the resolved dependency list as the function's arguments.
    set -- $missing_deps

    # The dependency solver always lists all dependencies regardless of
    # whether or not they are installed. Ensure that all explicit packages
    # are included and ensure that all installed packages are excluded.
    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

    # Set the filtered dependency list as the function's arguments.
    set -- $build_packages

    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/$pkg_db"

        # Move to the build directory and call the build script.
        (cd "$mak_dir/$pkg"; fakeroot "$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/$pkg_db/"

        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/$pkg_db/$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.

    # 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.
            # 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/$1/${src##*/}" ] &&
                    src_path=$src_dir/$1

                # Die here if source for some reason, doesn't exist.
                [ "$src_path" ] ||
                    die "[$1]: 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 "[$1]: 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"
}

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 "./$pkg_db/$2/manifest" |
    while read -r line; do
        [ "${line%%*/}" ] && printf '%s\n' "$line" >> "$cac_dir/manifest-$pid"
    done ||:

    # Enable globbing.
    set +f

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

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

    # Disable globbing.
    set -f

    # 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.

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

    # Enable globbing.
    set +f

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

    # Disable globbing.
    set -f

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

    # Block being able to abort the script with 'Ctrl+C' during removal.
    # Removes all risk of the user aborting a package removal 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
            rmdir "$KISS_ROOT/$file" 2>/dev/null || continue
        else
            rm -f -- "$KISS_ROOT/$file" ||
                log "[$1]: Failed to remove '$file'."
        fi
    done < "$KISS_ROOT/$pkg_db/$1/manifest"

    # Reset 'trap' to its original value. Removal 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 "\./$pkg_db/.*/version") ||
            die "'${tar_file##*/}' is not a valid KISS package."

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

        pkg_conflicts "$tar_file" "$pkg_name"

        mkdir -p "$tar_dir/$pkg_name"

        # Extract the tar-ball to catch any errors before installation begins.
        tar pxf "$tar_file" -C "$tar_dir/$pkg_name" ||
            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/$pkg_name/$pkg_db/$pkg_name/depends" ] &&
            while read -r dep dep_type; do
                [ "${dep##\#*}" ] || continue
                [ "$dep_type" ]   || pkg_list "$dep" >/dev/null ||
                    required_install="$required_install'$dep', "
            done < "$tar_dir/$pkg_name/$pkg_db/$pkg_name/depends"

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

        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

        # If the package is already installed (and this is an upgrade) make a
        # backup of the manifest file.
        [ -f "$KISS_ROOT/$pkg_db/$pkg_name/manifest" ] &&
            cp -f "$KISS_ROOT/$pkg_db/$pkg_name/manifest" "$cac_dir/m-$pkg_name"

        # This is repeated multiple times. Better to make it a function.
        rsync_pkg() {
            rsync -HKav --exclude etc -- "$tar_dir/$pkg_name/" "$KISS_ROOT/"
        }

        # Install the package by using 'rsync' and overwrite any existing files
        # (excluding '/etc/').
        rsync_pkg

        # If '/etc/' exists in the package, install it but don't overwrite.
        [ -d "$tar_dir/$pkg_name/etc" ] &&
            rsync -HKav --ignore-existing "$tar_dir/$pkg_name/etc" "$KISS_ROOT/"

        # Remove any leftover files if this is an upgrade.
        [ -f "$cac_dir/m-$pkg_name" ] && {
            awk 'NR==FNR{lines[$0];next}!($0 in lines)' \
                "$KISS_ROOT/$pkg_db/$pkg_name/manifest" "$cac_dir/m-$pkg_name" |
            while read -r file; do
                # Skip deleting some leftover files.
                [ -f "$KISS_ROOT/$file" ] && [ ! -L "$KISS_ROOT/$file" ] &&
                    case $file in
                        *bin/rm|*bin/busybox|*bin/rsync|/etc/*) ;;
                        *) rm -f "$KISS_ROOT/$file" ;;
                    esac
            done
        }

        # Install the package again to fix any non-leftover files being
        # removed above.
        rsync_pkg
        rsync_pkg

        # 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/$pkg_db/$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.

    log "Updating repositories..."

    # Create a list of all repositories.
    IFS=:; set -- $KISS_PATH; IFS=$old_ifs

    # 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..."

    # Enable globbing.
    set +f

    for pkg in "$KISS_ROOT/$pkg_db/"*; 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

    # Disable globbing.
    set -f

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

    # Turn the string of outdated packages into a 'list'.
    set -- $outdated

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

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

    # Build all packages requiring an update.
    pkg_build "$@"
}

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 temporary files.
    (set +f; rm  -f "$cac_dir/c-"* "$cac_dir/m-"* "$cac_dir/rm")
}

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|bu|bui|buil|build)
            shift

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

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

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

            pkg_build "$@"
        ;;

        # Generate checksums for packages.
        c|ch|che|chec|check|checks|checksu|checksum|checksums)
            shift
            [ "$1" ] || die "'kiss checksum' requires an argument."

            for pkg; do pkg_lint    "$pkg"; done
            for pkg; do pkg_sources "$pkg"; done
            for pkg; do
                pkg_checksums "$pkg" > "$(pkg_search "$pkg")/checksums"

                log "[$pkg]: Generated checksums."
            done
        ;;

        # List dependencies for a package.
        d|de|dep|depe|depen|depend|depends)
            shift
            [ "$1" ] || die "'kiss depends' requires an argument."

            for pkg; do
                repo_dir=$(pkg_search "$pkg")

                [ -f "$repo_dir/depends" ] && {
                    log "[$pkg]: Has the following dependencies:"
                    cat "$repo_dir/depends"
                }
            done
        ;;

        # Install packages.
        i|in|ins|inst|insta|instal|install)
            shift
            [ "$1" ] || die "'kiss install' requires an argument."
            root_check

            # Create a list of each package's dependencies.
            for pkg; do pkg_depends "$pkg"; done

            # Filter the list, only including explicit packages.
            for pkg in $missing_deps; do
                case " $* " in
                    *" $pkg "*) install_pkgs="$install_pkgs $pkg " ;;
                esac
            done

            set -- $install_pkgs
            pkg_install "$@"
        ;;

        # Remove packages.
        r|re|rem|remo|remov|remove)
            shift
            [ "$1" ] || die "'kiss remove' requires an argument."
            root_check
            log "Removing packages..."

            # Create a list of each package's dependencies.
            for pkg; do pkg_depends "$pkg"; done

            # Reverse the list of dependencies filtering out anything
            # not explicitly set for removal.
            for pkg in $missing_deps; do
                case " $* " in
                    *" $pkg "*) remove_pkgs="$pkg $remove_pkgs" ;;
                esac
            done

            for pkg in $remove_pkgs; do
                pkg_list "$pkg" >/dev/null ||
                    die "[$pkg]: Not installed."

                pkg_remove "$pkg" check
            done
        ;;

        # List installed packages.
        l|li|lis|list)
            shift
            pkg_list "$@"
        ;;

        # Print package manifest.
        m|ma|man|mani|manif|manife|manifest)
            shift
            [ "$1" ] || die "'kiss manifest' requires an argument."

            for pkg; do
                if pkg_list "$pkg" >/dev/null; then
                    log "[$pkg]: Owns the following files:"
                    cat "$KISS_ROOT/$pkg_db/$pkg/manifest"
                else
                    log "[$pkg]: Not installed."
                fi
            done
        ;;

        # Check which package owns a file.
        o|ow|own|owns)
            shift
            [ "$1" ] || die "'kiss owns' requires an argument."

            for file; do
                [ -f "$KISS_ROOT/$file" ] || die "File '$file' doesn't exist."

                set +f
                pkg_owns=$(grep -lFx "$file" "$KISS_ROOT/$pkg_db/"*/manifest)
                set -f

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

                log "[$pkg_owns] owns '$file'"
            done
        ;;

        # Upgrade packages.
        u|up|upg|upgr|upgra|upgrad|upgrade)
            pkg_updates
        ;;

        # Search for packages.
        s|se|sea|sear|searc|search)
            shift
            [ "$1" ] || die "'kiss search' requires an argument."

            for pkg; do
                # Create a list of all matching packages.
                set -- $(IFS=:; find $KISS_PATH -mindepth 1 \
                                     -maxdepth 1 -name "$pkg")

                # Print all matches. If there aren't any, print an error.
                printf '%s\n' "${@:-$(log "[$pkg] Not found.")}"

                # Exit with an error if a search fails.
                [ "$1" ] || exit 1
            done
        ;;

        # Print version and exit.
        v|ve|ver|vers|versi|versio|version)
            log "kiss 0.5.10"
        ;;

        # Print usage and exit.
        h|he|hel|help|-h|--help|'')
            log "kiss [b|c|d|i|l|r|s|u] [pkg] [pkg] [pkg]" \
                "build:     Build a package." \
                "checksum:  Generate checksums." \
                "depends:   List package dependencies." \
                "install:   Install a package (Runs build if needed)." \
                "list:      List installed packages." \
                "manifest:  List package files owned by package." \
                "owns:      Check which package owns a file." \
                "remove:    Remove a package." \
                "search:    Search for a package." \
                "update:    Check for updates."
        ;;

        # Print message about invalid commands.
        *)
            die "'kiss $1' is not a valid command."
        ;;
    esac
}

main() {
    # Set the location to the repository and package database.
    pkg_db=var/db/kiss/installed

    # 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.
    mkdir -p "${cac_dir:=$KISS_ROOT/var/cache/kiss}" \
             "${mak_dir:=$cac_dir/build-$pid}" \
             "${pkg_dir:=$cac_dir/pkg-$pid}" \
             "${tar_dir:=$cac_dir/extract-$pid}" \
             "${src_dir:=$cac_dir/sources}" \
             "${bin_dir:=$cac_dir/bin}" \
        || die "Couldn't create cache directories."

    # Set sticky bit in the "permanent" directories so users can write to it.
    chmod 1777 "$cac_dir" "$src_dir" "$bin_dir"

    args "$@"
}

main "$@"