├── .github ├── ISSUE_TEMPLATE.md └── workflows │ └── main.yml ├── .woodpecker.yml ├── LICENSE ├── README ├── contrib ├── kiss-chroot ├── kiss-depends ├── kiss-fork ├── kiss-help ├── kiss-link ├── kiss-maintainer ├── kiss-manifest ├── kiss-new ├── kiss-orphans ├── kiss-outdated ├── kiss-owns ├── kiss-repodepends ├── kiss-reporevdepends ├── kiss-revdepends └── kiss-size └── kiss /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | - [ ] Does this issue occur in master? 2 | 3 | ## Description 4 | 5 | 6 | ## Error message 7 | 8 | ``` 9 | terminal output 10 | ``` 11 | 12 | ## Verbose log 13 | 14 | 1. Download https://github.com/kisslinux/kiss/raw/master/kiss 15 | 2. Run `env KISS_PROMPT=0 sh -x ./kiss > log 2>&1` 16 | 2. Upload the contents of `log`. 17 | 18 | --- 19 | 20 | Issues without attached log file will be closed unless steps to reproduce the 21 | problem are provided in place of it. Issues not using the template will be 22 | closed. Feature requests and otherwise non-issues can disregard this notice. 23 | 24 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Shellcheck 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - name: Run shellcheck. 9 | run: shellcheck kiss contrib/* 10 | -------------------------------------------------------------------------------- /.woodpecker.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | build: 3 | image: alpine 4 | when: 5 | branch: [ master ] 6 | event: [push, pull_request] 7 | commands: | 8 | apk add --no-cache shellcheck 9 | 10 | shellcheck kiss contrib/* 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019-2021 Dylan Araps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | |/ 2 | |\ISS LINUX https://kisslinux.github.io 3 | ________________________________________________________________________________ 4 | 5 | 6 | Package Manager 7 | ________________________________________________________________________________ 8 | 9 | KISS' tiny package manager. 10 | 11 | Documentation: 12 | 13 | - https://kisslinux.github.io/package-manager 14 | - https://kisslinux.github.io/package-system 15 | -------------------------------------------------------------------------------- /contrib/kiss-chroot: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Enter a kiss chroot 3 | 4 | log() { 5 | printf '\033[32m->\033[m %s.\n' "$*" 6 | } 7 | 8 | die() { 9 | log "$*" >&2 10 | exit 1 11 | } 12 | 13 | run() { 14 | printf '%s\n' "$*" 15 | "$@" || return "${_ret:=0}" 16 | } 17 | 18 | clean() { 19 | log Unmounting host paths; { 20 | run umount "$1/dev/shm" 2>/dev/null 21 | run umount "$1/dev/pts" 22 | run umount "$1/dev" 23 | run umount "$1/proc" 24 | run umount "$1/run" 25 | run umount "$1/sys/firmware/efi/efivars" 2>/dev/null 26 | run umount "$1/sys" 27 | run umount "$1/tmp" 28 | run umount "$1/etc/resolv.conf" 29 | } 30 | } 31 | 32 | mounted() { 33 | # This is a pure shell mountpoint implementation. We're dealing 34 | # with basic (and fixed/known) input so this doesn't need to 35 | # handle more complex cases. 36 | [ -e "$1" ] || return 1 37 | [ -e /proc/mounts ] || return 1 38 | 39 | while read -r _ target _; do 40 | [ "$target" = "$1" ] && return 0 41 | done < /proc/mounts 42 | 43 | return 1 44 | } 45 | 46 | mmount() { 47 | dest=$1 48 | shift 49 | mounted "$dest" || run mount "$@" "$dest" 50 | } 51 | 52 | main() { 53 | # Ensure input does not end in '/'. 54 | set -- "${1%"${1##*[!/]}"}" 55 | 56 | [ "$1" ] || die Need a path to the chroot 57 | [ -d "$1" ] || die Given path does not exist 58 | [ "$(id -u)" = 0 ] || die Script needs to be run as root 59 | 60 | # Intended behaviour. 61 | # shellcheck disable=SC2064 62 | trap "clean ${1%"${1##*[!/]}"}" EXIT INT 63 | 64 | log Mounting host paths; { 65 | mmount "$1/dev" -o bind /dev 66 | mmount "$1/dev/pts" -o bind /dev/pts 67 | mmount "$1/dev/shm" -t tmpfs shmfs 2>/dev/null 68 | mmount "$1/proc" -t proc proc 69 | mmount "$1/run" -t tmpfs tmpfs 70 | mmount "$1/sys" -t sysfs sys 71 | mmount "$1/sys/firmware/efi/efivars" -t efivarfs efivarfs 2>/dev/null 72 | mmount "$1/tmp" -o mode=1777,nosuid,nodev -t tmpfs tmpfs 73 | 74 | touch "$1/etc/resolv.conf" 75 | mmount "$1/etc/resolv.conf" -o bind /etc/resolv.conf 76 | } 77 | 78 | log Entering chroot; { 79 | _ret=1 80 | 81 | run chroot "$1" /usr/bin/env -i \ 82 | HOME=/root \ 83 | TERM="$TERM" \ 84 | SHELL=/bin/sh \ 85 | USER=root \ 86 | LOGNAME=root \ 87 | CFLAGS="${CFLAGS:--march=x86-64 -mtune=generic -pipe -O2}" \ 88 | CXXFLAGS="${CXXFLAGS:--march=x86-64 -mtune=generic -pipe -O2}" \ 89 | MAKEFLAGS="${MAKEFLAGS:--j$(nproc 2>/dev/null || echo 1)}" \ 90 | /bin/sh -l 91 | } || die chroot failed 92 | } 93 | 94 | main "$1" 95 | -------------------------------------------------------------------------------- /contrib/kiss-depends: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ef 2 | # Display a package's dependencies 3 | 4 | pkg=${1:-"${PWD##*/}"} 5 | 6 | kiss list "$pkg" >/dev/null || { 7 | printf 'usage: kiss-depends [pkg]\n' >&2 8 | exit 1 9 | } 10 | 11 | while read -r dep mak || [ "$dep" ]; do 12 | printf '%s%s\n' "$dep" "${mak:+ "$mak"}" 13 | done 2>/dev/null < "$KISS_ROOT/var/db/kiss/installed/$pkg/depends" 14 | -------------------------------------------------------------------------------- /contrib/kiss-fork: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ef 2 | # Copy a package's repository files into the current directory 3 | 4 | pkg=${1:-"${PWD##*/}"} 5 | num=$(printf %d "${2:-0}") 6 | 7 | dir=$(kiss search "$pkg" 2>/dev/null) || { 8 | printf 'usage: [kiss-fork [pkg]] [index]\n' 9 | exit 1 10 | } 11 | 12 | # Globbing is disabled and word splitting is intentional. 13 | # shellcheck disable=2086 14 | set -- $dir 15 | 16 | [ "$num" -ge "$#" ] && { 17 | printf 'index exceeds maximum\n' 18 | exit 1 19 | } 20 | 21 | shift "$num" 22 | 23 | [ "$1" ] || [ -d "$1" ] || { 24 | printf 'failed to locate package\n' 25 | exit 1 26 | } 27 | 28 | printf 'found package in %s\n' "$1" 29 | cp -Lrf "$1" . 30 | printf 'forked package to %s\n' "$PWD/$pkg" 31 | -------------------------------------------------------------------------------- /contrib/kiss-help: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # Read KISS documentation 3 | 4 | cd "$KISS_ROOT/usr/share/doc/kiss" 2>/dev/null || { 5 | printf 'Documentation is missing from /usr/share/doc/kiss\n' 6 | exit 1 7 | } 8 | 9 | _q=$1 10 | 11 | ! [ -f "${_q:-.}/index.txt" ] || file=./${_q:-.}/index.txt 12 | ! [ -f "${_q:-.}.txt" ] || file=./${_q:-.}.txt 13 | ! [ -f "${_q:-:}" ] || file=./${_q:-.} 14 | 15 | # Fallback to package READMEs. 16 | # False positive, intended behavior. 17 | # shellcheck disable=2046 18 | [ "$file" ] || { 19 | set -f 20 | set +f -- $(kiss s "${_q##*/}") 21 | file=${1:+"$1/README"} 22 | } 23 | 24 | # Fallback to search (allows 'kiss help firefox' to work). 25 | # False positive, intended behavior. 26 | # shellcheck disable=2046 27 | [ "$file" ] || { 28 | set -f 29 | set +f -- $(find . -name "${_q##*/}.txt") 30 | file=$1 31 | } 32 | 33 | : "${file:=404.txt}" 34 | 35 | cat </dev/null 20 | 21 | # Disable this warning as globbing is disabled and word splitting 22 | # is intentional. This grabs the location of the package's files. 23 | # shellcheck disable=2046 24 | { 25 | # Generate a list of repositories in which the package 26 | # exists. Then 'cd' to the first found directory to do a 27 | # comparison. 28 | set -- $(kiss search "${PWD##*/}"); cd "$1" 29 | 30 | # Error if the package exists nowhere but the current 31 | # directory and this script would create a broken symlink. 32 | [ -z "$2" ] && [ "$PWD" = "$oPWD" ] && { 33 | printf 'error: cannot symlink file to itself\n' 34 | exit 1 35 | } 36 | 37 | # If the first repository in '$KISS_PATH' is the current 38 | # directory, use the second repository in the list. 39 | [ "$PWD" = "$oPWD" ] && shift 40 | 41 | # Finally, make the link to the file in whatever repository 42 | # it was found in. 43 | ln -sf "$1/$file" "$oPWD/$file" 44 | } 45 | 46 | printf 'linked %s to %s\n' "$file" "$1" 47 | -------------------------------------------------------------------------------- /contrib/kiss-maintainer: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ef 2 | # Find the maintainer of a package 3 | 4 | # Use the current directory as the package name if no package is given. 5 | [ "$1" ] || { 6 | export KISS_PATH="${PWD%/*}:$KISS_PATH" 7 | set -- "${PWD##*/}" 8 | } 9 | 10 | kiss search "$@" | sort -u | while read -r repo; do cd "$repo" 11 | m=$(git log -1 --format='%an <%ae>' -- version 2>/dev/null) ||: 12 | 13 | [ "$m" ] || continue 14 | 15 | printf '=> %s\n%s\n' "$PWD" "$m" 16 | done 17 | -------------------------------------------------------------------------------- /contrib/kiss-manifest: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ef 2 | # Display all files owned by a package 3 | 4 | pkg=${1:-"${PWD##*/}"} 5 | 6 | kiss list "$pkg" >/dev/null || { 7 | printf 'usage: kiss-manifest [pkg]\n' >&2 8 | exit 1 9 | } 10 | 11 | cat "$KISS_ROOT/var/db/kiss/installed/$pkg/manifest" 2>/dev/null 12 | -------------------------------------------------------------------------------- /contrib/kiss-new: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Create a boilerplate package 3 | 4 | die() { 5 | printf '%s\n' "$*" 6 | exit 1 7 | } 8 | 9 | log() { 10 | printf '=> %s.\n' "$1" 11 | } 12 | 13 | [ "$1" ] || die "usage: kiss-new [name] [version] [source]" 14 | [ -d "$1" ] && die "error: Package $1 already exists" 15 | mkdir -p "$1" || die "error: Couldn't create directory in $PWD" 16 | cd "$1" || die "error: Couldn't enter directory $1/" 17 | 18 | log "Creating build file"; { 19 | printf '#!/bin/sh -e\n' > build 20 | chmod +x build 21 | } 22 | 23 | log "Creating version file with '${2%% *} 1'"; { 24 | printf '%s\n' "${2%% *} 1" > version 25 | } 26 | 27 | log "Creating sources file with '$3'"; { 28 | printf '%s\n' "$3" > sources 29 | } 30 | 31 | log "Package $1 created in $PWD" 32 | -------------------------------------------------------------------------------- /contrib/kiss-orphans: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # List orphaned packages 3 | 4 | n=' 5 | ' 6 | 7 | cd "$KISS_ROOT/var/db/kiss/installed" 8 | set -- * 9 | 10 | l=$n$( 11 | for pkg do shift 12 | set -- "$@" -e "$pkg" 13 | done 14 | 15 | # Get a list of non-orphans. 16 | grep -Fx "$@" -- */depends | 17 | 18 | { 19 | # Strip filename. 20 | sed s,.\*/depends:,, 21 | 22 | # Exclude packages which are not really orphans. 23 | printf '%s\n' baseinit baselayout busybox bzip2 e2fsprogs gcc \ 24 | git grub kiss make musl 25 | } | 26 | 27 | # Remove duplicates. 28 | sort -u 29 | )$n 30 | 31 | # Generate the list of orphans by finding the inverse of the non-orphan list. 32 | for pkg do shift 33 | case $l in (*"$n$pkg$n"*) 34 | continue 35 | esac 36 | 37 | set -- "$@" "$pkg" 38 | done 39 | 40 | printf '%s\n' "$@" 41 | 42 | -------------------------------------------------------------------------------- /contrib/kiss-outdated: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Check repository for outdated packages 3 | 4 | die() { 5 | printf '%s\n' "$*" >&2 6 | exit 1 7 | } 8 | 9 | mkcd() { 10 | mkdir -p "$1" && cd "$1" 11 | } 12 | 13 | repology_name() { 14 | # Fix any known naming inconsistences between packages and Repology. 15 | remote=$( 16 | # Strip unrelated suffixes. 17 | remote=${1%%-bin} 18 | remote=${remote%%-git} 19 | 20 | # Remote names are all lowercase. 21 | tr '[:upper:]' '[:lower:]' < manpages 201 | # man-pages-posix -> man-pages-posix 202 | remote=manpages 203 | ;; 204 | 205 | netsurf-fb) 206 | remote=netsurf 207 | ;; 208 | 209 | noto-emoji) 210 | remote=fonts:noto-emoji 211 | ;; 212 | 213 | nq) 214 | remote=nq-queue-utilities 215 | ;; 216 | 217 | openjpeg2) 218 | # TODO [community]: Rename package? 219 | remote=openjpeg 220 | ;; 221 | 222 | osh) 223 | remote=oil-shell 224 | ;; 225 | 226 | pass) 227 | remote=password-store 228 | ;; 229 | 230 | pinentry-dmenu) 231 | remote=pinentry-dmenu-cemkeylan 232 | ;; 233 | 234 | pybind11) 235 | remote=python:pybind11 236 | ;; 237 | 238 | pyqt5) 239 | # TODO [community]: Rename package? 240 | remote=python-qt 241 | ;; 242 | 243 | python-*) 244 | remote=python:${remote#*-} 245 | ;; 246 | 247 | python2) 248 | remote=python 249 | ;; 250 | 251 | qt5*) 252 | remote=qt 253 | ;; 254 | 255 | rage) 256 | remote=rage-encryption 257 | ;; 258 | 259 | samurai) 260 | remote=samurai-build-tool 261 | ;; 262 | 263 | sane) 264 | remote=sane-backends 265 | ;; 266 | 267 | spleen-font) 268 | remote=fonts:spleen 269 | ;; 270 | 271 | sshfs) 272 | remote=fusefs:sshfs 273 | ;; 274 | 275 | slides) 276 | remote=slides-presentation-tool 277 | ;; 278 | 279 | surf) 280 | remote=surf-browser 281 | ;; 282 | 283 | st) 284 | remote=st-term 285 | ;; 286 | 287 | sway-no-seat | sway-tiny) 288 | remote=sway 289 | ;; 290 | 291 | tea) 292 | remote=gitea-tea 293 | ;; 294 | 295 | terminus-font) 296 | remote=fonts:terminus 297 | ;; 298 | 299 | tiv) 300 | remote=tiv-terminalimageviewer 301 | ;; 302 | 303 | tree) 304 | remote=tree-steve-baker 305 | ;; 306 | 307 | ttf-croscore) 308 | remote=fonts:croscore 309 | ;; 310 | 311 | ttf-font-awesome) 312 | remote=font-awesome6 313 | ;; 314 | 315 | unifont) 316 | remote=fonts:unifont 317 | ;; 318 | 319 | vis) 320 | remote=vis-editor 321 | ;; 322 | 323 | webkit2gtk) 324 | # TODO [community]: Rename package? 325 | remote=webkitgtk 326 | ;; 327 | 328 | xf86-*) 329 | remote=xdrv:${remote##*-} 330 | ;; 331 | 332 | xmlsec1) 333 | # TODO [community]: Rename package? 334 | remote=xmlsec 335 | ;; 336 | esac 337 | } 338 | 339 | repology_version() { 340 | [ -f "$1.svg" ] || return 1 341 | read -r remote_ver < "$1.svg" || : 342 | remote_ver=${remote_ver%*} 343 | remote_ver=${remote_ver##*>} 344 | } 345 | 346 | repo_version() { 347 | read -r ver _ 2>/dev/null < "$2/version" || { 348 | printf '%-30s local version not found\n' "$1" >&2 349 | return 1 350 | } 351 | 352 | [ "$ver" != git ] 353 | } 354 | 355 | get_outdated() { 356 | repo=${PWD%%/} 357 | printf '\n[Checking Repology for outdated packages in %s]\n\n' "$repo" >&2 358 | 359 | for pkg in */; do 360 | pkg=${pkg%%/} 361 | repology_name "${pkg##*/}" 362 | 363 | [ "$remote" = - ] || 364 | set -- "$@" -z "$remote.svg" \ 365 | "https://repology.org/badge/latest-versions/$remote.svg" 366 | done 367 | 368 | mkcd "$tmp/${repo##*/}" 369 | 370 | curl -SsZA 'kiss-outdated' --parallel-max 16 --remote-name-all "$@" || 371 | die 'fatal: network error' 372 | 373 | for _pkg in "$OLDPWD"/*/; do 374 | pkg=${_pkg%%/} 375 | pkg=${pkg##*/} 376 | 377 | repo_version "$pkg" "$_pkg" || continue 378 | repology_name "$pkg" 379 | repology_version "$remote" || continue 380 | 381 | case $remote_ver in 382 | *", $ver"* | *"$ver,"* | "$ver" | '') continue ;; 383 | -) printf '%-30s no remote version found\n' "$pkg" >&2; continue ;; 384 | esac 385 | 386 | printf '%-30s %s -> %s\n' "$pkg" "$ver" "$remote_ver" 387 | done 388 | } 389 | 390 | main() { 391 | set -e 392 | 393 | [ "$1" ] || set -- "$PWD" 394 | 395 | mkdir -p "${tmp:=${XDG_CACHE_HOME:-"$HOME/.cache"}/kiss/repology}" 396 | 397 | for repo do 398 | old_pwd=$PWD 399 | cd "$repo" 400 | get_outdated 401 | cd "$old_pwd" 402 | done 403 | } 404 | 405 | main "$@" 406 | -------------------------------------------------------------------------------- /contrib/kiss-owns: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # Check which package owns a file 3 | 4 | # Follow symlinks to any paths. 5 | case $1 in 6 | /*) 7 | cd -P "${KISS_ROOT:-/}${1%/*}" 8 | [ "$PWD" = / ] && KISS_ROOT= 9 | ;; 10 | 11 | */*) 12 | cd -P "${1%/*}" 13 | ;; 14 | 15 | *) 16 | cd -P . 17 | ;; 18 | esac 19 | 20 | [ -f "$PWD/${1##*/}" ] || { 21 | printf 'usage: kiss-owns [/path/to/file]\n' >&2 22 | exit 1 23 | } 24 | 25 | # Print the full path to the manifest file which contains 26 | # the match to our search. 27 | pkg_owns=$(grep -lFx \ 28 | "${PWD#"$KISS_ROOT"}/${1##*/}" \ 29 | "$KISS_ROOT/var/db/kiss/installed/"*/manifest) 30 | 31 | 32 | # Extract the package name from the path above. 33 | pkg_owns=${pkg_owns%/*} 34 | pkg_owns=${pkg_owns##*/} 35 | 36 | printf '%s\n' "$pkg_owns" 37 | -------------------------------------------------------------------------------- /contrib/kiss-repodepends: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ef 2 | # Display a package's dependencies in repositories 3 | 4 | pkg=${1:-"${PWD##*/}"} 5 | 6 | kiss search "$pkg" >/dev/null || { 7 | printf 'usage: kiss-depends [pkg]\n' >&2 8 | exit 1 9 | } 10 | 11 | kiss search "$pkg" | while read -r pkgpath; do 12 | printf '=> %s\n' "$pkgpath" 13 | while read -r dep mak || [ "$dep" ]; do 14 | printf '%s%s\n' "$dep" "${mak:+ "$mak"}" 15 | done 2>/dev/null < "$pkgpath/depends" 16 | done 17 | -------------------------------------------------------------------------------- /contrib/kiss-reporevdepends: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Display packages in all repositories which depend on a package 3 | 4 | [ "$1" ] || set -- "${PWD##*/}" 5 | 6 | suffix () { 7 | case "$1" in *"$2") return 0; esac; return 1 8 | } 9 | 10 | IFS=: 11 | for repo in $KISS_PATH; do 12 | # remove trailing slashes 13 | while suffix "$repo" /; do repo="${repo%/}"; done 14 | gitdir="$(git -C "$repo" rev-parse --show-toplevel 2>/dev/null || echo "$repo")" 15 | case "$gitdir" in "$repo") unset prefix ;; *) prefix="${repo##*/}/" ;; esac 16 | 17 | cd "$gitdir/.." || continue 18 | grep -E "^$1([[:space:]]|$)" -- "${gitdir##*/}/$prefix"*/depends 2>/dev/null ||: 19 | done 20 | -------------------------------------------------------------------------------- /contrib/kiss-revdepends: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | # Display packages which depend on package 3 | 4 | [ "$1" ] || set -- "${PWD##*/}" 5 | 6 | cd "$KISS_ROOT/var/db/kiss/installed" 7 | 8 | grep -E "^$1( |$)" -- */depends 9 | 10 | -------------------------------------------------------------------------------- /contrib/kiss-size: -------------------------------------------------------------------------------- 1 | #!/bin/sh -ef 2 | # Show the size on disk for a package 3 | 4 | get_size() { 5 | # Naive function to convert bytes to human readable 6 | # sizes (MB, KB, etc). This is probably wrong in places 7 | # though we can fix this over time. It's a start. 8 | case ${#1} in 9 | [0-3]) hum=$(($1))KB ;; 10 | [4-6]) hum=$(($1 / 1024))MB ;; 11 | [7-9]) hum=$(($1 / 1024 / 1024))GB ;; 12 | *) hum=$(($1)) ;; 13 | esac 14 | 15 | printf '%s\t%s\n' "$hum" "$2" 16 | } 17 | 18 | # Use the current directory as the package name if no package is given. 19 | [ "$1" ] || set -- "${PWD##*/}" 20 | 21 | # Ignore shellcheck as we want the warning's behavior. 22 | # shellcheck disable=2015 23 | kiss list "${1:-null}" >/dev/null || { 24 | printf 'usage: kiss-size [pkg]\n' 25 | exit 1 26 | } 27 | 28 | # Filter directories from manifest and leave only files. 29 | # Directories in the manifest end in a trailing '/'. 30 | # Send the file list to 'xargs' to run through 'du', 31 | # this prevents du from exiting due to too many arguments 32 | sed -e "s|^|$KISS_ROOT|" -e '/.*\/$/d' -e 's/[^[:alnum:]]/\\&/g' \ 33 | "$KISS_ROOT/var/db/kiss/installed/$1/manifest" \ 34 | | xargs du -sk -- 2>/dev/null | 35 | 36 | # Iterate over each line and convert the byte output to human 37 | # readable (MB, KB, GB, etc). 38 | while read -r size file || { 39 | get_size "$tot" total >&2 40 | break 41 | } do 42 | get_size "$size" "$file" 43 | tot=$((tot + size)) 44 | done 45 | -------------------------------------------------------------------------------- /kiss: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # shellcheck source=/dev/null 3 | # 4 | # Simple package manager written in POSIX shell for https://kisslinux.github.io 5 | # 6 | # The MIT License (MIT) 7 | # 8 | # Copyright (c) 2019-2021 Dylan Araps 9 | # 10 | # Permission is hereby granted, free of charge, to any person obtaining a copy 11 | # of this software and associated documentation files (the "Software"), to deal 12 | # in the Software without restriction, including without limitation the rights 13 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | # copies of the Software, and to permit persons to whom the Software is 15 | # furnished to do so, subject to the following conditions: 16 | # 17 | # The above copyright notice and this permission notice shall be included in all 18 | # copies or substantial portions of the Software. 19 | # 20 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 21 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 26 | # SOFTWARE. 27 | 28 | log() { 29 | printf '%b%s %b%s%b %s\n' \ 30 | "$c1" "${3:-->}" "${c3}${2:+$c2}" "$1" "$c3" "$2" >&2 31 | } 32 | 33 | war() { 34 | log "$1" "$2" "${3:-WARNING}" 35 | } 36 | 37 | die() { 38 | log "$1" "$2" "${3:-ERROR}" 39 | exit 1 40 | } 41 | 42 | run() { 43 | # Print the command, then run it. 44 | printf '%s\n' "$*" 45 | "$@" 46 | } 47 | 48 | contains() { 49 | # Check if a "string list" contains a word. 50 | case " $1 " in *" $2 "*) return 0; esac; return 1 51 | } 52 | 53 | equ() { 54 | # Check if a string is equal to enother. 55 | # This replaces '[ "$var" = str ]' and '[ "$var" != str ]'. 56 | case $1 in "$2") return 0 ;; *) return 1; esac 57 | } 58 | 59 | ok() { 60 | # Check if a string is non-null. 61 | # This replaces '[ "$var" ]', '[ -n "$var" ]'. 62 | case $1 in '') return 1 ;; *) return 0; esac 63 | } 64 | 65 | null() { 66 | # Check if a string is non-null. 67 | # This replaces '[ -z "$var" ]'. 68 | case $1 in '') return 0 ;; *) return 1; esac 69 | } 70 | 71 | tmp_file() { 72 | # Create a uniquely named temporary file and store its absolute path 73 | # in a variable (_tmp_file). 74 | # 75 | # To prevent subshell usage and to handle cases where multiple files 76 | # are needed, this saves the last two temporary files to variables 77 | # for access by the caller (allowing 3 files at once). 78 | _tmp_file_pre_pre=$_tmp_file_pre 79 | _tmp_file_pre=$_tmp_file 80 | _tmp_file=$tmp_dir/$1-$2 81 | 82 | : > "$_tmp_file" || die "$1" "Failed to create temporary file" 83 | } 84 | 85 | tmp_file_copy() { 86 | # Create a uniquely named temporary file and make a duplicate of 87 | # the file in '$3' if it exists. 88 | tmp_file "$1" "$2" 89 | 90 | ! [ -f "$3" ] || cp -f "$3" "$_tmp_file" 91 | } 92 | 93 | prompt() { 94 | null "$1" || log "$1" 95 | 96 | log "Continue?: Press Enter to continue or Ctrl+C to abort" 97 | equ "$KISS_PROMPT" 0 || dd bs=1 count=1 >/dev/null 2>&1 || exit 1 98 | } 99 | 100 | mkcd() { 101 | mkdir -p "$@" && cd "$1" 102 | } 103 | 104 | fnr() { 105 | # Replace all occurrences of substrings with substrings. This 106 | # function takes pairs of arguments iterating two at a time 107 | # until everything has been replaced. 108 | _fnr=$1 109 | shift 1 110 | 111 | while :; do case $_fnr-$# in 112 | *"$1"*) _fnr=${_fnr%"$1"*}${2}${_fnr##*"$1"} ;; 113 | *-2) break ;; 114 | *) shift 2 115 | esac done 116 | } 117 | 118 | am_owner() { 119 | # Figure out if we need to change users to operate on 120 | # a given file or directory. 121 | inf=$(ls -ld "$1") || 122 | die "Failed to file information for '$1'" 123 | 124 | # Split the ls output into fields. 125 | read -r _ _ user _ </dev/null; then 155 | _parent=$PWD 156 | cd "$OLDPWD" 157 | else 158 | _parent=${_rpath%/*} 159 | fi 160 | 161 | _rpath=${_parent#"$KISS_ROOT"}/${_rpath##*/} 162 | } 163 | 164 | run_hook() { 165 | # Run all hooks in KISS_HOOK (a colon separated 166 | # list of absolute file paths). 167 | IFS=: 168 | 169 | for hook in ${KISS_HOOK:-}; do case $hook in *?*) 170 | "$hook" "$@" || die "$1 hook failed: '$hook'" 171 | esac done 172 | 173 | unset IFS 174 | } 175 | 176 | run_hook_pkg() { 177 | # Run a hook from the package's database files. 178 | hook_path="$sys_db/$2/$1" 179 | if [ -x "$hook_path" ]; then 180 | log "$2" "Running $1 hook" 181 | 182 | user=root 183 | 184 | set -- 185 | equ "$LOGNAME" "$user" || set -- as_user 186 | 187 | "$@" env KISS_ROOT='' \ 188 | chroot "${KISS_ROOT:-/}" "${hook_path#"$KISS_ROOT"}" 189 | 190 | elif [ -f "$hook_path" ]; then 191 | war "$2" "skipping $1 hook: not executable" 192 | fi 193 | } 194 | 195 | compress() { 196 | case $KISS_COMPRESS in 197 | bz2) bzip2 -c ;; 198 | gz) gzip -c ;; 199 | lz) lzip -c ;; 200 | lzma) lzma -cT0 ;; 201 | xz) xz -cT0 ;; 202 | zst) zstd -cT0 ;; 203 | esac 204 | } 205 | 206 | decompress() { 207 | case $1 in 208 | *.tar) cat ;; 209 | *.tbz|*.bz2) bzip2 -dc ;; 210 | *.lz) lzip -dc ;; 211 | *.tgz|*.gz) gzip -dc ;; 212 | *.lzma) lzma -dcT0 ;; 213 | *.xz|*.txz) xz -dcT0 ;; 214 | *.zst) zstd -dcT0 ;; 215 | esac < "$1" 216 | } 217 | 218 | b3() { 219 | # Higher level blake3 function which filters out non-existent 220 | # files (and also directories). 221 | for f do shift 222 | [ -d "$f" ] || [ ! -e "$f" ] || set -- "$@" "$f" 223 | done 224 | 225 | _b3 "$@" 226 | } 227 | 228 | _b3() { 229 | unset hash 230 | 231 | # Skip generation if no arguments. 232 | ! equ "$#" 0 || return 0 233 | 234 | IFS=$newline 235 | 236 | # Generate checksums for all input files. This is a single 237 | # call to the utility rather than one per file. 238 | # 239 | # The length of the checksum is set to 33 bytes to 240 | # differentiate it from sha256 checksums. 241 | _hash=$("$cmd_b3" -l 33 "$@") || die "Failed to generate checksums" 242 | 243 | # Strip the filename from each element. 244 | # ' ?' -> '' 245 | for sum in $_hash; do 246 | hash=$hash${hash:+"$newline"}${sum%% *} 247 | done 248 | 249 | printf '%s\n' "$hash" 250 | unset IFS 251 | } 252 | 253 | sh256() { 254 | # Higher level sh256 function which filters out non-existent 255 | # files (and also directories). 256 | for f do shift 257 | [ -d "$f" ] || [ ! -e "$f" ] || set -- "$@" "$f" 258 | done 259 | 260 | _sh256 "$@" 261 | } 262 | 263 | _sh256() { 264 | # There's no standard utility to generate sha256 checksums. 265 | # This is a simple wrapper around sha256sum, sha256, shasum, 266 | # openssl, digest, ... which will use whatever is available. 267 | # 268 | # All utilities must match 'sha256sum' output. 269 | # 270 | # Example: ' ' 271 | unset hash 272 | 273 | # Skip generation if no arguments. 274 | ! equ "$#" 0 || return 0 275 | 276 | # Set the arguments based on found sha256 utility. 277 | case ${cmd_sha##*/} in 278 | openssl) set -- dgst -sha256 -r "$@" ;; 279 | sha256) set -- -r "$@" ;; 280 | shasum) set -- -a 256 "$@" ;; 281 | digest) set -- -a sha256 "$@" ;; 282 | esac 283 | 284 | IFS=$newline 285 | 286 | # Generate checksums for all input files. This is a single 287 | # call to the utility rather than one per file. 288 | _hash=$("$cmd_sha" "$@") || die "Failed to generate checksums" 289 | 290 | # Strip the filename from each element. 291 | # ' ?' -> '' 292 | for sum in $_hash; do 293 | hash=$hash${hash:+"$newline"}${sum%% *} 294 | done 295 | 296 | printf '%s\n' "$hash" 297 | unset IFS 298 | } 299 | 300 | pkg_find_version() { 301 | ver_pre=$repo_ver 302 | rel_pre=$repo_rel 303 | 304 | pkg_find "$@" 305 | 306 | read -r repo_ver repo_rel 2>/dev/null < "$repo_dir/version" || 307 | die "$1" "Failed to read version file ($repo_dir/version)" 308 | 309 | ok "$repo_rel" || 310 | die "$1" "Release field not found in version file" 311 | 312 | # This belongs somewhere else, for now it can live here. 313 | [ -x "$repo_dir/build" ] || 314 | die "$pkg" "Build file not found or not executable" 315 | } 316 | 317 | pkg_find() { 318 | _pkg_find "$@" || die "'$1' not found" 319 | } 320 | 321 | _pkg_find() { 322 | # Figure out which repository a package belongs to by searching for 323 | # directories matching the package name in $KISS_PATH/*. 324 | set -- "$1" "$2" "$3" "${4:-"$KISS_PATH"}" 325 | IFS=: 326 | 327 | # Iterate over KISS_PATH, grabbing all directories which match the query. 328 | # Intentional. 329 | # shellcheck disable=2086 330 | for _find_path in $4 "${3:-$sys_db}"; do set +f 331 | ok "$_find_path" || continue 332 | for _find_pkg in "$_find_path/"$1; do 333 | test "${3:--d}" "$_find_pkg" && set -f -- "$@" "$_find_pkg" 334 | done 335 | done 336 | 337 | unset IFS 338 | 339 | # Show all search results if called from 'kiss search', else store the 340 | # values in variables. If there are 4 arguments, no package has been found. 341 | case $2-$# in 342 | *-4) return 1 ;; 343 | -*) repo_dir=$5; repo_name=${5##*/} ;; 344 | *) shift 4; printf '%s\n' "$@" 345 | esac 346 | } 347 | 348 | pkg_list_version() { 349 | # List installed packages. As the format is files and directories, this 350 | # just involves a simple for loop and file read. 351 | 352 | # Optional arguments can be passed to check for specific packages. If no 353 | # arguments are passed, list all. 354 | ok "$1" || { set +f; set -f -- "$sys_db"/*; } 355 | 356 | # Loop over each package and print its name and version. 357 | for _list_pkg do 358 | pkg_find_version "${_list_pkg##*/}" "" "" "$sys_db" 359 | 360 | printf '%s\n' "$repo_name $repo_ver-$repo_rel" 361 | done 362 | } 363 | 364 | pkg_cache() { 365 | # Find the tarball of a package using a glob. Use the user's set compression 366 | # method if found or first match of the below glob. 367 | pkg_find_version "$1" 368 | 369 | set +f -- "$bin_dir/$1@$repo_ver-$repo_rel.tar." 370 | set -f -- "$1$KISS_COMPRESS" "$1"* 371 | 372 | tar_file=$1 373 | 374 | # If the first match does not exist, use the second. If neither exist, 375 | # this function returns 1 and the caller handles the error. 376 | [ -f "$1" ] || { tar_file=$2; [ -f "$2" ]; } 377 | } 378 | 379 | pkg_source_resolve() { 380 | # Given a line of input from the sources file, return an absolute 381 | # path to the source if it already exists, error if not. 382 | unset _res _des 383 | 384 | ok "${2##\#*}" || return 0 385 | 386 | set -- "$1" "$2" "${3%"${3##*[!/]}"}" "$4" 387 | 388 | # Git repository. 389 | if null "${2##git+*}"; then 390 | _res=$2 391 | _des=$src_dir/$1/${3:+"$3/"}${2##*/} 392 | _des=${_des%[@#]*}/ 393 | 394 | # Remote source. 395 | elif null "${2##*://*}"; then 396 | # Remote source (cached). 397 | # Only check if we detect a remote source to avoid 398 | # masking local paths like 'files/test' as cached 399 | # if '$src_dir/$1/test' is present 400 | if [ -f "$src_dir/$1/${3:+"$3/"}${2##*/}" ]; then 401 | _res=$src_dir/$1/${3:+"$3/"}${2##*/} 402 | else 403 | _res=url+$2 404 | _des=$src_dir/$1/${3:+"$3/"}${2##*/} 405 | fi 406 | 407 | # Local relative dir. 408 | elif [ -d "$repo_dir/$2" ]; then 409 | _res=$repo_dir/$2/. 410 | 411 | # Local absolute dir. 412 | elif [ -d "/${2##/}" ]; then 413 | _res=/${2##/}/. 414 | 415 | # Local relative file. 416 | elif [ -f "$repo_dir/$2" ]; then 417 | _res=$repo_dir/$2 418 | 419 | # Local absolute file. 420 | elif [ -f "/${2##/}" ]; then 421 | _res=/${2##/} 422 | 423 | else 424 | die "$1" "No local file '$2'" 425 | fi 426 | 427 | ok "$4" || printf 'found %s\n' "$_res" 428 | } 429 | 430 | pkg_source() { 431 | # Download any remote package sources. The existence of local files is 432 | # also checked. 433 | pkg_find_version "$1" 434 | 435 | # Support packages without sources. Simply do nothing. 436 | [ -f "$repo_dir/sources" ] || return 0 437 | 438 | log "$1" "Reading sources" 439 | 440 | while read -r src dest || ok "$src"; do 441 | pkg_source_resolve "$1" "$src" "$dest" "$2" 442 | 443 | # arg1: pre-source 444 | # arg2: package name 445 | # arg3: source 446 | run_hook pre-source "$1" "$src" 447 | 448 | # '$2' is set when this function is called from 'kiss c' and it is used 449 | # here to skip calling the Git code. 450 | case $2$_res in "$2url+"*|git+*) 451 | mkcd "${_des%/*}" 452 | "pkg_source_${_res%%+*}" "$_des" "${_res##"${_res%%+*}+"}" 453 | esac 454 | 455 | # arg1: post-source 456 | # arg2: package name 457 | # arg3: source 458 | # arg4: destination 459 | run_hook post-source "$1" "$src" "${_des:-"$_res"}" 460 | done < "$repo_dir/sources" 461 | } 462 | 463 | pkg_source_url() { 464 | log "$repo_name" "Downloading $2" 465 | 466 | # Download to a temporary file and only move it to the destination if 467 | # everything completed successfully, to avoid leaving incomplete downloads 468 | # in the cache if it is interrupted. 469 | download_source=$2 470 | download_dest=$1 471 | tmp_file "${1##*/}" download 472 | shift 473 | 474 | set -- "$_tmp_file" "$@" 475 | rm -f "$_tmp_file" 476 | 477 | # Set the arguments based on found download utility. 478 | case ${cmd_get##*/} in 479 | aria2c) set -- -d / -o "$@" ;; 480 | axel) set -- -o "$@" ;; 481 | curl) set -- -fLo "$@" ;; 482 | wget|wget2) set -- -O "$@" ;; 483 | esac 484 | 485 | "$cmd_get" "$@" || { 486 | rm -f "$_tmp_file" 487 | die "$repo_name" "Failed to download $download_source" 488 | } 489 | 490 | mv "$_tmp_file" "$download_dest" 491 | } 492 | 493 | pkg_source_git() { 494 | com=${2##*[@#]} 495 | com=${com#"${2%[#@]*}"} 496 | 497 | log "$repo_name" "Checking out ${com:-FETCH_HEAD}" 498 | 499 | [ -d .git ] || git init 500 | 501 | git remote set-url origin "${2%[#@]*}" 2>/dev/null || 502 | git remote add origin "${2%[#@]*}" 503 | 504 | git fetch --depth=1 origin "$com" 505 | git reset --hard FETCH_HEAD 506 | } 507 | 508 | pkg_source_tar() { 509 | # This is a portable shell implementation of GNU tar's 510 | # '--strip-components 1'. Use of this function denotes a 511 | # performance penalty. 512 | tmp_file "$repo_name" tarball 513 | tmp_file "$repo_name" tarball-manifest 514 | 515 | decompress "$1" > "$_tmp_file_pre" || 516 | die "$repo_name" "Failed to decompress $1" 517 | 518 | tar xf "$_tmp_file_pre" || 519 | die "$repo_name" "Failed to extract $1" 520 | 521 | # The sort command filters out all duplicate top-level 522 | # directories from the tarball's manifest. This is an optimization 523 | # as we avoid looping (4000 times for Python(!)). 524 | tar tf "$_tmp_file_pre" | sort -ut / -k1,1 > "$_tmp_file" || 525 | die "$repo_name" "Failed to extract manifest" 526 | 527 | # Iterate over all directories in the first level of the 528 | # tarball's manifest. Each directory is moved up a level. 529 | while IFS=/ read -r dir _; do case ${dir#.} in *?*) 530 | # Skip entries which aren't directories. 531 | [ -d "$dir" ] || continue 532 | 533 | # Move the parent directory to prevent naming conflicts 534 | # with the to-be-moved children. 535 | mv -f "$dir" "$KISS_PID-$dir" 536 | 537 | # Move all children up a directory level. If the mv command 538 | # fails, fallback to copying the remainder of the files. 539 | # 540 | # We can't use '-exec {} +' with any arguments between 541 | # the '{}' and '+' as this is not POSIX. We must also 542 | # use '$0' and '$@' to reference all arguments. 543 | find "$KISS_PID-$dir/." ! -name . -prune \ 544 | -exec sh -c 'mv -f "$0" "$@" .' {} + 2>/dev/null || 545 | 546 | find "$KISS_PID-$dir/." ! -name . -prune \ 547 | -exec sh -c 'cp -fRPp "$0" "$@" .' {} + 548 | 549 | # Remove the directory now that all files have been 550 | # transferred out of it. This can't be a simple 'rmdir' 551 | # as we may leave files in here if any were copied. 552 | rm -rf "$KISS_PID-$dir" 553 | esac done < "$_tmp_file" 554 | 555 | # Remove the tarball now that we are done with it. 556 | rm -f "$_tmp_file_pre" 557 | } 558 | 559 | pkg_extract() { 560 | # Extract all source archives to the build directory and copy over any 561 | # local repository files. 562 | # 563 | # NOTE: repo_dir comes from caller. 564 | log "$1" "Extracting sources" 565 | 566 | # arg1: pre-extract 567 | # arg2: package name 568 | # arg3: path to DESTDIR 569 | run_hook pre-extract "$pkg" "$pkg_dir/$pkg" 570 | 571 | while read -r src dest || ok "$src"; do 572 | pkg_source_resolve "$1" "$src" "$dest" >/dev/null 573 | 574 | # Create the source's directories if not null. 575 | null "$_res" || mkcd "$mak_dir/$1/$dest" 576 | 577 | case $_res in 578 | git+*) 579 | cp -PRf "$_des/." . 580 | ;; 581 | 582 | *.tar|*.tar.??|*.tar.???|*.tar.????|*.t?z) 583 | pkg_source_tar "$_res" 584 | ;; 585 | 586 | *?*) 587 | cp -PRf "$_res" . 588 | ;; 589 | esac 590 | done < "$repo_dir/sources" || die "$1" "Failed to extract $_res" 591 | } 592 | 593 | pkg_depends() { 594 | # Resolve all dependencies and generate an ordered list. The deepest 595 | # dependencies are listed first and then the parents in reverse order. 596 | ! contains "$deps" "$1" || return 0 597 | 598 | # Filter out non-explicit, already installed packages. 599 | null "$3" || ok "$2" || contains "$explicit" "$1" || 600 | ! [ -d "$sys_db/$1" ] || return 0 601 | 602 | # Detect circular dependencies and bail out. 603 | # Looks for multiple repeating patterns of (dep dep_parent) (5 is max). 604 | case " $4 " in 605 | *" ${4##* } "*" $1 "\ 606 | *" ${4##* } "*" $1 "\ 607 | *" ${4##* } "*" $1 "\ 608 | *" ${4##* } "*" $1 "\ 609 | *" ${4##* } "*" $1 "*) 610 | die "Circular dependency detected $1 <> ${4##* }" 611 | esac 612 | 613 | # Packages which exist and have depends. 614 | ! _pkg_find "$1" || ! [ -e "$repo_dir/depends" ] || 615 | 616 | # Recurse through the dependencies of the child packages. 617 | while read -r dep dep_type || ok "$dep"; do 618 | ! ok "${dep##\#*}" || pkg_depends "$dep" '' "$3" "$4 $1" "$dep_type" 619 | done < "$repo_dir/depends" || : 620 | 621 | # Add parent to dependencies list. 622 | if ! equ "$2" expl || { equ "$5" make && ! pkg_cache "$1"; }; then 623 | deps="$deps $1" 624 | fi 625 | } 626 | 627 | pkg_order() { 628 | # Order a list of packages based on dependence and take into account 629 | # pre-built tarballs if this is to be called from 'kiss i'. 630 | unset order redro deps 631 | 632 | for pkg do case $pkg in 633 | /*@*.tar.*) deps="$deps $pkg" ;; 634 | *@*.tar.*) deps="$deps $ppwd/$pkg" ;; 635 | */*) die "Not a package' ($pkg)" ;; 636 | *) pkg_depends "$pkg" raw 637 | esac done 638 | 639 | # Filter the list, only keeping explicit packages. The purpose of these 640 | # two loops is to order the argument list based on dependence. 641 | for pkg in $deps; do case " $* " in *" $pkg "*|*" ${pkg##"$ppwd/"} "*) 642 | order="$order $pkg" 643 | redro="$pkg $redro" 644 | esac done 645 | 646 | unset deps 647 | } 648 | 649 | pkg_strip() { 650 | # Strip package binaries and libraries. This saves space on the system as 651 | # well as on the tarballs we ship for installation. 652 | [ -f "$mak_dir/$pkg/nostrip" ] || equ "$KISS_STRIP" 0 && return 653 | 654 | log "$1" "Stripping binaries and libraries" 655 | 656 | # Strip only files matching the below ELF types. This uses 'od' to print 657 | # the first 18 bytes of the file. This is the location of the ELF header 658 | # (up to the ELF type) and contains the type information we need. 659 | # 660 | # Static libraries (.a) are in reality AR archives which contain ELF 661 | # objects. We simply read from the same 18 bytes and assume that the AR 662 | # header equates to an archive containing objects (.o). 663 | # 664 | # Example ELF output ('003' is ELF type): 665 | # 0000000 177 E L F 002 001 001 \0 \0 \0 \0 \0 \0 \0 \0 \0 666 | # 0000020 003 \0 667 | # 0000022 668 | # 669 | # Example AR output (.a): 670 | # 0000000 ! < a r c h > \n / 671 | # 0000020 672 | # 0000022 673 | while read -r file; do [ -h "$pkg_dir/$1$file" ] || case $file in 674 | # Look only in these locations for files of interest (libraries, 675 | # programs, etc). This includes all subdirectories. Old behavior 676 | # would run od on all files (upwards of 4000 for Python). 677 | */sbin/?*[!/]|*/bin/?*[!/]|*/lib/?*[!/]|\ 678 | */lib??/?*[!/]|*/lib???/?*[!/]|*/lib????/?*[!/]) 679 | 680 | case $(od -A o -t c -N 18 "$pkg_dir/$1$file") in 681 | # REL (object files (.o), static libraries (.a)). 682 | *177*E*L*F*0000020\ 001\ *|*\!*\<*a*r*c*h*\>*) 683 | run strip -g -R .comment -R .note "$pkg_dir/$1$file" 684 | ;; 685 | 686 | # EXEC (binaries), DYN (shared libraries). 687 | # Shared libraries keep global symbols in a separate ELF section 688 | # called '.dynsym'. '--strip-all/-s' does not touch the dynamic 689 | # symbol entries which makes this safe to do. 690 | *177*E*L*F*0000020\ 00[23]\ *) 691 | run strip -s -R .comment -R .note "$pkg_dir/$1$file" 692 | ;; 693 | esac 694 | esac done < "$pkg_dir/$1/$pkg_db/$1/manifest" || : 695 | } 696 | 697 | should_ignore_rpath() { 698 | # Intentional, globbing disabled. 699 | # shellcheck disable=2086 700 | { IFS=:; set -- $1; unset IFS; } 701 | 702 | for path do 703 | # TODO maybe check if the RPATH is set to a redundant value 704 | # like /lib or /usr/lib ? 705 | # ORIGIN is relative - no need to ignore it. 706 | # Library rpath: [$ORIGIN/../lib] 707 | # shellcheck disable=2016 708 | case $path in '$ORIGIN'*|'${ORIGIN}'*) continue; esac 709 | # Non-relative path, should be ignored. 710 | return 0 711 | done 712 | 713 | return 1 714 | } 715 | 716 | pkg_fix_deps() { 717 | # Dynamically look for missing runtime dependencies by checking each 718 | # binary and library with 'ldd'. This catches any extra libraries and or 719 | # dependencies pulled in by the package's build suite. 720 | log "$1" "looking for dependencies (using ${cmd_elf##*/})" 721 | 722 | tmp_file_copy "$1" depends depends 723 | tmp_file "$1" depends-fixed 724 | 725 | set +f 726 | set -f -- "$sys_db/"*/manifest 727 | 728 | unset _fdep_seen 729 | 730 | # False positive (not a write). 731 | # shellcheck disable=2094 732 | while read -r _file; do [ -h "$_file" ] || case $_file in 733 | # Look only in these locations for files of interest (libraries, 734 | # programs, etc). This includes all subdirectories. Old behavior 735 | # would run ldd on all files (upwards of 4000 for Python). 736 | */sbin/?*[!/]|*/bin/?*[!/]|*/lib/?*[!/]|\ 737 | */lib??/?*[!/]|*/lib???/?*[!/]|*/lib????/?*[!/]) 738 | 739 | unset elf 740 | 741 | # Attempt to get information from readelf. If this fails (or we 742 | # are in ldd mode), do full ldd mode (which has the downside of 743 | # listing dependencies of dependencies (and so on)). 744 | case $cmd_elf in *readelf) 745 | elf=$("$cmd_elf" -d "$pkg_dir/$repo_name$_file" 2>/dev/null) ||: 746 | esac 747 | 748 | # RPATH/RUNPATH allows the binary to set a relative path for the 749 | # dynamic loader that might not be present on the rootfs at the time 750 | # of installation. So, ignoring it can cause the dependency detector to 751 | # wrongly add packages. The problem also exists in reverse when the 752 | # package links to a library bundled inside it and present in system paths 753 | # aswell, and a package update unbundles it - the package gets a dependency 754 | # on itself due to RPATH causing ldd to find the bundled lib. 755 | # Example: libnss3.so exists in /usr/lib at the time of installation 756 | # But the package links to libnss3 in /usr/lib/PKG/libnss3.so, which is 757 | # present only in the build dir. So, KISS wrongly adds the installed nss 758 | # as a dependency. 759 | # To avoid dealing with this, we just ignore any libraries with RPATH set. 760 | # A solution to this could be to just get the NEEDED libs and RPATH with 761 | # readelf and resolve the paths ourselves - with adhoc logic for RPATH and 762 | # the build dir. 763 | # Another solution could be to resolve the dependencies after installation, 764 | # but that can cause issues with reinstalling that tarball later on. 765 | # https://github.com/kiss-community/kiss/issues/64 766 | # https://github.com/kiss-community/repo/issues/97 767 | while read -r _ entry_type value; do 768 | # Technically RUNPATH is supposed to have a higher priority 769 | # than RPATH but a binary that has both RPATH and RUNPATH set, 770 | # each with unique values is probably broken... 771 | case $entry_type in '(RPATH)'|'(RUNPATH)') 772 | value=${value##*\[} 773 | value=${value%%\]*} 774 | 775 | should_ignore_rpath "$value" && continue 2 776 | # Found RPATH which shouldn't be ignored 777 | break 778 | esac 779 | done </dev/null) || continue 786 | ok "$elf" || elf=$ldd 787 | 788 | # Iterate over the output of readelf or ldd, extract file names, 789 | # resolve their paths and finally, figure out their owner. 790 | while read -r _ entry_type lib; do case $entry_type in '(NEEDED)'|'=>') 791 | # readelf: 0x0000 (NEEDED) Shared library: [libjson-c.so.5] 792 | lib=${lib##*\[} 793 | lib=${lib%%\]*} 794 | 795 | # Resolve library path. It is already resolved for ldd in the 'lib' field 796 | # ldd: libjson-c.so.5 => /lib/libjson-c.so.5 ... 797 | case $cmd_elf in *readelf) 798 | lib=${ldd#*" $lib => "} 799 | # Remove prefix, else the file will not be owned by any 800 | # package in the pkg_owner check below 801 | lib=${lib#"$pkg_dir/$repo_name"} 802 | esac 803 | 804 | lib=${lib%% *} 805 | 806 | # Skip files owned by libc, libc++ and POSIX. 807 | case ${lib##*/} in 808 | ld-* |\ 809 | lib[cm].so* |\ 810 | libc++.so* |\ 811 | libc++abi.so* |\ 812 | libcrypt.so* |\ 813 | libdl.so* |\ 814 | libgcc_s.so* |\ 815 | libmvec.so* |\ 816 | libpthread.so* |\ 817 | libresolv.so* |\ 818 | librt.so* |\ 819 | libstdc++.so* |\ 820 | libtrace.so* |\ 821 | libunwind.so* |\ 822 | libutil.so* |\ 823 | libxnet.so* |\ 824 | ldd) 825 | continue 826 | esac 827 | 828 | # Skip files we have seen before. 829 | case " $_fdep_seen " in 830 | *" $lib "*) continue ;; 831 | *) _fdep_seen="$_fdep_seen $lib" 832 | esac 833 | 834 | resolve_path "$lib" 835 | 836 | # Skip file if owned by current package 837 | ! pkg_owner -e "$_rpath" manifest || 838 | continue 839 | 840 | ! pkg_owner -e "$_rpath" "$@" || 841 | printf '%s\n' "$_owns" 842 | 843 | esac done < "$_tmp_file" 852 | 853 | # If the depends file was modified, show a diff and replace it. 854 | ! [ -s "$_tmp_file" ] || { 855 | diff -U 3 "$_tmp_file_pre" "$_tmp_file" 2>/dev/null || : 856 | 857 | # Replace the existing depends file if one exists, otherwise this 858 | # just moves the file to its final resting place. 859 | mv -f "$_tmp_file" depends 860 | 861 | # Generate a new manifest as we may be the creator of the depends 862 | # file. This could otherwise be implemented by inserting a line 863 | # at the correct place in the existing manifest. 864 | pkg_manifest "${PWD##*/}" "$pkg_dir" 865 | } 866 | } 867 | 868 | pkg_manifest() { 869 | # Generate the package's manifest file. This is a list of each file 870 | # and directory inside the package. The file is used when uninstalling 871 | # packages, checking for package conflicts and for general debugging. 872 | log "$1" "Generating manifest" 873 | 874 | tmp_file "$1" manifest 875 | 876 | # Create a list of all files and directories. Append '/' to the end of 877 | # directories so they can be easily filtered out later. Also filter out 878 | # all libtool .la files and charset.alias. 879 | { 880 | printf '%s\n' "$2/$1/$pkg_db/$1/manifest" 881 | 882 | ! [ -d "$2/$1/etc" ] || 883 | printf '%s\n' "$2/$1/$pkg_db/$1/etcsums" 884 | 885 | find "$2/$1" ! -path "$2/$1" -type d -exec printf '%s/\n' {} + \ 886 | -o \( ! -type d -a ! -name \*.la -a ! -name charset.alias \) -print 887 | 888 | # Sort the output in reverse. Directories appear after their contents. 889 | } | sort -ur > "$_tmp_file" 890 | 891 | # Remove the prefix from each line. 892 | while read -r file; do 893 | printf '%s\n' "${file#"$2/$1"}" 894 | done < "$_tmp_file" > "$2/$1/$pkg_db/$1/manifest" 895 | } 896 | 897 | pkg_manifest_validate() { 898 | # NOTE: _pkg comes from caller. 899 | log "$_pkg" "Checking if manifest valid" 900 | 901 | cnt=0 902 | 903 | while read -r line; do 904 | [ -e "$tar_dir/$_pkg$line" ] || [ -h "$tar_dir/$_pkg$line" ] || { 905 | printf '%s\n' "$line" 906 | : $((cnt += 1)) 907 | } 908 | done < "$pkg_db/$_pkg/manifest" 909 | 910 | equ "$cnt" 0 || 911 | die "$_pkg" "manifest contains $cnt non-existent files" 912 | } 913 | 914 | pkg_manifest_replace() { 915 | # Replace the matching line in the manifest with the desired replacement. 916 | # This used to be a 'sed' call which turned out to be a little 917 | # error-prone in some cases. This new method is a tad slower but ensures 918 | # we never wipe the file due to a command error. 919 | tmp_file "$1" "manifest-replace-${2##*/}" 920 | 921 | while read -r line; do 922 | ! equ "$line" "$2" || line=$3 923 | 924 | printf '%s\n' "$line" 925 | done < "$sys_db/$1/manifest" | sort -r > "$_tmp_file" 926 | 927 | mv -f "$_tmp_file" "$sys_db/$1/manifest" 928 | } 929 | 930 | pkg_etcsums() { 931 | # Generate checksums for each configuration file in the package's /etc/ 932 | # directory for use in "smart" handling of these files. 933 | log "$repo_name" "Generating etcsums" 934 | 935 | # Minor optimization - skip packages without /etc/. 936 | [ -d "$pkg_dir/$repo_name/etc" ] || return 0 937 | 938 | while read -r etc; do case $etc in /etc/*[!/]) 939 | etc="$pkg_dir/$repo_name/$etc" 940 | # Always use hash of /dev/null as the hash for symlinks as it's 941 | # possible that they can never be resolved/hashed 942 | # (eg. directory, non-existent path, ...) 943 | # So they leave missing lines in etcsums which breaks removal of all 944 | # other unmodified files in /etc aswell 945 | [ -h "$etc" ] && etc=/dev/null 946 | 947 | set -- "$@" "$etc" 948 | esac done < manifest 949 | 950 | b3 "$@" > etcsums 951 | } 952 | 953 | pkg_tar() { 954 | # Create a tarball from the built package's files. This tarball also 955 | # contains the package's database entry. 956 | # 957 | # NOTE: repo_ comes from caller. 958 | log "$1" "Creating tarball" 959 | 960 | _tar_file=$bin_dir/$1@$repo_ver-$repo_rel.tar.$KISS_COMPRESS 961 | 962 | # Use 'cd' to avoid needing tar's '-C' flag which may not be portable 963 | # across implementations. 964 | cd "$pkg_dir/$1" 965 | 966 | # Create a tarball from the contents of the built package. 967 | tar cf - . | compress > "$_tar_file" 968 | 969 | cd "$OLDPWD" 970 | 971 | log "$1" "Successfully created tarball" 972 | 973 | # arg1: post-package 974 | # arg2: package name 975 | # arg3: path to tarball 976 | run_hook post-package "$1" "$_tar_file" 977 | } 978 | 979 | pkg_build_all() { 980 | # Build packages and turn them into packaged tarballs. 981 | # Order the argument list and filter out duplicates. 982 | 983 | # Mark packages passed on the command-line explicit. 984 | # Also resolve dependencies for all explicit packages. 985 | for pkg do 986 | equ "$KISS_FORCE" 1 || pkg_depends "$pkg" expl filter 987 | explicit="$explicit $pkg " 988 | done 989 | 990 | # If this is an update, don't always build explicitly passsed packages 991 | # and instead install pre-built binaries if they exist. 992 | ok "$prefer_cache" || explicit_build=$explicit 993 | 994 | set -- 995 | 996 | # If an explicit package is a dependency of another explicit package, 997 | # remove it from the explicit list. 998 | for pkg in $explicit; do 999 | contains "$deps" "$pkg" || set -- "$@" "$pkg" 1000 | done 1001 | explicit_cnt=$# 1002 | explicit=$* 1003 | 1004 | log "Building: explicit: $*${deps:+, implicit: ${deps## }}" 1005 | 1006 | # Intentional, globbing disabled. 1007 | # shellcheck disable=2046,2086 1008 | set -- $deps "$@" 1009 | 1010 | # Ask for confirmation if extra packages need to be built. 1011 | equ "$#" "$explicit_cnt" || prompt 1012 | 1013 | log "Checking for pre-built dependencies" 1014 | 1015 | # Install any pre-built dependencies if they exist in the binary 1016 | # directory and are up to date. 1017 | for pkg in "$@"; do 1018 | if ! contains "$explicit_build" "$pkg" && pkg_cache "$pkg"; then 1019 | log "$pkg" "Found pre-built binary" 1020 | 1021 | # Intended behavior. 1022 | # shellcheck disable=2030,2031 1023 | (export KISS_FORCE=1; args i "$tar_file") 1024 | else 1025 | set -- "$@" "$pkg" 1026 | fi 1027 | 1028 | shift 1029 | done 1030 | 1031 | for pkg do 1032 | pkg_source "$pkg" 1033 | 1034 | ! [ -f "$repo_dir/sources" ] || pkg_verify "$pkg" 1035 | done 1036 | 1037 | # Finally build and create tarballs for all passed packages and 1038 | # dependencies. 1039 | for pkg do 1040 | log "$pkg" "Building package ($((_build_cur+=1))/$#)" 1041 | 1042 | pkg_find_version "$pkg" 1043 | 1044 | # arg1: queue-status 1045 | # arg2: package name 1046 | # arg3: number in queue 1047 | # arg4: total in queue 1048 | run_hook queue-status "$pkg" "$_build_cur" "$#" 1049 | 1050 | ! [ -f "$repo_dir/sources" ] || pkg_extract "$pkg" 1051 | 1052 | pkg_build "$pkg" 1053 | pkg_manifest "$pkg" "$pkg_dir" 1054 | pkg_strip "$pkg" 1055 | 1056 | cd "$pkg_dir/$pkg/$pkg_db/$pkg" 1057 | 1058 | pkg_fix_deps "$pkg" 1059 | pkg_etcsums 1060 | pkg_tar "$pkg" 1061 | 1062 | if equ "${prefer_cache:=0}" 1 || ! contains "$explicit" "$pkg"; then 1063 | log "$pkg" "Needed as a dependency or has an update, installing" 1064 | 1065 | # Intended behavior. 1066 | # shellcheck disable=2030,2031 1067 | (export KISS_FORCE=1; args i "$pkg") 1068 | fi 1069 | done 1070 | 1071 | # Intentional, globbing disabled. 1072 | # shellcheck disable=2046,2086 1073 | ! equ "${build_install:=1}" 1 || ! equ "${KISS_PROMPT:=1}" 1 || 1074 | ! prompt "Install built packages? [$explicit]" || (args i $explicit) 1075 | } 1076 | 1077 | pkg_build() { 1078 | # Install built packages to a directory under the package name to 1079 | # avoid collisions with other packages. 1080 | mkcd "$mak_dir/$1" "$pkg_dir/$1/$pkg_db" 1081 | 1082 | log "$1" "Starting build" 1083 | 1084 | # arg1: pre-build 1085 | # arg2: package name 1086 | # arg3: path to build directory 1087 | run_hook pre-build "$1" "$mak_dir/$1" 1088 | 1089 | # Attempt to create the log file early so any permissions errors are caught 1090 | # before the build starts. 'tee' is run in a pipe and POSIX shell has no 1091 | # pipe-fail causing confusing behavior when tee fails. 1092 | : > "$log_dir/$1-$time-$KISS_PID" 1093 | 1094 | # Call the build script, log the output to the terminal and to a file. 1095 | # There's no PIPEFAIL in POSIX shell so we must resort to tricks like kill. 1096 | { 1097 | # Give the script a modified environment. Define toolchain program 1098 | # environment variables assuming a generic environment by default. 1099 | # 1100 | # Define GOPATH to sane defaults as its use is mandatory for Go projects. 1101 | # Define KISS_ROOT as the sanitized value used internally by the package 1102 | # manager. This is safe to join with other paths. 1103 | AR="${AR:-ar}" \ 1104 | CC="${CC:-cc}" \ 1105 | CXX="${CXX:-c++}" \ 1106 | NM="${NM:-nm}" \ 1107 | RANLIB="${RANLIB:-ranlib}" \ 1108 | RUSTFLAGS="--remap-path-prefix=$PWD=. $RUSTFLAGS" \ 1109 | GOFLAGS="-trimpath -modcacherw $GOFLAGS" \ 1110 | GOPATH="$PWD/go" \ 1111 | KISS_ROOT="$KISS_ROOT" \ 1112 | \ 1113 | "$repo_dir/build" "$pkg_dir/$1" "$repo_ver" 2>&1 || { 1114 | log "$1" "Build failed" 1115 | log "$1" "Log stored to $log_dir/$1-$time-$KISS_PID" 1116 | 1117 | # arg1: build-fail 1118 | # arg2: package name 1119 | # arg3: path to build directory 1120 | (run_hook build-fail "$pkg" "$mak_dir/$1") || : 1121 | 1122 | pkg_clean 1123 | kill 0 1124 | } 1125 | } | tee "$log_dir/$1-$time-$KISS_PID" 1126 | 1127 | # Delete the log file if the build succeeded to prevent the directory 1128 | # from filling very quickly with useless logs. 1129 | equ "$KISS_KEEPLOG" 1 || rm -f "$log_dir/$1-$time-$KISS_PID" 1130 | 1131 | # Copy the repository files to the package directory. 1132 | cp -LRf "$repo_dir" "$pkg_dir/$1/$pkg_db/" 1133 | 1134 | log "$1" "Successfully built package" 1135 | 1136 | # arg1: post-build 1137 | # arg2: package name 1138 | # arg3: path to DESTDIR 1139 | run_hook post-build "$1" "$pkg_dir/$1" 1140 | } 1141 | 1142 | pkg_checksum() { 1143 | pkg_source "$1" c 1144 | 1145 | [ -f "$repo_dir/sources" ] || return 0 1146 | 1147 | pkg_checksum_gen 1148 | 1149 | if ok "$hash"; then 1150 | printf '%s\n' "$hash" > "$repo_dir/checksums" 1151 | log "$1" "Generated checksums" 1152 | 1153 | else 1154 | log "$1" "No sources needing checksums" 1155 | fi 1156 | } 1157 | 1158 | pkg_checksum_gen() { 1159 | # Generate checksums for packages. 1160 | # 1161 | # NOTE: repo_ comes from caller. 1162 | while read -r src dest || ok "$src"; do 1163 | pkg_source_resolve "$repo_name" "$src" "$dest" >/dev/null 1164 | 1165 | case ${_res##git+*} in */*[!.]) 1166 | set -- "$@" "$_res" 1167 | esac 1168 | done < "$repo_dir/sources" 1169 | 1170 | _b3 "$@" 1171 | } 1172 | 1173 | pkg_verify() { 1174 | # Verify all package checksums. This is achieved by generating a new set 1175 | # of checksums and then comparing those with the old set. 1176 | # 1177 | # NOTE: repo_dir comes from caller. 1178 | log "$repo_name" "Verifying sources" 1179 | 1180 | # Generate a new set of checksums to compare against. 1181 | pkg_checksum_gen >/dev/null 1182 | 1183 | # Intentional, globbing disabled. 1184 | # shellcheck disable=2038,2086 1185 | set -- $hash 1186 | 1187 | # Check that the first column (separated by whitespace) match in both 1188 | # checksum files. If any part of either file differs, mismatch. Abort. 1189 | null "$1" || while read -r chk _ || ok "$1"; do 1190 | equ "${#chk}" 64 && { 1191 | log "$repo_name" "Detected sha256 checksums." ERROR 1192 | log "blake3 is the new checksum provider for kiss. Please run" 1193 | log "'kiss checksum $repo_name' to regenerate the checksums file." 1194 | return 1 1195 | } 1196 | 1197 | printf '%s\n%s\n' "- ${chk:-missing}" "+ ${1:-no source}" 1198 | 1199 | equ "$1-${chk:-null}" "$chk-$1" || 1200 | equ "$1-${chk:-null}" "$1-SKIP" || 1201 | die "$repo_name" "Checksum mismatch" 1202 | 1203 | shift "$(($# != 0))" 1204 | done < "$repo_dir/checksums" 1205 | } 1206 | 1207 | pkg_conflicts() { 1208 | # Check to see if a package conflicts with another. 1209 | # _pkg comes from the caller. 1210 | log "$_pkg" "Checking for package conflicts" 1211 | 1212 | tmp_file "$_pkg" manifest-files 1213 | tmp_file "$_pkg" found-conflicts 1214 | 1215 | # Filter the tarball's manifest and select only files. Resolve all 1216 | # symlinks in file paths as well. 1217 | while read -r file; do case $file in *[!/]) 1218 | resolve_path "$file" 1219 | 1220 | printf '%s\n' "$_rpath" 1221 | esac done < "$PWD/$pkg_db/$_pkg/manifest" > "$_tmp_file_pre" 1222 | 1223 | cd "$tar_dir/$_pkg" 1224 | set +f 1225 | set -f "$sys_db"/*/manifest 1226 | 1227 | # Remove the current package from the manifest list. 1228 | fnr " $* " " $sys_db/$_pkg/manifest " " " 1229 | 1230 | # Intentional, globbing disabled. 1231 | # shellcheck disable=2046,2086 1232 | set -- $_fnr 1233 | 1234 | # Return here if there is nothing to check conflicts against. 1235 | ! equ "$#" 0 || return 0 1236 | 1237 | # Store the list of found conflicts in a file as we'll be using the 1238 | # information multiple times. Storing things in the cache dir allows 1239 | # us to be lazy as they'll be automatically removed on script end. 1240 | grep -Fxf "$_tmp_file_pre" -- "$@" 2>/dev/null > "$_tmp_file" || : 1241 | 1242 | # Enable alternatives automatically if it is safe to do so. 1243 | # This checks to see that the package that is about to be installed 1244 | # doesn't overwrite anything it shouldn't in '/var/db/kiss/installed'. 1245 | grep -q ":/var/db/kiss/installed/" "$_tmp_file" || safe=1 1246 | 1247 | if ! equ "$KISS_CHOICE" 1 && equ "$safe" 1 && [ -s "$_tmp_file" ]; then 1248 | # This is a novel way of offering an "alternatives" system. 1249 | # It is entirely dynamic and all "choices" are created and 1250 | # destroyed on the fly. 1251 | # 1252 | # When a conflict is found between two packages, the file 1253 | # is moved to a directory called "choices" and its name 1254 | # changed to store its parent package and its intended 1255 | # location. 1256 | # 1257 | # The package's manifest is then updated to reflect this 1258 | # new location. 1259 | # 1260 | # The 'kiss alternatives' command parses this directory and 1261 | # offers you the CHOICE of *swapping* entries in this 1262 | # directory for those on the filesystem. 1263 | # 1264 | # The alternatives command does the same thing we do here, 1265 | # it rewrites manifests and moves files around to make 1266 | # this work. 1267 | # 1268 | # Pretty nifty huh? 1269 | while IFS=: read -r _ con; do 1270 | printf '%s\n' "Found conflict $con" 1271 | 1272 | # Create the "choices" directory inside of the tarball. 1273 | # This directory will store the conflicting file. 1274 | mkdir -p "$PWD/$cho_db" 1275 | 1276 | # Construct the file name of the "db" entry of the 1277 | # conflicting file. (pkg_name>usr>bin>ls) 1278 | fnr "$con" '/' '>' 1279 | 1280 | # Move the conflicting file to the choices directory 1281 | # and name it according to the format above. 1282 | mv -f "$PWD$con" "$PWD/$cho_db/$_pkg$_fnr" 2>/dev/null || { 1283 | log "File must be in ${con%/*} and not a symlink to it" 1284 | log "This usually occurs when a binary is installed to" 1285 | die "/sbin instead of /usr/bin (example)" 1286 | } 1287 | done < "$_tmp_file" 1288 | 1289 | log "$_pkg" "Converted all conflicts to choices (kiss a)" 1290 | 1291 | # Rewrite the package's manifest to update its location 1292 | # to its new spot (and name) in the choices directory. 1293 | pkg_manifest "$_pkg" "$tar_dir" 1294 | 1295 | elif [ -s "$_tmp_file" ]; then 1296 | log "Package '$_pkg' conflicts with another package" "" "!>" 1297 | log "Run 'KISS_CHOICE=1 kiss i $_pkg' to add conflicts" "" "!>" 1298 | die "as alternatives." "" "!>" 1299 | fi 1300 | } 1301 | 1302 | pkg_alternatives() { 1303 | if equ "$1" -; then 1304 | while read -r pkg path; do 1305 | pkg_swap "$pkg" "$path" 1306 | done 1307 | 1308 | elif ok "$1"; then 1309 | pkg_swap "$@" 1310 | 1311 | else 1312 | # Go over each alternative and format the file 1313 | # name for listing. (pkg_name>usr>bin>ls) 1314 | set +f; for pkg in "$sys_ch/"*; do 1315 | fnr "${pkg##*/}" '>' '/' 1316 | printf '%s %s\n' "${_fnr%%/*}" "/${_fnr#*/}" 1317 | done 1318 | fi 1319 | } 1320 | 1321 | pkg_preferred() { 1322 | cd "$sys_db" 1323 | 1324 | # We only respect first argument 1325 | if ok "$1"; then 1326 | [ -d "$1" ] || die "'$1' not found" 1327 | set -- "$1/manifest" 1328 | else 1329 | set +f; set -f -- */manifest 1330 | fi 1331 | 1332 | # Pass /dev/null to grep to always ensure that the file name 1333 | # is printed even if we have a single file 1334 | pkg_alternatives | 1335 | cut -d' ' -f2- | 1336 | grep -Fxf - "$@" /dev/null | 1337 | sed 's/\/manifest:/ /' 1338 | 1339 | cd "$OLDPWD" 1340 | } 1341 | 1342 | pkg_swap() { 1343 | # Swap between package alternatives. 1344 | [ -d "$sys_db/$1" ] || die "'$1' not found" 1345 | 1346 | fnr "$1$2" '/' '>' 1347 | 1348 | [ -f "$sys_ch/$_fnr" ] || [ -h "$sys_ch/$_fnr" ] || 1349 | die "Alternative '$1 ${2:-null}' doesn't exist" 1350 | 1351 | if [ -f "$KISS_ROOT$2" ]; then 1352 | pkg_owner "/${2#/}" || 1353 | die "File '$2' exists on filesystem but isn't owned" 1354 | 1355 | log "Swapping '$2' from '$_owns' to '$1'" 1356 | 1357 | # Convert the current owner to an alternative and rewrite its manifest 1358 | # file to reflect this. Also ensure that timestamps are preserved. 1359 | cp -Pf "$KISS_ROOT$2" "$sys_ch/$_owns>${_fnr#*>}" 1360 | ! [ -h "$KISS_ROOT$2" ] && touch -r "$KISS_ROOT$2" "$sys_ch/$_owns>${_fnr#*>}" 1361 | pkg_manifest_replace "$_owns" "$2" "/$cho_db/$_owns>${_fnr#*>}" 1362 | fi 1363 | 1364 | # Convert the desired alternative to a real file and rewrite the manifest 1365 | # file to reflect this. The reverse of above. 1366 | mv -f "$sys_ch/$_fnr" "$KISS_ROOT/$2" 1367 | pkg_manifest_replace "$1" "/$cho_db/$_fnr" "$2" 1368 | } 1369 | 1370 | file_rwx() { 1371 | # Convert the output of 'ls' (rwxrwx---) to octal. This is simply 1372 | # a 1-9 loop with the second digit being the value of the field. 1373 | # 1374 | # NOTE: This drops setgid/setuid permissions and does not include 1375 | # them in the conversion. This is intentional. 1376 | unset oct o 1377 | 1378 | rwx=$(ls -ld "$1") 1379 | 1380 | for c in 14 22 31 44 52 61 74 82 91; do 1381 | rwx=${rwx#?} 1382 | 1383 | case $rwx in 1384 | [rwx]*) o=$((o + ${c#?})) ;; 1385 | [st]*) o=$((o + 1)) ;; 1386 | esac 1387 | 1388 | case $rwx in 1389 | [tT]*) oct=1$oct 1390 | esac 1391 | 1392 | case $((${c%?} % 3)) in 0) 1393 | oct=$oct$o 1394 | o=0 1395 | esac 1396 | done 1397 | } 1398 | 1399 | pkg_install_files() { 1400 | # Copy files and create directories (preserving permissions). 1401 | # The 'test $1' will run with '-z' for overwrite and '-e' for verify. 1402 | while { read -r file && _file=$KISS_ROOT$file; } do case $file in 1403 | */) 1404 | # Skip directories if they already exist in the file system. 1405 | # (Think /usr/bin, /usr/lib, etc). 1406 | [ -d "$_file" ] || { 1407 | file_rwx "$2/${file#/}" 1408 | mkdir -m "$oct" "$_file" 1409 | } 1410 | ;; 1411 | 1412 | *) 1413 | # Skip directories and files which exist in verify mode. 1414 | [ -d "$_file" ] || ! test "$1" "$_file" || 1415 | continue 1416 | 1417 | case $file in /etc/*[!/]) 1418 | # Handle /etc/ files in a special way (via a 3-way checksum) to 1419 | # determine how these files should be installed. Do we overwrite 1420 | # the existing file? Do we install it as $file.new to avoid 1421 | # deleting user configuration? etc. 1422 | # 1423 | # This is more or less similar to Arch Linux's Pacman with the 1424 | # user manually handling the .new files when and if they appear. 1425 | pkg_etc || continue 1426 | esac 1427 | 1428 | if [ -h "$_file" ]; then 1429 | # Copy the file to the destination directory overwriting 1430 | # any existing file. 1431 | cp -fP "$2$file" "${_file%/*}/." 1432 | 1433 | else 1434 | # Construct a temporary filename which is a) unique and 1435 | # b) identifiable as related to the package manager. 1436 | __tmp=${_file%/*}/__kiss-tmp-$_pkg-${file##*/}-$KISS_PID 1437 | 1438 | # Copy the file to the destination directory with the 1439 | # temporary name created above. 1440 | cp -fP "$2$file" "$__tmp" && 1441 | 1442 | # Ensure timestamps are preserved. 1443 | if ! [ -h "$2$file" ]; then touch -r "$2$file" "$__tmp"; fi && 1444 | 1445 | # Atomically move the temporary file to its final 1446 | # destination. The running processes will either get 1447 | # the old file or the new one. 1448 | mv -f "$__tmp" "$_file" 1449 | fi 1450 | esac || return 1; done 1451 | } 1452 | 1453 | pkg_remove_files() { 1454 | # Remove a file list from the system. This function runs during package 1455 | # installation and package removal. Combining the removals in these two 1456 | # functions allows us to stop duplicating code. 1457 | while read -r file; do 1458 | case $file in /etc/?*[!/]) 1459 | read -r sum_pkg <&3 ||: 1460 | 1461 | case "${#sum_pkg}" in 1462 | 64) sh256 "$KISS_ROOT/$file" >/dev/null ;; 1463 | *) b3 "$KISS_ROOT/$file" >/dev/null ;; 1464 | esac 1465 | 1466 | equ "$hash" "$sum_pkg" || { 1467 | printf 'Skipping %s (modified)\n' "$file" 1468 | continue 1469 | } 1470 | esac 1471 | 1472 | _file=${KISS_ROOT:+"$KISS_ROOT/"}${file%%/} 1473 | 1474 | # Queue all directory symlinks for later removal. 1475 | if [ -h "$_file" ] && [ -d "$_file" ]; then 1476 | case $file in /*/*/) 1477 | set -- "$@" "$_file" 1478 | esac 1479 | 1480 | # Remove empty directories. 1481 | elif [ -d "$_file" ]; then 1482 | rmdir "$_file" 2>/dev/null || : 1483 | 1484 | # Remove everything else. 1485 | else 1486 | rm -f "$_file" 1487 | fi 1488 | done 1489 | 1490 | # Remove all broken directory symlinks. 1491 | for sym do 1492 | [ -e "$sym" ] || rm -f "$sym" 1493 | done 1494 | } 1495 | 1496 | pkg_etc() { 1497 | read -r sum_old <&3 2>/dev/null ||: 1498 | 1499 | case "${#sum_old}" in 1500 | 64) sh256 "$tar_dir/$_pkg$file" "$KISS_ROOT$file" >/dev/null ;; 1501 | *) b3 "$tar_dir/$_pkg$file" "$KISS_ROOT$file" >/dev/null ;; 1502 | esac 1503 | 1504 | sum_new=${hash%%"$newline"*} 1505 | sum_sys=${hash#*"$newline"} 1506 | 1507 | # Compare the three checksums to determine what to do. 1508 | case ${sum_old:-null}${sum_sys:-null}${sum_new} in 1509 | # old = Y, sys = X, new = Y 1510 | "${sum_new}${sum_sys}${sum_old}") 1511 | return 1 1512 | ;; 1513 | 1514 | # old = X, sys = X, new = X 1515 | # old = X, sys = Y, new = Y 1516 | # old = X, sys = X, new = Y 1517 | "${sum_old}${sum_old}${sum_old}"|\ 1518 | "${sum_old:-null}${sum_sys}${sum_sys}"|\ 1519 | "${sum_sys}${sum_old}"*) 1520 | 1521 | ;; 1522 | 1523 | # All other cases. 1524 | *) 1525 | war "$_pkg" "saving $file as $file.new" 1526 | _file=$_file.new 1527 | ;; 1528 | esac 1529 | } 1530 | 1531 | pkg_removable() { 1532 | # Check if a package is removable and die if it is not. 1533 | # A package is removable when it has no dependents. 1534 | log "$1" "Checking if package removable" 1535 | 1536 | cd "$sys_db" 1537 | set +f 1538 | 1539 | ! grep -lFx -- "$1" */depends || 1540 | die "$1" "Not removable, has dependents" 1541 | 1542 | set -f 1543 | cd "$OLDPWD" 1544 | 1545 | # Check if a package would leave dangling "orphaned" alternatives. 1546 | tmp_file "$1" owned-conflicts 1547 | pkg_preferred "$1" | tee "$_tmp_file" 1548 | 1549 | cnt=$(wc -l < "$_tmp_file") 1550 | equ "$cnt" 0 || 1551 | die "$1" "Not removable, package leaves behind $cnt orphaned alternatives" 1552 | } 1553 | 1554 | pkg_remove() { 1555 | # Remove a package and all of its files. The '/etc' directory is handled 1556 | # differently and configuration files are *not* overwritten. 1557 | [ -d "$sys_db/$1" ] || die "'$1' not installed" 1558 | 1559 | trap_off 1560 | 1561 | # Intended behavior. 1562 | # shellcheck disable=2030,2031 1563 | equ "$KISS_FORCE" 1 || pkg_removable "$1" 1564 | 1565 | # arg1: pre-remove 1566 | # arg2: package name 1567 | # arg3: path to installed database 1568 | run_hook_pkg pre-remove "$1" 1569 | run_hook pre-remove "$1" "$sys_db/$1" 1570 | 1571 | # Make a backup of any etcsums if they exist. 1572 | tmp_file_copy "$1" etcsums-copy "$sys_db/$1/etcsums" 1573 | 1574 | log "$1" "Removing package" 1575 | pkg_remove_files < "$sys_db/$1/manifest" 3< "$_tmp_file" 1576 | 1577 | trap_on 1578 | log "$1" "Removed successfully" 1579 | } 1580 | 1581 | pkg_installable() { 1582 | # Check if a package is removable and die if it is not. 1583 | # A package is removable when all of its dependencies 1584 | # are satisfied. 1585 | log "$1" "Checking if package installable" 1586 | 1587 | # False positive. 1588 | # shellcheck disable=2094 1589 | ! [ -f "$2" ] || 1590 | 1591 | while read -r dep dep_type || ok "$dep"; do 1592 | case "$dep $dep_type" in [!\#]?*\ ) 1593 | ! [ -d "$sys_db/$dep" ] || continue 1594 | 1595 | printf '%s %s\n' "$dep" "$dep_type" 1596 | 1597 | set -- "$1" "$2" "$(($3 + 1))" 1598 | esac 1599 | done < "$2" 1600 | 1601 | case ${3:-0} in [1-9]*) 1602 | die "$1" "Package not installable, missing $3 package(s)" 1603 | esac 1604 | } 1605 | 1606 | pkg_install() { 1607 | # Install a built package tarball. 1608 | # 1609 | # Package installation works similarly to the method used by Slackware in 1610 | # some of their tooling. It's not the obvious solution to the problem, 1611 | # however it is the best solution at this given time. 1612 | # 1613 | # When an installation is an update to an existing package, instead of 1614 | # removing the old version first we do something different. 1615 | # 1616 | # The new version is installed overwriting any files which it has in 1617 | # common with the previously installed version of the package. 1618 | # 1619 | # A "diff" is then generated between the old and new versions and contains 1620 | # any files existing in the old version but not the new version. 1621 | # 1622 | # The package manager then goes and removes these files which leaves us 1623 | # with the new package version in the file system and all traces of the 1624 | # old version gone. 1625 | # 1626 | # For good measure the package manager will then install the new package 1627 | # an additional time. This is to ensure that the above diff didn't contain 1628 | # anything incorrect. 1629 | # 1630 | # This is the better method as it is "seamless". An update to busybox won't 1631 | # create a window in which there is no access to all of its utilities. 1632 | 1633 | # Install can also take the full path to a tarball. We don't need to check 1634 | # the repository if this is the case. 1635 | case $1 in 1636 | *.tar.*) 1637 | [ -f "$1" ] || die "File '$1' does not exist" 1638 | 1639 | tar_file=$1 1640 | _pkg=${1##*/} 1641 | _pkg=${_pkg%@*} 1642 | ;; 1643 | 1644 | *) 1645 | pkg_cache "$1" || die "$1" "Not yet built" 1646 | _pkg=$1 1647 | ;; 1648 | esac 1649 | 1650 | trap_off 1651 | mkcd "$tar_dir/$_pkg" 1652 | 1653 | # The tarball is extracted to a temporary directory where its contents are 1654 | # then "installed" to the filesystem. Running this step as soon as possible 1655 | # allows us to also check the validity of the tarball and bail out early 1656 | # if needed. 1657 | decompress "$tar_file" | tar xf - 1658 | 1659 | # Naively assume that the existence of a manifest file is all that 1660 | # determines a valid KISS package from an invalid one. This should be a 1661 | # fine assumption to make in 99.99% of cases. 1662 | [ -f "$PWD/$pkg_db/$_pkg/manifest" ] || die "Not a valid KISS package" 1663 | 1664 | # Intended behavior. 1665 | # shellcheck disable=2030,2031 1666 | equ "$KISS_FORCE" 1 || { 1667 | pkg_manifest_validate 1668 | pkg_installable "$_pkg" "$PWD/$pkg_db/$_pkg/depends" 1669 | } 1670 | 1671 | # arg1: pre-install 1672 | # arg2: package name 1673 | # arg3: path to extracted package 1674 | run_hook pre-install "$_pkg" "$PWD" 1675 | 1676 | pkg_conflicts 1677 | 1678 | log "$_pkg" "Installing package (${tar_file##*/})" 1679 | 1680 | # If the package is already installed (and this is an upgrade) make a 1681 | # backup of the manifest and etcsums files. 1682 | tmp_file_copy "$_pkg" manifest-copy "$sys_db/$_pkg/manifest" 1683 | tmp_file_copy "$_pkg" etcsums-copy "$sys_db/$_pkg/etcsums" 1684 | tmp_file "$_pkg" manifest-diff 1685 | 1686 | tar_man=$PWD/$pkg_db/$_pkg/manifest 1687 | 1688 | # Generate a list of files which exist in the currently installed manifest 1689 | # but not in the newer (to be installed) manifest. 1690 | grep -vFxf "$tar_man" "$_tmp_file_pre_pre" > "$_tmp_file" 2>/dev/null ||: 1691 | 1692 | # Reverse the manifest file so that we start shallow and go deeper as we 1693 | # iterate over each item. This is needed so that directories are created 1694 | # going down the tree. 1695 | tmp_file "$_pkg" manifest-reverse 1696 | sort "$tar_man" > "$_tmp_file" 1697 | 1698 | if 1699 | # Install the package's files by iterating over its manifest. 1700 | pkg_install_files -z "$PWD" < "$_tmp_file" 3< "$_tmp_file_pre_pre" && 1701 | 1702 | # This is the aforementioned step removing any files from the old 1703 | # version of the package if the installation is an update. Each file 1704 | # type has to be specially handled to ensure no system breakage occurs. 1705 | pkg_remove_files < "$_tmp_file_pre" 3< "$_tmp_file_pre_pre" && 1706 | 1707 | # Install the package's files a second time to fix any mess caused by 1708 | # the above removal of the previous version of the package. 1709 | pkg_install_files -e "$PWD" < "$_tmp_file" 3< "$_tmp_file_pre_pre" 1710 | 1711 | then 1712 | trap_on 1713 | 1714 | # arg1: post-install 1715 | # arg2: package name 1716 | # arg3: path to installed package database 1717 | run_hook_pkg post-install "$_pkg" 1718 | run_hook post-install "$_pkg" "$sys_db/$_pkg" 1719 | 1720 | log "$_pkg" "Installed successfully" 1721 | 1722 | else 1723 | pkg_clean 1724 | log "$_pkg" "Failed to install package." ERROR 1725 | die "$_pkg" "Filesystem now dirty, manual repair needed." 1726 | fi 1727 | } 1728 | 1729 | pkg_update() { 1730 | log "Updating repositories" 1731 | 1732 | # Create a list of all repositories. 1733 | # Intentional, globbing disabled. 1734 | # shellcheck disable=2046,2086 1735 | { IFS=:; set -- $KISS_PATH; unset IFS; } 1736 | 1737 | # Update each repository in '$KISS_PATH'. 1738 | for repo do 1739 | ok "$repo" || continue 1740 | if git -C "$repo" rev-parse 'HEAD@{upstream}' >/dev/null; then 1741 | repo_type=git 1742 | 1743 | # Get the Git repository root directory. 1744 | subm=$(git -C "$repo" rev-parse --show-superproject-working-tree) 1745 | repo=$(git -C "${subm:-"$repo"}" rev-parse --show-toplevel) 1746 | 1747 | elif ! [ -d "$repo" ]; then 1748 | continue 1749 | 1750 | else 1751 | unset repo_type 1752 | fi 1753 | 1754 | pkg_update_repo 1755 | done 1756 | 1757 | log "Run 'kiss U' to upgrade packages" 1758 | } 1759 | 1760 | pkg_update_repo() { 1761 | cd "$repo" || die "Repository '$repo' inaccessible" 1762 | 1763 | contains "$repos" "$PWD" || { 1764 | repos="$repos $PWD" 1765 | 1766 | log "$PWD" " " 1767 | 1768 | am_owner "$PWD" || { 1769 | printf 'Need "%s" to update\n' "$user" 1770 | set -- as_user 1771 | } 1772 | 1773 | # arg1: pre-update 1774 | # arg2: need su? 1775 | # arg3: owner 1776 | # env: PWD is path to repository 1777 | run_hook pre-update "$#" "$user" 1778 | 1779 | case $repo_type in git) 1780 | pkg_update_git "$@" 1781 | esac 1782 | 1783 | # arg1: post-update 1784 | # env: PWD is path to repository 1785 | run_hook post-update 1786 | 1787 | [ ! -r MOTD ] || { 1788 | log "$PWD" "Printing MOTD" 1789 | cat MOTD 1790 | } 1791 | } 1792 | } 1793 | 1794 | pkg_update_git() { 1795 | # Display whether or not signature verification is enabled. 1796 | case $(git config --get merge.verifySignatures) in true) 1797 | printf 'Signature verification enabled.\n' 1798 | esac 1799 | 1800 | "$@" git pull 1801 | "$@" git submodule update --remote --init -f 1802 | } 1803 | 1804 | pkg_upgrade() { 1805 | log "Checking for new package versions" 1806 | set +f 1807 | 1808 | for pkg in "$sys_db/"*; do set -f 1809 | pkg_find_version "${pkg##*/}" "" "" "$sys_db" 1810 | pkg_find_version "${pkg##*/}" 1811 | 1812 | # Detect repository orphans (installed packages with no 1813 | # associated repository). 1814 | case $repo_dir in */var/db/kiss/installed/*) 1815 | _repo_orp="$_repo_orp$newline${pkg##*/}" 1816 | esac 1817 | 1818 | # Compare installed packages to repository packages. 1819 | equ "$ver_pre-$rel_pre" "$repo_ver-$repo_rel" || { 1820 | set -- "$@" "${pkg##*/}" 1821 | 1822 | printf '%s %s => %s\n' \ 1823 | "${pkg##*/}" "$ver_pre-$rel_pre" "$repo_ver-$repo_rel" 1824 | } 1825 | done 1826 | 1827 | case $_repo_orp in *?*) 1828 | war "Packages without repository$_repo_orp" 1829 | esac 1830 | 1831 | build_install=0 1832 | prefer_cache=1 1833 | 1834 | ! contains "$*" kiss || { 1835 | log "Detected package manager update" 1836 | log "The package manager will be updated first" 1837 | 1838 | prompt 1839 | pkg_build_all kiss 1840 | 1841 | log "Updated the package manager" 1842 | log "Re-run 'kiss update' to update your system" 1843 | return 0 1844 | } 1845 | 1846 | for _ do 1847 | pkg_order "$@" 1848 | 1849 | # Intentional, globbing disabled. 1850 | # shellcheck disable=2046,2086 1851 | set -- $order 1852 | 1853 | prompt "Packages to update ($#): $*" 1854 | pkg_build_all "$@" 1855 | log "Updated all packages" 1856 | return 0 1857 | done 1858 | 1859 | log "Nothing to do" 1860 | } 1861 | 1862 | pkg_clean() { 1863 | # Clean up on exit or error. This removes everything related to the build. 1864 | # If _KISS_LVL is (1) we are the top-level process - the entire cache will 1865 | # be removed. If _KISS_LVL is any other value, remove only the tar directory. 1866 | case ${KISS_DEBUG:-0}-${_KISS_LVL:-1} in 1867 | 0-1) rm -rf "$proc" ;; 1868 | 0-*) rm -rf "$tar_dir" 1869 | esac 1870 | } 1871 | 1872 | pkg_help_ext() { 1873 | log 'Installed extensions (kiss-* in PATH)' 1874 | 1875 | # Intentional, globbing disabled. 1876 | # shellcheck disable=2046,2030,2031 1877 | set -- $(pkg_find kiss-\* all -x "$PATH") 1878 | 1879 | # To align descriptions figure out which extension has the longest 1880 | # name by doing a simple 'name > max ? name : max' on the basename 1881 | # of the path with 'kiss-' stripped as well. 1882 | # 1883 | # This also removes any duplicates found in '$PATH', picking the 1884 | # first match. 1885 | for path do 1886 | p=${path#*/kiss-} 1887 | 1888 | case " $seen " in *" $p "*) 1889 | shift 1890 | continue 1891 | esac 1892 | 1893 | seen=" $seen $p " 1894 | max=$((${#p} > max ? ${#p}+1 : max)) 1895 | done 1896 | 1897 | # Print each extension, grab its description from the second line 1898 | # in the file and align the output based on the above max. 1899 | for path do 1900 | # Open the extension as a file descriptor. 1901 | exec 3< "$path" 1902 | 1903 | # Grab the second line in the extension. 1904 | { read -r _ && IFS=\#$IFS read -r _ cmt; } <&3 1905 | 1906 | printf "%b->%b %-${max}s %s\\n" \ 1907 | "$c1" "$c3" "${path#*/kiss-}" "$cmt" 1908 | done >&2 1909 | } 1910 | 1911 | trap_on() { 1912 | # Catch errors and ensure that build files and directories are cleaned 1913 | # up before we die. This occurs on 'Ctrl+C' as well as success and error. 1914 | trap trap_INT INT 1915 | trap trap_EXIT EXIT 1916 | } 1917 | 1918 | trap_INT() { 1919 | run_hook SIGINT 1920 | exit 1 1921 | } 1922 | 1923 | trap_EXIT() { 1924 | pkg_clean 1925 | run_hook SIGEXIT 1926 | } 1927 | 1928 | trap_off() { 1929 | # Block being able to abort the script with 'Ctrl+C'. Removes all risk of 1930 | # the user aborting a package install/removal leaving an incomplete package 1931 | # installed. 1932 | trap "" INT EXIT 1933 | } 1934 | 1935 | args() { 1936 | # Parse script arguments manually. This is rather easy to do in our case 1937 | # since the first argument is always an "action" and the arguments that 1938 | # follow are all package names. 1939 | action=$1 1940 | shift "$(($# != 0))" 1941 | 1942 | # Ensure that arguments do not contain invalid characters. Wildcards can 1943 | # not be used here as they would conflict with kiss extensions. 1944 | case $action in 1945 | a|alternatives|p|preferred) 1946 | case $1 in *\**|*\!*|*\[*|*\ *|*\]*|*/*|*"$newline"*) 1947 | die "Invalid argument: '!*[ ]/\\n' ($1)" 1948 | esac 1949 | ;; 1950 | 1951 | b|build|c|checksum|d|download|i|install|l|list|r|remove) 1952 | for _arg do case ${action%%"${action#?}"}-$_arg in 1953 | i-*\!*|i-*\**|i-*\[*|i-*\ *|i-*\]*|i-*"$newline"*) 1954 | die "Invalid argument: '!*[ ]\\n' ('$_arg')" 1955 | ;; 1956 | 1957 | [!i]-*\!*|[!i]-*\**|[!i]-*\[*|[!i]-*\ *|\ 1958 | [!i]-*\]*|[!i]-*/*|[!i]-*"$newline"*) 1959 | die "Invalid argument: '!*[ ]/\\n' ('$_arg')" 1960 | ;; 1961 | esac done 1962 | 1963 | # When no arguments are given on the command-line, use the basename 1964 | # of the current directory as the package name and add the parent 1965 | # directory to the running process' KISS_PATH. 1966 | case ${action%%"${action#?}"}-$# in [!l]-0) 1967 | export KISS_PATH="${PWD%/*}:$KISS_PATH" 1968 | set -- "${PWD##*/}" 1969 | esac 1970 | 1971 | # Search the installed database first when removing packages. Dependency 1972 | # files may differ when repositories change. Removal is not dependent on 1973 | # the state of the repository. 1974 | case $action in r|remove) 1975 | export KISS_PATH="$sys_db:$KISS_PATH" 1976 | esac 1977 | 1978 | # Order the argument list based on dependence. 1979 | pkg_order "$@" 1980 | 1981 | # Intentional, globbing disabled. 1982 | # shellcheck disable=2046,2086 1983 | set -- $order 1984 | ;; 1985 | esac 1986 | 1987 | # Need to increment _KISS_LVL here to ensure we don't wipe the cache 1988 | # early by non-asroot invocations. 1989 | export _KISS_LVL=$((_KISS_LVL + 1)) 1990 | 1991 | # Rerun the script as root with a fixed environment if needed. We sadly 1992 | # can't run singular functions as root so this is needed. 1993 | # 1994 | # Intended behavior. 1995 | # shellcheck disable=2030,2031 1996 | case $action in a|alternatives|i|install|r|remove) 1997 | if ok "$1" && ! am_owner "$KISS_ROOT/"; then 1998 | trap_off 1999 | 2000 | cd "$ppwd" 2001 | as_user env \ 2002 | LOGNAME="$user" \ 2003 | HOME="$HOME" \ 2004 | XDG_CACHE_HOME="$XDG_CACHE_HOME" \ 2005 | KISS_CHOICE="$KISS_CHOICE" \ 2006 | KISS_COLOR="$KISS_COLOR" \ 2007 | KISS_COMPRESS="$KISS_COMPRESS" \ 2008 | KISS_FORCE="$KISS_FORCE" \ 2009 | KISS_HOOK="$KISS_HOOK" \ 2010 | KISS_PATH="$KISS_PATH" \ 2011 | KISS_PID="$KISS_PID" \ 2012 | KISS_ROOT="$KISS_ROOT" \ 2013 | KISS_TMPDIR="$KISS_TMPDIR" \ 2014 | _KISS_DATE="$time" \ 2015 | _KISS_LVL="$_KISS_LVL" \ 2016 | "$0" "$action" "$@" 2017 | 2018 | trap_on 2019 | return 2020 | fi 2021 | esac 2022 | 2023 | # Actions can be abbreviated to their first letter. This saves keystrokes 2024 | # once you memorize the commands. 2025 | case $action in 2026 | a|alternatives) pkg_alternatives "$@" ;; 2027 | b|build) pkg_build_all "$@" ;; 2028 | c|checksum) for pkg do pkg_checksum "$pkg"; done ;; 2029 | d|download) for pkg do pkg_source "$pkg"; done ;; 2030 | H|help-ext) pkg_help_ext "$@" ;; 2031 | i|install) for pkg do pkg_install "$pkg"; done ;; 2032 | l|list) pkg_list_version "$@" ;; 2033 | p|preferred) pkg_preferred "$@" ;; 2034 | r|remove) for pkg in $redro; do pkg_remove "$pkg"; done ;; 2035 | s|search) for pkg do pkg_find "$pkg" all; done ;; 2036 | u|update) pkg_update ;; 2037 | U|upgrade) pkg_upgrade ;; 2038 | v|version) printf '6.2.0\n' ;; 2039 | 2040 | '') 2041 | log 'kiss [a|b|c|d|i|l|p|r|s|u|U|v] [pkg]...' 2042 | log 'alternatives List and swap alternatives' 2043 | log 'build Build packages' 2044 | log 'checksum Generate checksums' 2045 | log 'download Download sources' 2046 | log 'install Install packages' 2047 | log 'list List installed packages' 2048 | log 'preferred List owners of files with alternatives' 2049 | log 'remove Remove packages' 2050 | log 'search Search for packages' 2051 | log 'update Update the repositories' 2052 | log 'upgrade Update the system' 2053 | log 'version Package manager version' 2054 | 2055 | printf '\nRun "kiss [H|help-ext]" to see all actions\n' 2056 | ;; 2057 | 2058 | *) 2059 | # _KISS_LVL must be reset here so the that any extensions 2060 | # which call the package manager do not increment the value 2061 | # further than the parent instance. 2062 | pkg_find "kiss-$action*" "" -x "$PATH" 2063 | _KISS_LVL=0 KISS_PID='' "$repo_dir" "$@" 2064 | ;; 2065 | esac 2066 | } 2067 | 2068 | create_tmp_dirs() { 2069 | # Root directory. 2070 | KISS_ROOT=${KISS_ROOT%"${KISS_ROOT##*[!/]}"} 2071 | 2072 | # This allows for automatic setup of a KISS chroot and will 2073 | # do nothing on a normal system. 2074 | mkdir -p "$KISS_ROOT/" 2>/dev/null || : 2075 | 2076 | # System package database. 2077 | sys_db=$KISS_ROOT/${pkg_db:=var/db/kiss/installed} 2078 | sys_ch=$KISS_ROOT/${cho_db:=var/db/kiss/choices} 2079 | 2080 | # Top-level cache directory. 2081 | cac_dir=${XDG_CACHE_HOME:-"${HOME%"${HOME##*[!/]}"}/.cache"} 2082 | cac_dir=${cac_dir%"${cac_dir##*[!/]}"}/kiss 2083 | 2084 | # Persistent cache directories. 2085 | src_dir=$cac_dir/sources 2086 | log_dir=$cac_dir/logs/${time%-*} 2087 | bin_dir=$cac_dir/bin 2088 | 2089 | # Top-level Temporary cache directory. 2090 | proc=${KISS_TMPDIR:="$cac_dir/proc"} 2091 | proc=${proc%"${proc##*[!/]}"}/$KISS_PID 2092 | 2093 | # Temporary cache directories. 2094 | mak_dir=$proc/build 2095 | pkg_dir=$proc/pkg 2096 | tar_dir=$proc/extract 2097 | tmp_dir=$proc/tmp 2098 | 2099 | mkdir -p "$src_dir" "$log_dir" "$bin_dir" \ 2100 | "$mak_dir" "$pkg_dir" "$tar_dir" "$tmp_dir" 2101 | } 2102 | 2103 | main() { 2104 | # Globally disable globbing and enable exit-on-error. 2105 | set -ef 2106 | 2107 | # Color can be disabled via the environment variable KISS_COLOR. Colors are 2108 | # also automatically disabled if output is being used in a pipe/redirection. 2109 | equ "$KISS_COLOR" 0 || ! [ -t 2 ] || { 2110 | c1='\033[1;33m' 2111 | c2='\033[1;34m' 2112 | c3='\033[m' 2113 | } 2114 | 2115 | # Store the original working directory to ensure that relative paths 2116 | # passed by the user on the command-line properly resolve to locations 2117 | # in the filesystem. 2118 | ppwd=$PWD 2119 | 2120 | # Never know when you're gonna need one of these. 2121 | newline=" 2122 | " 2123 | 2124 | # Defaults for environment variables. 2125 | : "${KISS_COMPRESS:=gz}" 2126 | : "${KISS_PID:=$$}" 2127 | : "${LOGNAME:?POSIX requires LOGNAME be set}" 2128 | 2129 | # Figure out which 'sudo' command to use based on the user's choice or what 2130 | # is available on the system. 2131 | cmd_su=${KISS_SU:-"$( 2132 | command -v ssu || 2133 | command -v sudo || 2134 | command -v doas || 2135 | command -v su 2136 | )"} || cmd_su=su 2137 | 2138 | # Figure out which utility is available to dump elf information. 2139 | cmd_elf=${KISS_ELF:-"$( 2140 | command -v readelf || 2141 | command -v eu-readelf || 2142 | command -v llvm-readelf 2143 | )"} || cmd_elf=ldd 2144 | 2145 | # b3sum is, for now, the only supported blake3 digest utility. 2146 | cmd_b3=b3sum 2147 | 2148 | # Figure out which sha256 utility is available. 2149 | cmd_sha=${KISS_CHK:-"$( 2150 | command -v openssl || 2151 | command -v sha256sum || 2152 | command -v sha256 || 2153 | command -v shasum || 2154 | command -v digest 2155 | )"} || war "No sha256 utility found" 2156 | 2157 | # Figure out which download utility is available. 2158 | cmd_get=${KISS_GET:-"$( 2159 | command -v aria2c || 2160 | command -v axel || 2161 | command -v curl || 2162 | command -v wget || 2163 | command -v wget2 2164 | )"} || die "No download utility found (aria2c, axel, curl, wget, wget2)" 2165 | 2166 | # Store the date and time of script invocation to be used as the name of 2167 | # the log files the package manager creates during builds. 2168 | # Accept _KISS_DATE in case this process has been called by a kiss process 2169 | # started on a different day, which could cause permission issues if we 2170 | # create log_dir as root. 2171 | time=${_KISS_DATE:-"$(date +%Y-%m-%d-%H:%M)"} 2172 | 2173 | create_tmp_dirs 2174 | trap_on 2175 | 2176 | args "$@" 2177 | } 2178 | 2179 | main "$@" 2180 | --------------------------------------------------------------------------------