kiss/kiss-new
2019-06-29 10:11:07 +03:00

366 lines
12 KiB
Bash
Executable File

#!/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 is 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 "Sources file not found."
[ -x build ] || die "Build file not found or not executable."
[ -s licenses ] || die "License file not found or empty."
[ -s version ] || die "Version file not found or empty."
# 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.
# Changing directories is similar to storing the full
# full path in a variable, only there is no variable as
# you can access children relatively.
cd "$KISS_ROOT/var/db/kiss" || \
die "KISS database doesn't exist or is inaccessible."
# 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 -- *
# 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."
return
}
read -r version release < "$pkg/version" &&
printf '%s\n' "${pkg%/*} $version-$release"
done
}
pkg_sources() {
# Download any remote package sources.
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" || 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_build() {
# Build packages and turn them into packaged tarballs. This function
# also checks checksums, downloads sources and ensure all dependencies
# are installed.
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."
}
pkg_checksums() {
# Generate checksums for packages.
# This also downloads any remote sources.
for pkg; do pkg_lint "$pkg"; done
for pkg; do pkg_sources "$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")
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/checksums"
log "[$pkg]: Generated checksums."
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.
rm -rf -- "$mak_dir" "$pkg_dir" "$tar_dir"
}
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."
pkg_checksums "$@"
;;
# Install packages.
i*)
shift
[ "$1" ] || die "'kiss install' requires an argument."
root_check
;;
# Remove packages.
r*)
shift
[ "$1" ] || die "'kiss remove' requires an argument."
root_check
;;
# List installed packages.
l*)
shift
pkg_list "$@"
;;
# Print version and exit.
v*)
log "$kiss 0.1.10"
;;
# 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
setup_caching
args "$@"
}
main "$@"