mirror of
https://codeberg.org/kiss-community/kiss
synced 2024-10-02 06:21:01 -06:00
bcbdd99e5c
It is now possible to do 'kiss f' instead of 'kiss fork' to give a simple example. In the case where there is a conflict in single lettering, the first match will be used. This extends to 'kiss fo', 'kiss for' and so on.
1441 lines
49 KiB
Bash
Executable File
1441 lines
49 KiB
Bash
Executable File
#!/bin/sh -ef
|
|
# shellcheck source=/dev/null
|
|
#
|
|
# This is a simple package manager written in POSIX 'sh' for use
|
|
# in KISS Linux (https://k1ss.org).
|
|
#
|
|
# This script runs with '-ef' meaning:
|
|
# '-e': Abort on any non-zero exit code.
|
|
# '-f': Disable globbing globally.
|
|
#
|
|
# [1] Warnings related to word splitting and globbing are disabled.
|
|
# All word splitting in this script is *safe* and intentional.
|
|
#
|
|
# Dylan Araps.
|
|
|
|
log() {
|
|
# Print a message prettily.
|
|
#
|
|
# All messages are printed to stderr to allow the user to hide build
|
|
# output which is the only thing printed to stdout.
|
|
#
|
|
# '\033[1;32m' Set text to color '2' and make it bold.
|
|
# '\033[m': Reset text formatting.
|
|
# '${3:-->}': If the 3rd argument is missing, set prefix to '->'.
|
|
# '${2:+\033[1;3Xm}': If the 2nd argument exists, set text style of '$1'.
|
|
# '${2:+\033[m}': If the 2nd argument exists, reset text formatting.
|
|
printf '\033[1;33m%s \033[m%b%s\033[m %s\n' \
|
|
"${3:-->}" "${2:+\033[1;36m}" "$1" "$2" >&2
|
|
}
|
|
|
|
die() {
|
|
# Print a message and exit with '1' (error).
|
|
log "$1" "$2" "!>"
|
|
exit 1
|
|
}
|
|
|
|
contains() {
|
|
# Check if a "string list" contains a word.
|
|
case " $1 " in *" $2 "*) return 0; esac; return 1
|
|
}
|
|
|
|
prompt() {
|
|
# Ask the user for some input.
|
|
[ "$1" ] && log "$1"
|
|
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'.
|
|
# '_' is used as 'dash' errors when no variable is given to 'read'.
|
|
read -r _
|
|
}
|
|
|
|
as_root() {
|
|
# Simple function to run a command as root using either 'sudo',
|
|
# 'doas' or 'su'. Hurrah for choice.
|
|
[ "$uid" = 0 ] || log "Using '${su:-su}' (to become ${user:=root})"
|
|
|
|
case $su in
|
|
*sudo) sudo -E -u "$user" -- "$@" ;;
|
|
*doas) doas -u "$user" -- "$@" ;;
|
|
*) su -pc "$* <&3" "$user" 3<&0 </dev/tty ;;
|
|
esac
|
|
}
|
|
|
|
esc() {
|
|
# Escape all required characters in both the search and
|
|
# replace portions of two strings for use in a 'sed' call
|
|
# as "plain-text".
|
|
printf 's/^%s$/%s/' "$(printf %s "$1" | sed 's/[]\/$*.^[]/\\&/g')" \
|
|
"$(printf %s "$2" | sed 's/[\/&]/\\&/g')"
|
|
}
|
|
|
|
pop() {
|
|
# Remove an item from a "string list". This allows us
|
|
# to remove a 'sed' call and reuse this code throughout.
|
|
del=$1
|
|
shift 2
|
|
|
|
for i do [ "$i" = "$del" ] || printf %s " $i "; done
|
|
}
|
|
|
|
run_hook() {
|
|
[ "${KISS_HOOK:-}" ] || return 0
|
|
|
|
log "$2" "Running $1 hook"
|
|
|
|
TYPE=$1 PKG=$2 DEST=$3 . "$KISS_HOOK"
|
|
}
|
|
|
|
decompress() {
|
|
case $1 in
|
|
*.bz2) bzip2 -d ;;
|
|
*.xz) xz -dcT 0 ;;
|
|
*.tgz|*.gz) gzip -d ;;
|
|
*.zst) zstd -dc ;;
|
|
esac < "$1"
|
|
}
|
|
|
|
pkg_lint() {
|
|
log "$1" "Checking repository files"
|
|
|
|
cd "$(pkg_find "$1")"
|
|
read -r _ release 2>/dev/null < version || die "Version file not found"
|
|
|
|
[ "$release" ] || die "$1" "Release field not found in version file"
|
|
[ -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"
|
|
|
|
[ "$2" ] || [ -f checksums ] ||
|
|
die "$1" "Checksums are missing"
|
|
}
|
|
|
|
pkg_find() {
|
|
# Figure out which repository a package belongs to by
|
|
# searching for directories matching the package name
|
|
# in $KISS_PATH/*.
|
|
query=$1 all=$2 IFS=:; set --
|
|
|
|
# Both counts of word-splitting are intentional here.
|
|
# Firstly to split the repositories and secondly to
|
|
# allow for the query to be a glob.
|
|
# shellcheck disable=2086
|
|
for path in $KISS_PATH "$sys_db"; do
|
|
set +f
|
|
|
|
for path2 in "$path/"$query; do
|
|
[ -e "$path2" ] && set -f -- "$@" "$path2"
|
|
done
|
|
done
|
|
|
|
IFS=$old_ifs
|
|
|
|
# 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.
|
|
[ "$1" ] || die "Package '$query' not in any repository"
|
|
|
|
# Show all search results if called from 'kiss search', else
|
|
# print only the first match.
|
|
[ -t 1 ] || [ "$all" ] && printf '%s\n' "$@" || printf '%s\n' "$1"
|
|
}
|
|
|
|
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. .
|
|
cd "$sys_db" 2>/dev/null
|
|
|
|
# Optional arguments can be passed to check for specific
|
|
# packages. If no arguments are passed, list all.
|
|
[ "$1" ] || { set +f; set -f -- *; }
|
|
|
|
# Loop over each package and print its name and version.
|
|
for pkg do
|
|
[ -d "$pkg" ] || { log "$pkg" "not installed"; return 1; }
|
|
|
|
read -r version 2>/dev/null < "$pkg/version" || version=null
|
|
printf '%s\n' "$pkg $version"
|
|
done
|
|
}
|
|
|
|
pkg_cache() {
|
|
read -r version release 2>/dev/null < "$(pkg_find "$1")/version"
|
|
|
|
set +f; set -f -- "$bin_dir/$1#$version-$release.tar."*
|
|
tar_file=$1
|
|
|
|
[ -f "$tar_file" ]
|
|
}
|
|
|
|
pkg_sources() {
|
|
# Download any remote package sources. The existence of local
|
|
# files is also checked.
|
|
log "$1" "Downloading sources"
|
|
|
|
# Store each downloaded source in a directory 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"
|
|
|
|
while read -r src dest || [ "$src" ]; do
|
|
# Comment.
|
|
if [ -z "${src##\#*}" ]; then :
|
|
|
|
# Remote source (cached).
|
|
elif [ -f "${src##*/}" ]; then
|
|
log "$1" "Found cached source '${src##*/}'"
|
|
|
|
# Remote git repository.
|
|
elif [ -z "${src##git+*}" ]; then
|
|
# This is a checksums check, skip it.
|
|
[ "$2" ] && continue
|
|
|
|
mkdir -p "$mak_dir/$1/$dest"
|
|
|
|
# Run in a subshell to keep the variables, path and
|
|
# argument list local to each loop iteration.
|
|
(
|
|
repo_src=${src##git+}
|
|
|
|
log "$1" "Cloning ${repo_src%[@#]*}"
|
|
|
|
# Git has no option to clone a repository to a
|
|
# specific location so we must do it ourselves
|
|
# beforehand.
|
|
cd "$mak_dir/$1/$dest" || die 2>/dev/null
|
|
|
|
# Clear the argument list as we'll be overwriting
|
|
# it below based on what kind of checkout we're
|
|
# dealing with.
|
|
set -- "$repo_src"
|
|
|
|
# If a branch was given, shallow clone it directly.
|
|
# This speeds things up as we don't have to grab
|
|
# a lot of unneeded commits.
|
|
[ "${src##*@*}" ] ||
|
|
set -- -b "${src##*@}" "${repo_src%@*}"
|
|
|
|
# Maintain compatibility with older versions of
|
|
# kiss by shallow cloning all branches. This has
|
|
# the added benefit of allowing checkouts of
|
|
# specific commits in specific branches.
|
|
[ "${src##*#*}" ] ||
|
|
set -- --no-single-branch "${repo_src%#*}"
|
|
|
|
# Always do a shallow clone as we will unshallow it if
|
|
# needed later (when a commit is desired).
|
|
git clone --depth=1 "$@" .
|
|
|
|
) || die "$1" "Failed to clone $src"
|
|
|
|
# Remote source.
|
|
elif [ -z "${src##*://*}" ]; then
|
|
log "$1" "Downloading $src"
|
|
|
|
curl "$src" -fLo "${src##*/}" || {
|
|
rm -f "${src##*/}"
|
|
die "$1" "Failed to download $src"
|
|
}
|
|
|
|
# Local source.
|
|
elif [ -f "$(pkg_find "$1")/$src" ]; then
|
|
log "$1" "Found local file '$src'"
|
|
|
|
else
|
|
die "$1" "No local file '$src'"
|
|
fi
|
|
done < "$(pkg_find "$1")/sources"
|
|
}
|
|
|
|
pkg_extract() {
|
|
# Extract all source archives to the build directory and copy over
|
|
# any local repository files.
|
|
log "$1" "Extracting sources"
|
|
|
|
while read -r src dest || [ "$src" ]; do
|
|
mkdir -p "$mak_dir/$1/$dest" && cd "$mak_dir/$1/$dest"
|
|
|
|
case $src in
|
|
# Git repository with supplied commit hash.
|
|
git+*\#*)
|
|
log "Checking out ${src##*#}"
|
|
|
|
# A commit was requested, unshallow the repository.
|
|
# This will convert it to a regular repository with
|
|
# full history.
|
|
git fetch --unshallow
|
|
|
|
# Try to checkout the repository. If we fail here,
|
|
# the requested commit doesn't exist.
|
|
git -c advice.detachedHead=false checkout "${src##*#}" ||
|
|
die "Commit hash ${src##*#} doesn't exist"
|
|
;;
|
|
|
|
# Git repository, comment or blank line.
|
|
git+*|\#*|'') continue ;;
|
|
|
|
# Only 'tar' archives are currently supported for extraction.
|
|
# Any other file-types are simply copied to '$mak_dir' which
|
|
# allows for manual extraction.
|
|
*://*.tar|*://*.tar.??|*://*.tar.???|*://*.tar.????|*://*.tgz)
|
|
decompress "$src_dir/$1/${src##*/}" |
|
|
tar xf - --strip-components 1 ||
|
|
die "$1" "Couldn't extract ${src##*/}"
|
|
;;
|
|
|
|
*://*.zip)
|
|
unzip "$src_dir/$1/${src##*/}" ||
|
|
die "$1" "Couldn't extract ${src##*/}"
|
|
;;
|
|
|
|
*)
|
|
# Local file.
|
|
if [ -f "$(pkg_find "$1")/$src" ]; then
|
|
cp -f "$(pkg_find "$1")/$src" .
|
|
|
|
# Remote file.
|
|
elif [ -f "$src_dir/$1/${src##*/}" ]; then
|
|
cp -f "$src_dir/$1/${src##*/}" .
|
|
|
|
else
|
|
die "$1" "Local file $src not found"
|
|
fi
|
|
;;
|
|
esac
|
|
done < "$(pkg_find "$1")/sources"
|
|
}
|
|
|
|
pkg_depends() {
|
|
# Resolve all dependencies and generate an ordered list.
|
|
# This does a depth-first search. The deepest dependencies are
|
|
# listed first and then the parents in reverse order.
|
|
contains "$deps" "$1" || {
|
|
# Filter out non-explicit, aleady installed dependencies.
|
|
# Only filter installed if called from 'pkg_build()'.
|
|
[ "$pkg_build" ] && [ -z "$2" ] &&
|
|
(pkg_list "$1" >/dev/null) && return
|
|
|
|
# Recurse through the dependencies of the child packages.
|
|
while read -r dep _ || [ "$dep" ]; do
|
|
[ "${dep##\#*}" ] && pkg_depends "$dep"
|
|
done 2>/dev/null < "$(pkg_find "$1")/depends" ||:
|
|
|
|
# After child dependencies are added to the list,
|
|
# add the package which depends on them.
|
|
[ "$2" = explicit ] || deps="$deps $1 "
|
|
}
|
|
}
|
|
|
|
pkg_order() {
|
|
# Order a list of packages based on dependence and
|
|
# take into account pre-built tarballs if this is
|
|
# to be called from 'kiss i'.
|
|
order=; redro=; deps=
|
|
|
|
for pkg do case $pkg in
|
|
*.tar.*) deps="$deps $pkg " ;;
|
|
*) pkg_depends "$pkg" raw
|
|
esac done
|
|
|
|
# Filter the list, only keeping explicit packages.
|
|
# The purpose of these two loops is to order the
|
|
# argument list based on dependence.
|
|
for pkg in $deps; do ! contains "$*" "$pkg" || {
|
|
order="$order $pkg "
|
|
redro=" $pkg $redro"
|
|
} done
|
|
|
|
deps=
|
|
}
|
|
|
|
pkg_strip() {
|
|
# Strip package binaries and libraries. This saves space on the
|
|
# system as well as on the tar-balls we ship for installation.
|
|
[ -f "$mak_dir/$pkg/nostrip" ] && return
|
|
|
|
log "$1" "Stripping binaries and libraries"
|
|
|
|
# Strip only files matching the below ELF types.
|
|
# NOTE: 'readelf' is used in place of 'file' as
|
|
# it allows us to remove 'file' from the
|
|
# core repositories altogether.
|
|
find "$pkg_dir/$1" -type f | while read -r file; do
|
|
case $(readelf -h "$file") in
|
|
*" DYN "*) strip_opt=unneeded ;;
|
|
*" EXEC "*) strip_opt=all ;;
|
|
*" REL "*) strip_opt=debug ;;
|
|
*) continue
|
|
esac
|
|
|
|
strip "--strip-$strip_opt" "$file" &&
|
|
printf 'Stripped %10s %s\n' "($strip_opt)" "${file##$pkg_dir/$1}"
|
|
done 2>/dev/null ||:
|
|
}
|
|
|
|
pkg_fixdeps() {
|
|
# Dynamically look for missing runtime dependencies by checking
|
|
# each binary and library with 'ldd'. This catches any extra
|
|
# libraries and or dependencies pulled in by the package's
|
|
# build suite.
|
|
log "$1" "Checking for missing dependencies"
|
|
|
|
# Go to the directory containing the built package to
|
|
# simplify path building.
|
|
cd "$pkg_dir/$1/$pkg_db/$1"
|
|
|
|
# Generate a list of all installed manifests.
|
|
set +f; set -f -- "$sys_db/"*/manifest
|
|
|
|
# Make a copy of the depends file if it exists to have a
|
|
# reference to 'diff' against.
|
|
if [ -f depends ]; then
|
|
cp -f depends "$mak_dir/d"
|
|
dep_file=$mak_dir/d
|
|
else
|
|
dep_file=/dev/null
|
|
fi
|
|
|
|
# Get a list of binaries and libraries, false files
|
|
# will be found, however it's faster to get 'ldd' to check
|
|
# them anyway than to filter them out.
|
|
find "$pkg_dir/${PWD##*/}/" -type f 2>/dev/null |
|
|
|
|
while read -r file; do
|
|
# Run 'ldd' on the file and parse each line. The code
|
|
# then checks to see which packages own the linked
|
|
# libraries and it prints the result.
|
|
ldd "$file" 2>/dev/null | while read -r dep; do
|
|
# Skip lines containing 'ldd'.
|
|
[ "${dep##*ldd*}" ] || continue
|
|
|
|
# Extract the file path from 'ldd' output.
|
|
dep=${dep#* => }
|
|
dep=${dep% *}
|
|
dep=$(readlink -f "$dep")
|
|
|
|
# Figure out which package owns the file.
|
|
own=$("$grep" -lFx "${dep##$KISS_ROOT}" "$@")
|
|
own=${own%/*}
|
|
own=${own##*/}
|
|
|
|
# Skip listing these packages as dependencies.
|
|
case $own in musl|gcc|llvm|"${PWD##*/}"|"") continue; esac
|
|
|
|
printf '%s\n' "$own"
|
|
done ||:
|
|
done >> depends
|
|
|
|
# Remove duplicate entries from the new depends file.
|
|
# This removes duplicate lines looking *only* at the
|
|
# first column.
|
|
sort -uk1,1 -o depends depends 2>/dev/null ||:
|
|
|
|
# Display a 'diff' of the new dependencies against
|
|
# the old ones. '-N' treats non-existent files as blank.
|
|
diff "$dep_file" depends ||:
|
|
|
|
[ -s depends ] || rm -f depends
|
|
}
|
|
|
|
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 function 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: Print all files and directories and append '/' to directories.
|
|
# sort: Sort the output in *reverse*. Directories appear *after* their
|
|
# contents.
|
|
# sed: Remove the first character in each line (./dir -> /dir) and
|
|
# remove all lines which only contain '.'.
|
|
find . -type d -exec printf '%s/\n' {} + -o -print |
|
|
sort -r | sed '/^\.\/$/d;ss.ss' > "$pkg_dir/$1/$pkg_db/$1/manifest"
|
|
)
|
|
|
|
pkg_etcsums() (
|
|
# Generate checksums for each configuration file in the package's
|
|
# /etc/ directory for use in "smart" handling of these files.
|
|
log "$1" "Generating etcsums"
|
|
|
|
# This function runs as a sub-shell to avoid having to 'cd' back to the
|
|
# prior directory before being able to continue.
|
|
cd "$pkg_dir/$1/etc" 2>/dev/null || return 0; cd ..
|
|
|
|
find etc -type f -exec sha256sum {} + > "$pkg_dir/$1/$pkg_db/$1/etcsums"
|
|
)
|
|
|
|
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"
|
|
|
|
# Read the version information to name the package.
|
|
read -r version release < "$(pkg_find "$1")/version"
|
|
|
|
# Create a tar-ball from the contents of the built package.
|
|
"$tar" cf - -C "$pkg_dir/$1" . | case ${KISS_COMPRESS:=gz} in
|
|
bz2) bzip2 -z ;;
|
|
gz) gzip -6 ;;
|
|
xz) xz -zT 0 ;;
|
|
zst) zstd -z ;;
|
|
esac > "$bin_dir/$1#$version-$release.tar.${KISS_COMPRESS:=gz}"
|
|
|
|
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.
|
|
pkg_build=1
|
|
|
|
log "Resolving dependencies"
|
|
|
|
for pkg do contains "$explicit" "$pkg" || {
|
|
pkg_depends "$pkg" explicit
|
|
|
|
# Mark packages passed on the command-line
|
|
# separately from those detected as dependencies.
|
|
explicit="$explicit $pkg "
|
|
} done
|
|
|
|
[ "$pkg_update" ] || explicit_build=$explicit
|
|
|
|
# If an explicit package is a dependency of another explicit
|
|
# package, remove it from the explicit list as it needs to be
|
|
# installed as a dependency.
|
|
# shellcheck disable=2086
|
|
for pkg do contains "$deps" "$pkg" &&
|
|
explicit=$(pop "$pkg" from $explicit)
|
|
done
|
|
|
|
# See [1] at top of script.
|
|
# shellcheck disable=2046,2086
|
|
set -- $deps $explicit
|
|
|
|
log "Building: $*"
|
|
|
|
# Only ask for confirmation if more than one package needs to be built.
|
|
[ $# -gt 1 ] || [ "$pkg_update" ] && prompt
|
|
|
|
for pkg do pkg_lint "$pkg"; done
|
|
|
|
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 ! contains "$explicit_build" "$pkg" && pkg_cache "$pkg" && {
|
|
log "$pkg" "Found pre-built binary, installing"
|
|
(KISS_FORCE=1 args i "$tar_file")
|
|
|
|
# Remove the now installed package from the build list.
|
|
# See [1] at top of script.
|
|
# shellcheck disable=2046,2086
|
|
set -- $(pop "$pkg" from "$@")
|
|
} done
|
|
|
|
for pkg do pkg_sources "$pkg"; done
|
|
|
|
pkg_verify "$@"
|
|
|
|
# Finally build and create tarballs for all passed packages and
|
|
# dependencies.
|
|
for pkg do
|
|
log "$pkg" "Building package ($((in = in + 1))/$#)"
|
|
|
|
pkg_extract "$pkg"
|
|
repo_dir=$(pkg_find "$pkg")
|
|
|
|
# Install built packages to a directory under the package name
|
|
# to avoid collisions with other packages.
|
|
mkdir -p "$pkg_dir/$pkg/$pkg_db" && cd "$mak_dir/$pkg"
|
|
|
|
log "$pkg" "Starting build"
|
|
run_hook pre-build "$pkg" "$pkg_dir/$pkg"
|
|
|
|
# Call the build script, log the output to the terminal
|
|
# and to a file. There's no PIPEFAIL in POSIX shelll so
|
|
# we must resort to tricks like killing the script ourselves.
|
|
{ "$repo_dir/build" "$pkg_dir/$pkg" 2>&1 || {
|
|
log "$pkg" "Build failed"
|
|
log "$pkg" "Log stored to $log_dir/$pkg-$time-$pid"
|
|
run_hook build-fail "$pkg" "$pkg_dir/$pkg"
|
|
pkg_clean
|
|
kill 0
|
|
} } | tee "$log_dir/$pkg-$time-$pid"
|
|
|
|
# Delete the log file if the build succeeded to prevent
|
|
# the directory from filling very quickly with useless logs.
|
|
[ "$KISS_KEEPLOG" = 1 ] || rm -f "$log_dir/$pkg-$time-$pid"
|
|
|
|
# Copy the repository files to the package directory.
|
|
# This acts as the database entry.
|
|
cp -LRf "$repo_dir" "$pkg_dir/$pkg/$pkg_db/"
|
|
|
|
# We never ever want this. Let's end the endless conflicts
|
|
# and remove it. This will be the only exception for a
|
|
# specific removal of this kind.
|
|
rm -f "$pkg_dir/$pkg/usr/lib/charset.alias"
|
|
|
|
log "$pkg" "Successfully built package"
|
|
run_hook post-build "$pkg" "$pkg_dir/$pkg"
|
|
|
|
# Create the manifest file early and make it empty.
|
|
# This ensures that the manifest is added to the manifest.
|
|
: > "$pkg_dir/$pkg/$pkg_db/$pkg/manifest"
|
|
|
|
# If the package contains '/etc', add a file called
|
|
# 'etcsums' to the manifest. See comment directly above.
|
|
[ -d "$pkg_dir/$pkg/etc" ] &&
|
|
: > "$pkg_dir/$pkg/$pkg_db/$pkg/etcsums"
|
|
|
|
pkg_strip "$pkg"
|
|
pkg_fixdeps "$pkg"
|
|
pkg_manifest "$pkg"
|
|
pkg_etcsums "$pkg"
|
|
pkg_tar "$pkg"
|
|
|
|
# Install only dependencies of passed packages.
|
|
# Skip this check if this is a package update.
|
|
contains "$explicit" "$pkg" && [ -z "$pkg_update" ] && continue
|
|
|
|
log "$pkg" "Needed as a dependency or has an update, installing"
|
|
|
|
(KISS_FORCE=1 args i "$pkg")
|
|
done
|
|
|
|
# End here as this was a system update and all packages have been installed.
|
|
[ "$pkg_update" ] && return
|
|
|
|
log "Successfully built package(s)"
|
|
|
|
# Turn the explicit packages into a 'list'.
|
|
# See [1] at top of script.
|
|
# shellcheck disable=2046,2086
|
|
set -- $explicit
|
|
|
|
# Only ask for confirmation if more than one package needs to be installed.
|
|
[ $# -gt 1 ] && prompt "Install built packages? [$*]" && {
|
|
args i "$@"
|
|
return
|
|
}
|
|
|
|
log "Run 'kiss i $*' to install the package(s)"
|
|
}
|
|
|
|
pkg_checksums() {
|
|
# Generate checksums for packages.
|
|
while read -r src _ || [ "$src" ]; do
|
|
# Comment.
|
|
if [ -z "${src##\#*}" ]; then
|
|
continue
|
|
|
|
# File is local to the package.
|
|
elif [ -f "$(pkg_find "$1")/$src" ]; then
|
|
src_path=$(pkg_find "$1")/${src%/*}
|
|
|
|
# File is remote and was downloaded.
|
|
elif [ -f "$src_dir/$1/${src##*/}" ]; then
|
|
src_path=$src_dir/$1
|
|
|
|
# File is a git repository.
|
|
elif [ -z "${src##git+*}" ]; then
|
|
printf 'git %s\n' "$src"
|
|
continue
|
|
|
|
# Die here if source for some reason, doesn't exist.
|
|
else
|
|
die "$1" "Couldn't find source '$src'"
|
|
fi
|
|
|
|
# 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"
|
|
done < "$(pkg_find "$1")/sources"
|
|
}
|
|
|
|
pkg_verify() {
|
|
# Verify all package checksums. This is achieved by generating
|
|
# a new set of checksums and then comparing those with the old
|
|
# set.
|
|
for pkg do pkg_checksums "$pkg" | cmp - "$(pkg_find "$pkg")/checksums" || {
|
|
log "$pkg" "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$pkg "
|
|
} done
|
|
|
|
[ -z "$mismatch" ] || die "Checksum mismatch with: ${mismatch% }"
|
|
}
|
|
|
|
pkg_conflicts() {
|
|
# Check to see if a package conflicts with another.
|
|
log "$1" "Checking for package conflicts"
|
|
|
|
# Filter the tarball's manifest and select only files
|
|
# and any files they resolve to on the filesystem
|
|
# (/bin/ls -> /usr/bin/ls).
|
|
while read -r file; do
|
|
case $file in */) continue; esac
|
|
|
|
printf '%s/%s\n' \
|
|
"$(readlink -f "$KISS_ROOT/${file%/*}" 2>/dev/null)" \
|
|
"${file##*/}"
|
|
done < "$tar_dir/$1/$pkg_db/$1/manifest" > "$cac_dir/$pid-m"
|
|
|
|
[ -s "$cac_dir/$pid-m" ] || return 0
|
|
|
|
p_name=$1
|
|
|
|
# Generate a list of all installed package manifests
|
|
# and remove the current package from the list.
|
|
# shellcheck disable=2046,2086
|
|
set -- $(set +f; pop "$sys_db/$1/manifest" from "$sys_db"/*/manifest)
|
|
|
|
# Enable alternatives automatically if it is safe to do so.
|
|
# This checks to see that the package that is about to be installed
|
|
# doesn't overwrite anything it shouldn't in '/var/db/kiss/installed'.
|
|
"$grep" -Fxf "$cac_dir/$pid-m" -- "$@" |
|
|
"$grep" -q ":/var/db/kiss/installed/" || choice_auto=1
|
|
|
|
# Use 'grep' to list matching lines between the to
|
|
# be installed package's manifest and the above filtered
|
|
# list.
|
|
if [ "$KISS_CHOICE" != 0 ] && [ "$choice_auto" = 1 ]; then
|
|
"$grep" -Fxf "$cac_dir/$pid-m" -- "$@" |
|
|
|
|
# This is a novel way of offering an "alternatives" system.
|
|
# It is entirely dynamic and all "choices" are created and
|
|
# destroyed on the fly.
|
|
#
|
|
# When a conflict is found between two packages, the file
|
|
# is moved to a directory called "choices" and its name
|
|
# changed to store its parent package and its intended
|
|
# location.
|
|
#
|
|
# The package's manifest is then updated to reflect this
|
|
# new location.
|
|
#
|
|
# The 'kiss choices' command parses this directory and
|
|
# offers you the CHOICE of *swapping* entries in this
|
|
# directory for those on the filesystem.
|
|
#
|
|
# The choices command does the same thing we do here,
|
|
# it rewrites manifests and moves files around to make
|
|
# this work.
|
|
#
|
|
# Pretty nifty huh?
|
|
while IFS=: read -r _ con; do
|
|
log "$p_name" "Found conflict ($con), adding choice"
|
|
|
|
# Create the "choices" directory inside of the tarball.
|
|
# This directory will store the conflicting file.
|
|
mkdir -p "$tar_dir/$p_name/${cho_dir:=var/db/kiss/choices}"
|
|
|
|
# Construct the file name of the "db" entry of the
|
|
# conflicting file. (pkg_name>usr>bin>ls)
|
|
con_name=$(printf %s "$con" | sed 's|/|>|g')
|
|
|
|
# Move the conflicting file to the choices directory
|
|
# and name it according to the format above.
|
|
mv -f "$tar_dir/$p_name/$con" \
|
|
"$tar_dir/$p_name/$cho_dir/$p_name$con_name" 2>/dev/null || {
|
|
log "File must be in ${con%/*} and not a symlink to it"
|
|
log "This usually occurs when a binary is installed to"
|
|
log "/sbin instead of /usr/bin (example)"
|
|
log "Before this package can be used as an alternative,"
|
|
log "this must be fixed in $p_name. Contact the maintainer"
|
|
die "by checking 'git log' or by running 'kiss-maintainer'"
|
|
}
|
|
|
|
# Rewrite the package's manifest to update its location
|
|
# to its new spot (and name) in the choices directory.
|
|
sed -i "$(esc "$con" "/$cho_dir/$p_name$con_name")" \
|
|
"$tar_dir/$p_name/$pkg_db/$p_name/manifest"
|
|
done
|
|
|
|
elif "$grep" -Fxf "$cac_dir/$pid-m" -- "$@"; then
|
|
log "Package '$p_name' conflicts with another package" "" "!>"
|
|
log "Run 'KISS_CHOICE=1 kiss i $p_name' to add conflicts" "" "!>"
|
|
die "as alternatives."
|
|
fi
|
|
}
|
|
|
|
pkg_swap() {
|
|
# Swap between package alternatives.
|
|
pkg_list "$1" >/dev/null
|
|
|
|
alt=$(printf %s "$1$2" | sed 's|/|>|g')
|
|
cd "$sys_db/../choices"
|
|
|
|
[ -f "$alt" ] || [ -h "$alt" ] ||
|
|
die "Alternative '$1 $2' doesn't exist"
|
|
|
|
if [ -f "$2" ]; then
|
|
# Figure out which package owns the file we are going to
|
|
# swap for another package's.
|
|
#
|
|
# Print the full path to the manifest file which contains
|
|
# the match to our search.
|
|
pkg_owns=$(set +f; "$grep" -lFx "$2" "$sys_db/"*/manifest) ||:
|
|
|
|
# Extract the package name from the path above.
|
|
pkg_owns=${pkg_owns%/*}
|
|
pkg_owns=${pkg_owns##*/}
|
|
|
|
[ "$pkg_owns" ] ||
|
|
die "File '$2' exists on filesystem but isn't owned"
|
|
|
|
log "Swapping '$2' from '$pkg_owns' to '$1'"
|
|
|
|
# Convert the current owner to an alternative and rewrite
|
|
# its manifest file to reflect this.
|
|
cp -Pf "$2" "$pkg_owns>${alt#*>}"
|
|
sed -i "$(esc "$2" "$PWD/$pkg_owns>${alt#*>}")" \
|
|
"../installed/$pkg_owns/manifest"
|
|
fi
|
|
|
|
# Convert the desired alternative to a real file and rewrite
|
|
# the manifest file to reflect this. The reverse of above.
|
|
mv -f "$alt" "$2"
|
|
sed -i "$(esc "$PWD/$alt" "$2")" "../installed/$1/manifest"
|
|
}
|
|
|
|
pkg_etc() {
|
|
[ -d "$tar_dir/$pkg_name/etc" ] || return 0
|
|
|
|
(cd "$tar_dir/$pkg_name"
|
|
|
|
# Create all directories beforehand.
|
|
find etc -type d | while read -r dir; do
|
|
mkdir -p "$KISS_ROOT/$dir"
|
|
done
|
|
|
|
# Handle files in /etc/ based on a 3-way checksum check.
|
|
find etc ! -type d | while read -r file; do
|
|
{ sum_new=$(sha256sum "$file")
|
|
sum_sys=$(cd "$KISS_ROOT/"; sha256sum "$file")
|
|
sum_old=$("$grep" "$file$" "$mak_dir/c"); } 2>/dev/null ||:
|
|
|
|
log "$pkg_name" "Doing 3-way handshake for $file"
|
|
printf '%s\n' "Previous: ${sum_old:-null}"
|
|
printf '%s\n' "System: ${sum_sys:-null}"
|
|
printf '%s\n' "New: ${sum_new:-null}"
|
|
|
|
# Use a case statement to easily compare three strings at
|
|
# the same time. Pretty nifty.
|
|
case ${sum_old:-null}${sum_sys:-null}${sum_new} in
|
|
# old = Y, sys = X, new = Y
|
|
"${sum_new}${sum_sys}${sum_old}")
|
|
log "Skipping $file"
|
|
continue
|
|
;;
|
|
|
|
# old = X, sys = X, new = X
|
|
# old = X, sys = Y, new = Y
|
|
# old = X, sys = X, new = Y
|
|
"${sum_old}${sum_old}${sum_old}"|\
|
|
"${sum_old:-null}${sum_sys}${sum_sys}"|\
|
|
"${sum_sys}${sum_old}"*)
|
|
log "Installing $file"
|
|
new=
|
|
;;
|
|
|
|
# All other cases.
|
|
*)
|
|
log WARN "($pkg_name) saving /$file as /$file.new"
|
|
new=.new
|
|
;;
|
|
esac
|
|
|
|
cp -af "$file" "$KISS_ROOT/${file}${new}"
|
|
chown root:root "$KISS_ROOT/${file}${new}" 2>/dev/null
|
|
done) ||:
|
|
}
|
|
|
|
pkg_remove() {
|
|
# Remove a package and all of its files. The '/etc' directory
|
|
# is handled differently and configuration files are *not*
|
|
# overwritten.
|
|
pkg_list "$1" >/dev/null || return
|
|
|
|
# Make sure that nothing depends on this package.
|
|
[ "$2" = check ] && {
|
|
log "$1" "Checking for reverse dependencies"
|
|
|
|
(cd "$sys_db"; set +f; "$grep" -lFx "$1" -- */depends) &&
|
|
die "$1" "Can't remove package, others depend on it"
|
|
}
|
|
|
|
# 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
|
|
|
|
if [ -x "$sys_db/$1/pre-remove" ]; then
|
|
log "$1" "Running pre-remove script"
|
|
"$sys_db/$1/pre-remove" ||:
|
|
fi
|
|
|
|
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"
|
|
fi
|
|
done < "$sys_db/$1/manifest" 2>/dev/null
|
|
|
|
# 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.
|
|
#
|
|
# Package installation works similarly to the method used by
|
|
# Slackware in some of their tooling. It's not the obvious
|
|
# solution to the problem, however it is the best solution
|
|
# at this given time.
|
|
#
|
|
# When an installation is an update to an existing package,
|
|
# instead of removing the old version first we do something
|
|
# different.
|
|
#
|
|
# The new version is installed overwriting any files which
|
|
# it has in common with the previously installed version of
|
|
# the package.
|
|
#
|
|
# A "diff" is then generated between the old and new versions
|
|
# and contains any files existing in the old version but not
|
|
# the new version.
|
|
#
|
|
# The package manager then goes and removes these files which
|
|
# leaves us with the new package version in the file system
|
|
# and all traces of the old version gone.
|
|
#
|
|
# For good measure the package manager will then install the
|
|
# new package an additional two times. Firstly to ensure that
|
|
# the above diff didn't contain anything incorrect. And
|
|
# Secondly to confirm that everything is sane.
|
|
#
|
|
# This is the better method as it is "seamless". An update to
|
|
# busybox won't create a window in which there is no access
|
|
# to all of its utilities to give an example.
|
|
|
|
# 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 "$1" ] && [ -z "${1%%*.tar.*}" ] ; then
|
|
tar_file=$1 pkg_name=${1##*/} pkg_name=${pkg_name%#*}
|
|
|
|
elif pkg_cache "$1" 2>/dev/null; then
|
|
pkg_name=$1
|
|
|
|
else
|
|
die "package has not been built, run 'kiss b pkg'"
|
|
fi
|
|
|
|
mkdir -p "$tar_dir/$pkg_name"
|
|
log "$pkg_name" "Extracting $tar_file"
|
|
|
|
# The tarball is extracted to a temporary directory where its
|
|
# contents are then "installed" to the filesystem using 'rsync'.
|
|
#
|
|
# Running this step as soon as possible allows us to also check
|
|
# the validity of the tarball and bail out early if needed.
|
|
decompress "$tar_file" | "$tar" pxf - -C "$tar_dir/$pkg_name"
|
|
|
|
# Naively assume that the existence of a manifest file is all
|
|
# that determines a valid KISS package from an invalid one.
|
|
# This should be a fine assumption to make in 99.99% of cases.
|
|
[ -f "$tar_dir/$pkg_name/$pkg_db/$pkg_name/manifest" ] ||
|
|
die "'${tar_file##*/}' is not a valid KISS package"
|
|
|
|
log "$pkg_name" "Checking that all dependencies are installed"; {
|
|
[ -f "$tar_dir/$pkg_name/$pkg_db/$pkg_name/depends" ] &&
|
|
[ -z "$KISS_FORCE" ] &&
|
|
while read -r dep dep_type || [ "$dep" ]; do
|
|
[ "${dep##\#*}" ] || continue
|
|
[ "$dep_type" ] || pkg_list "$dep" >/dev/null ||
|
|
install_dep="$install_dep'$dep', "
|
|
done < "$tar_dir/$pkg_name/$pkg_db/$pkg_name/depends"
|
|
|
|
[ "$install_dep" ] && die "$1" "Package requires ${install_dep%, }"
|
|
}
|
|
|
|
run_hook pre-install "$pkg_name" "$tar_dir/$pkg_name"
|
|
pkg_conflicts "$pkg_name"
|
|
|
|
log "$pkg_name" "Installing package incrementally"
|
|
|
|
# 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 and etcsums files.
|
|
cp -f "$sys_db/$pkg_name/manifest" "$mak_dir/m" 2>/dev/null ||:
|
|
cp -f "$sys_db/$pkg_name/etcsums" "$mak_dir/c" 2>/dev/null ||:
|
|
|
|
# This rsync command is used to install the tarball's contents to the
|
|
# filesystem. Your first thought is most probably something along these
|
|
# lines; "Why don't you just use tar extraction for installation directly?"
|
|
#
|
|
# The tar command has no real standard for available features, command-line
|
|
# flags or behavior. This makes satisfying the requirements for installation
|
|
# difficult and error-prone across implementations of tar.
|
|
#
|
|
# We need to exclude /etc from the tarball, ensure permissions are all owned
|
|
# by root:root, dump suid/guid permissions from directories and overwrite
|
|
# all existing files.
|
|
#
|
|
# Rsync ticks all boxes here and it being a "single implementation" of itself
|
|
# ensures portability everywhere so long as rsync is available. To top it all
|
|
# off, rsync is really handy to have around regardless.
|
|
pkg_rsync() { rsync --chown=root:root --chmod=Du-s,Dg-s,Do-s \
|
|
-WhHKa --no-compress --exclude /etc "$1" \
|
|
"$tar_dir/$pkg_name/" "$KISS_ROOT/"; }
|
|
pkg_rsync --info=progress2
|
|
|
|
# Handle /etc/ files in a special way (via a 3-way checksum) to determine
|
|
# how these files should be installed. Do we overwrite the existing file?
|
|
# Do we install it as $file.new to avoid deleting user configuration? etc.
|
|
#
|
|
# This is more or less similar to Arch Linux's Pacman with the user manually
|
|
# handling the .new files when and if they appear.
|
|
pkg_etc
|
|
|
|
# This is the aforementioned step removing any files from the old version of
|
|
# the package if the installation is an update. Each file type has to be
|
|
# specially handled to ensure no system breakage occurs.
|
|
#
|
|
# Files in /etc/ are skipped entirely as they'll be handled via a 3-way
|
|
# checksum system due to the nature of their existence.
|
|
"$grep" -vFxf "$sys_db/$pkg_name/manifest" "$mak_dir/m" 2>/dev/null |
|
|
|
|
while read -r file; do
|
|
file=$KISS_ROOT/$file
|
|
|
|
# Skip deleting some leftover files.
|
|
case $file in /etc/*) continue; esac
|
|
|
|
# Remove files.
|
|
if [ -f "$file" ] && [ ! -L "$file" ]; then
|
|
rm -f "$file"
|
|
|
|
# Remove file symlinks.
|
|
elif [ -L "$file" ] && [ ! -d "$file" ]; then
|
|
unlink "$file" ||:
|
|
|
|
# Skip directory symlinks.
|
|
elif [ -L "$file" ] && [ -d "$file" ]; then :
|
|
|
|
# Remove directories if empty.
|
|
elif [ -d "$file" ]; then
|
|
rmdir "$file" 2>/dev/null ||:
|
|
fi
|
|
done ||:
|
|
|
|
# Install the package an additional two times. The first being to fix
|
|
# any potential issues (rare) with the above removal of old files.
|
|
# The second rsync call confirms that nothing else need to be done.
|
|
#
|
|
# This takes zero time at all if unneeded as rsync is incremental.
|
|
# If there is nothing to be done, nothing will be done.
|
|
{ pkg_rsync --; pkg_rsync --; } ||:
|
|
|
|
# Reset 'trap' to its original value. Installation is done so
|
|
# we no longer need to block 'Ctrl+C'.
|
|
trap pkg_clean EXIT INT
|
|
|
|
if [ -x "$sys_db/$pkg_name/post-install" ]; then
|
|
log "$pkg_name" "Running post-install script"
|
|
"$sys_db/$pkg_name/post-install" ||:
|
|
fi
|
|
|
|
run_hook post-install "$pkg_name" "$sys_db/$pkg_name"
|
|
|
|
log "$pkg_name" "Installed successfully"
|
|
}
|
|
|
|
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.
|
|
# See [1] at top of script.
|
|
# shellcheck disable=2046,2086
|
|
{ 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
|
|
# Go to the root of the repository (if it exists).
|
|
cd "$repo"
|
|
cd "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null ||:
|
|
|
|
[ -d .git ] || {
|
|
log "$repo" " "
|
|
printf '%s\n' "Not a git repository, skipping."
|
|
continue
|
|
}
|
|
|
|
[ "$(git remote 2>/dev/null)" ] || {
|
|
log "$repo" " "
|
|
printf '%s\n' "No remote, skipping."
|
|
continue
|
|
}
|
|
|
|
contains "$repos" "$PWD" || {
|
|
repos="$repos $PWD "
|
|
|
|
# Display a tick if signing is enabled for this
|
|
# repository.
|
|
case $(git config merge.verifySignatures) in
|
|
true) log "$PWD" "[signed ✓] " ;;
|
|
*) log "$PWD" " " ;;
|
|
esac
|
|
|
|
if [ -w "$PWD" ] && [ "$uid" != 0 ]; then
|
|
git fetch
|
|
git merge
|
|
|
|
else
|
|
[ "$uid" = 0 ] || log "$PWD" "Need root to update"
|
|
|
|
# Find out the owner of the repository and spawn
|
|
# git as this user below.
|
|
#
|
|
# This prevents 'git' from changing the original
|
|
# ownership of files and directories in the rare
|
|
# case that the repository is owned by a 3rd user.
|
|
(
|
|
user=$(stat -c %U "$PWD")
|
|
|
|
[ "${user:=root}" = root ] ||
|
|
log "Dropping permissions to $user for pull"
|
|
|
|
case $su in
|
|
su) "$su" -c "git fetch && git merge" "$user" ;;
|
|
*) "$su" -u "$user" git fetch
|
|
"$su" -u "$user" git merge
|
|
esac
|
|
)
|
|
fi
|
|
}
|
|
done
|
|
|
|
log "Checking for new package versions"
|
|
|
|
set +f
|
|
|
|
for pkg in "$sys_db/"*; do
|
|
pkg_name=${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 < "$(pkg_find "$pkg_name")/version"
|
|
|
|
# Compare installed packages to repository packages.
|
|
[ "$db_ver-$db_rel" != "$re_ver-$re_rel" ] && {
|
|
printf '%s\n' "$pkg_name $db_ver-$db_rel ==> $re_ver-$re_rel"
|
|
outdated="$outdated$pkg_name "
|
|
}
|
|
done
|
|
|
|
set -f
|
|
|
|
contains "$outdated" kiss && {
|
|
log "Detected package manager update"
|
|
log "The package manager will be updated first"
|
|
|
|
prompt
|
|
|
|
pkg_build kiss
|
|
args i kiss
|
|
|
|
log "Updated the package manager"
|
|
log "Re-run 'kiss update' to update your system"
|
|
|
|
exit 0
|
|
}
|
|
|
|
[ "$outdated" ] || {
|
|
log "Everything is up to date"
|
|
return
|
|
}
|
|
|
|
log "Packages to update: ${outdated% }"
|
|
|
|
# Build all packages requiring an update.
|
|
# See [1] at top of script.
|
|
# shellcheck disable=2046,2086
|
|
{
|
|
pkg_update=1
|
|
pkg_order $outdated
|
|
pkg_build $order
|
|
}
|
|
|
|
log "Updated all packages"
|
|
}
|
|
|
|
pkg_clean() {
|
|
# Clean up on exit or error. This removes everything related
|
|
# to the build.
|
|
[ "$KISS_DEBUG" != 1 ] || return
|
|
|
|
# Create a list containing the current invocation's temporary
|
|
# files and directories.
|
|
set +f -- "$mak_dir" "$pkg_dir" "$tar_dir" "$cac_dir/$pid-m"
|
|
|
|
# Go through the cache and add any entries which don't belong
|
|
# to a currently running kiss instance.
|
|
for dir in "$cac_dir/"[bep]*-[0-9]*; do
|
|
[ -e "/proc/${dir##*-}" ] || set -- "$@" "$dir"
|
|
done
|
|
|
|
rm -rf -- "$@"
|
|
}
|
|
|
|
args() {
|
|
# Parse script arguments manually. 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.
|
|
action=$1
|
|
|
|
# 'dash' exits on error here if 'shift' is used and there are zero
|
|
# arguments despite trapping the error ('|| :').
|
|
shift "$(($# > 0 ? 1 : 0))"
|
|
|
|
# Unless this is a search, sanitize the user's input. The call to
|
|
# 'pkg_find()' supports basic globbing, ensure input doesn't expand
|
|
# to anything except for when this behavior is needed.
|
|
#
|
|
# This handles the globbing characters '*', '!', '[' and ']' as per:
|
|
# https://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html
|
|
[ "${action##[as]*}" ] &&
|
|
case $@ in *\**|*\!*|*\[*|*\]*)
|
|
die "Arguments contain invalid characters: '!*[]'"
|
|
esac
|
|
|
|
# Parse some arguments earlier to remove the need to duplicate code.
|
|
case $action in
|
|
s|search)
|
|
[ "$1" ] || die "'kiss $action' requires an argument"
|
|
;;
|
|
|
|
a|alternatives)
|
|
# Rerun the script with 'su' if the user isn't root.
|
|
# Cheeky but 'su' can't be used on shell functions themselves.
|
|
[ -z "$1" ] || [ "$uid" = 0 ] || {
|
|
as_root kiss "$action" "$@"
|
|
return
|
|
}
|
|
;;
|
|
|
|
i|install|r|remove)
|
|
# Rerun the script with 'su' if the user isn't root.
|
|
# Cheeky but 'su' can't be used on shell functions themselves.
|
|
[ "$uid" = 0 ] || {
|
|
KISS_FORCE="$KISS_FORCE" as_root kiss "$action" "$@"
|
|
return
|
|
}
|
|
;;
|
|
esac
|
|
|
|
# Second early check to use $PWD in place of arguments.
|
|
[ "$1" ] || case $action in b|build|c|checksum|i|install|r|remove)
|
|
export KISS_PATH=${PWD%/*}:$KISS_PATH
|
|
set -- "${PWD##*/}"
|
|
esac
|
|
|
|
# Actions can be abbreviated to their first letter. This saves
|
|
# keystrokes once you memorize the commands.
|
|
case $action in
|
|
a|alternatives)
|
|
if [ "$1" = - ]; then
|
|
while read -r pkg path; do
|
|
pkg_swap "$pkg" "$path"
|
|
done
|
|
|
|
elif [ "$1" ]; then
|
|
pkg_swap "$@"
|
|
|
|
else
|
|
# Go over each alternative and format the file
|
|
# name for listing. (pkg_name>usr>bin>ls)
|
|
set +f; for pkg in "$sys_db/../choices"/*; do
|
|
printf '%s\n' "${pkg##*/}"
|
|
done | sed 's|>| /|; s|>|/|g; /\*/d'
|
|
fi
|
|
;;
|
|
|
|
c|checksum)
|
|
for pkg do pkg_lint "$pkg" c; done
|
|
for pkg do pkg_sources "$pkg" c; done
|
|
for pkg do
|
|
pkg_checksums "$pkg" | {
|
|
repo_dir=$(pkg_find "$pkg")/checksums
|
|
|
|
if [ -w "$repo_dir" ]; then
|
|
tee "$repo_dir"
|
|
else
|
|
log "$pkg" "Need permissions to generate checksums"
|
|
|
|
user=$(stat -c %U "$repo_dir") as_root tee "$repo_dir"
|
|
fi
|
|
}
|
|
|
|
log "$pkg" "Generated checksums"
|
|
done
|
|
;;
|
|
|
|
i|install)
|
|
pkg_order "$@"
|
|
|
|
for pkg in $order; do pkg_install "$pkg"; done
|
|
;;
|
|
|
|
r|remove)
|
|
pkg_order "$@"
|
|
|
|
for pkg in $redro; do
|
|
pkg_remove "$pkg" "${KISS_FORCE:-check}"
|
|
done
|
|
;;
|
|
|
|
b|build) pkg_build "${@:?No packages installed}" ;;
|
|
l|list) pkg_list "$@" ;;
|
|
u|update) pkg_updates ;;
|
|
s|search) for pkg do pkg_find "$pkg"; done ;;
|
|
v|version) log kiss 1.11.0 ;;
|
|
|
|
h|help|-h|--help|'')
|
|
log 'kiss [a|b|c|i|l|r|s|u|v] [pkg] [pkg] [pkg]'
|
|
log 'alternatives List and swap to alternatives'
|
|
log 'build Build a package'
|
|
log 'checksum Generate checksums'
|
|
log 'install Install a package'
|
|
log 'list List installed packages'
|
|
log 'remove Remove a package'
|
|
log 'search Search for a package'
|
|
log 'update Check for updates'
|
|
log 'version Package manager version
|
|
'
|
|
|
|
log "Installed extensions (kiss-* in \$PATH)"
|
|
|
|
for path in $(KISS_PATH=$PATH pkg_find kiss-\* all); do
|
|
set -- "${path#*/kiss-}" "$@"
|
|
max=$((${#1} > max ? ${#1} : max))
|
|
done
|
|
|
|
for path do
|
|
printf '\033[31;1m->\033[m %-*s ' "$max" "${path#*/kiss-}"
|
|
sed -n 's/^# *//;2p' "$(command -v "kiss-$path")"
|
|
done | sort -uk1 >&2
|
|
;;
|
|
|
|
*)
|
|
util=$(KISS_PATH=$PATH pkg_find "kiss-$action*" 2>/dev/null) ||
|
|
die "'kiss $action' is not a valid command"
|
|
|
|
"$util" "$@"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
main() {
|
|
# Die here if the user has no set KISS_PATH. This is a rare occurance
|
|
# as the environment variable should always be defined.
|
|
[ "$KISS_PATH" ] || die "\$KISS_PATH needs to be set"
|
|
|
|
# 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=${KISS_PID:-$$}
|
|
|
|
# Store the original value of IFS so we can revert back to it if the
|
|
# variable is ever changed.
|
|
old_ifs=$IFS
|
|
|
|
# Force the C locale to speed up things like 'grep' which disable unicode
|
|
# etc when this is set. We don't need unicode and a speed up is always
|
|
# welcome.
|
|
export LC_ALL=C LANG=C
|
|
|
|
# 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
|
|
|
|
# Prefer GNU grep if installed as it is much much faster than busybox's
|
|
# implementation. Very much worth it if you value performance over
|
|
# POSIX correctness (grep quoted to avoid shellcheck false-positive).
|
|
grep=$(command -v ggrep) || grep='grep'
|
|
|
|
# Prefer libarchive tar or GNU tar if installed as they are much
|
|
# much faster than busybox's implementation. Very much worth it if
|
|
# you value performance.
|
|
tar=$(command -v bsdtar || command -v gtar) || tar=tar
|
|
|
|
# Figure out which 'sudo' command to use based on the user's choice or
|
|
# what is available on the system.
|
|
su=${KISS_SU:-$(command -v sudo || command -v doas)} || su=su
|
|
|
|
# Store the date and time of script invocation to be used as the name
|
|
# of the log files the package manager creates uring builds.
|
|
time=$(date '+%Y-%m-%d-%H:%M')
|
|
|
|
# Make note of the user's current ID to do root checks later on.
|
|
# This is used enough to warrant a place here.
|
|
uid=$(id -u)
|
|
|
|
# Make sure that the KISS_ROOT doesn't end with a '/'. This might
|
|
# break some operations.
|
|
KISS_ROOT=${KISS_ROOT%/}
|
|
|
|
# This allows for automatic setup of a KISS chroot and will
|
|
# do nothing on a normal system.
|
|
mkdir -p "${sys_db:=$KISS_ROOT/$pkg_db}" 2>/dev/null ||:
|
|
|
|
# Create the required temporary directories and set the variables
|
|
# which point to them.
|
|
mkdir -p "${cac_dir:=$KISS_ROOT${XDG_CACHE_HOME:-$HOME/.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}" \
|
|
"${log_dir:=$cac_dir/logs}" \
|
|
"${bin_dir:=$cac_dir/bin}"
|
|
|
|
args "$@"
|
|
}
|
|
|
|
main "$@"
|