#!/bin/sh -e
#
# This is a simple package manager written in POSIX 'sh' for
# KISS Linux utlizing the core unix utilites 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..."

    pkg_location=$(pkg_search "$1")

    cd "$pkg_location" || die "'$pkg_location' 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."
    [ -f checksums ] || die "[$1]: Checksums file not found."

    # Ensure that the release field in the version file is set
    # to something.
    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
    # diectories, 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 diectory 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
            # diectory directly.
            git:*) ;;

            # Only 'tar' archives are currently supported for extaction.
            # Any other filetypes 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.
    if pkg_list "$1" >/dev/null; then
        # If a package is already installed but 'pkg_depends' was
        # given an argument, add it to the list anyway.
        [ "$2" ] && missing_deps="$missing_deps $1 "
    else
        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
    fi
}

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
    # repositorie'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 tarballs we ship for installation.
    log "[$1]: Stripping binaries and libraries..."

    # 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 subshell 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 tarball from the built package's files.
    # This tarball also contains the package's database entry.
    log "[$1]: Creating tarball..."

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

    log "[$1]: Successfully created tarball."
}

pkg_build() {
    # Build packages and turn them into packaged tarballs. 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" force; 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
    }
    log "Installing: $*."
    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 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 avod 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]: Sucessfully 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 $*' 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 basenames
                    # 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 tarball as an argument.
    log "[$2]: Checking for package conflicts."

    # Extract manifest from the tarball 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 coreutils of choice doesn't break the package manager.
    cp "$(command -v rm)"    "$cac_dir"
    cp "$(command -v rmdir)" "$cac_dir"

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

        while read -r file; do
            # The file is in '/etc' skip it. This prevents the package
            # manager from removing user edited config 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 "[$pkg]: Failed to remove '$file'."
            fi
        done < "$KISS_ROOT/var/db/kiss/$pkg/manifest"

        log "[$pkg]: Removed successfully."
    done
}

pkg_install() {
    # Install a built package tarball.

    for pkg; do
        # Install can also take the full path to a tarball.
        # 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 tarball installs by checking for
        # a database entry inside the tarball. If no database entry exists,
        # exit here as the tarball 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 tarball 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 tarball."

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

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

        # Installation works by unpacking the tarball to a specified location,
        # manually running 'mkdir' to create each directory and finally, using
        # 'mv' to move each file.
        cd "$tar_dir"

        # Create all of the package's directories.
        # Optimization: Only find the deepest directories.
        "$cac_dir/find" . -type d -links -3 -prune | while read -r dir; do
            "$cac_dir/mkdir" -p "$KISS_ROOT/${dir#./}"
        done

        # Move all package files to '$KISS_ROOT'.
        "$cac_dir/find" ./ -mindepth 1 -not -type d | while read -r file; do
            rpath=${file#.}

            # Don't overwrite existing '/etc' files.
            [ -z "${rpath##/etc/*}" ] &&
            [ -f "$KISS_ROOT/${rpath%/*}/${file##*/}" ] &&
                return

            "$cac_dir/mv" "$file" "$KISS_ROOT/${rpath%/*}"
        done

        # 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.
    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##*/} $re_ver-$re_rel"
    done
}

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/mkdir" \
              "$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 themand 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
            [ "$1" ] || die "'kiss build' requires an argument."
            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
            pkg_remove "$@"
        ;;

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

        # Upgrade packages.
        u*)
            pkg_updates
        ;;

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

        # Catch all invalid arguments as well as
        # any help related flags (-h, --help, help).
        *)
            log "$kiss [b|c|i|l|r|u] [pkg]" \
                "build:     Build a package." \
                "checksum:  Generate checksums." \
                "install:   Install a package (Runs build if needed)." \
                "list:      List packages." \
                "remove:    Remove 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=$$

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

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

    args "$@"
}

main "$@"