diff --git a/kiss b/kiss index 730e8f7..c309ef4 100755 --- a/kiss +++ b/kiss @@ -148,6 +148,14 @@ dosu() { fi } +regex_escape() { + # Escape all required characters in both the search and + # replace portions of two strings for use in a 'sed' call + # as "plain-text". + sea=$(echo "$1" | sed 's/[]\/$*.^[]/\\&/g') + rep=$(echo "$2" | sed 's/[\/&]/\\&/g') +} + pkg_lint() { # Check that each mandatory file in the package entry exists. log "$1" "Checking repository files" @@ -745,15 +753,14 @@ pkg_conflicts() { tar xf "$1" -O "./$pkg_db/$2/manifest" | while read -r file; do case $file in */) continue; esac - printf '%s\n' "$file" - - readlink -f "$KISS_ROOT/$file" ||: + printf '%s/%s\n' \ + "$(readlink -f "$KISS_ROOT/${file%/*}")" "${file##*/}" done > "$cac_dir/$pid-m" p_name=$2 # Generate a list of all installed package manifests. - set +ef + set +f set -f -- "$sys_db"/*/manifest # Filter the manifest list and remove the previously @@ -762,21 +769,118 @@ pkg_conflicts() { i_name=${pkg%/*} i_name=${i_name##*/} - shift + shift "$(($# ? 1 : 0))" [ "$p_name" = "$i_name" ] && continue set -- "$@" "$pkg" done + [ -s "$cac_dir/$pid-m" ] || return 0 + # Use 'grep' to list matching lines between the to # be installed package's manifest and the above filtered # list. - [ -s "$cac_dir/$pid-m" ] && - "$grep" -Fxf "$cac_dir/$pid-m" -- "$@" && - die "Package '$p_name' conflicts with another package" + if [ "$KISS_CHOICE" ]; then + "$grep" -Fxf "$cac_dir/$pid-m" -- "$@" | - set -e + # 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 pro con || [ "$pro" ]; 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=$(echo "$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" + + regex_escape "$con" "/$cho_dir/$p_name$con_name" + + # Rewrite the package's manifest to update its location + # to its new spot (and name) in the choices directory. + sed -i "s/$sea/$rep/" \ + "$tar_dir/$p_name/$pkg_db/$p_name/manifest" + done + else + if "$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 + fi +} + +pkg_swap() { + # Swap between package alternatives. + + # Check to see if the package is installed. This + # will exit with an error if it is not. + 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'" + + regex_escape "$2" "$PWD/$pkg_owns>${alt#*>}" + + # Convert the current owner to an alternative and rewrite + # its manifest file to reflect this. + dosu mv -f "'$2'" "'$pkg_owns>${alt#*>}'" + dosu sed -i "'s/$sea/$rep/'" "'../installed/$pkg_owns/manifest'" + fi + + regex_escape "$PWD/$alt" "$2" + + # Convert the desired alternative to a real file and rewrite + # the manifest file to reflect this. The reverse of above. + dosu mv -f "'$alt'" "'$2'" + dosu sed -i "'s/$sea/$rep/'" "'../installed/$1/manifest'" } pkg_remove() { @@ -860,8 +964,6 @@ pkg_install() { pkg_name=${pkg_name%/*} pkg_name=${pkg_name##*/} - pkg_conflicts "$tar_file" "$pkg_name" - mkdir -p "$tar_dir/$pkg_name" # Extract the tar-ball to catch any errors before installation begins. @@ -882,6 +984,8 @@ pkg_install() { [ "$install_dep" ] && die "$1" "Package requires ${install_dep%, }" + pkg_conflicts "$tar_file" "$pkg_name" + log "$pkg_name" "Installing package incrementally" # Block being able to abort the script with Ctrl+C during installation. @@ -1109,12 +1213,16 @@ args() { # # This handles the globbing characters '*', '!', '[' and ']' as per: # https://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html - [ "$action" != search ] && [ "$action" != s ] && - case $@ in - *'*'*|*'!'*|*'['*|*']'*) - die "Arguments contain invalid characters: '!*[]'" - ;; - esac + case $action in + a|alternatives|s|search) ;; + + *) + case $@ in + *'*'*|*'!'*|*'['*|*']'*) + die "Arguments contain invalid characters: '!*[]'" + ;; + esac + esac # Parse some arguments earlier to remove the need to duplicate code. case $action in @@ -1126,6 +1234,28 @@ args() { # Actions can be abbreviated to their first letter. This saves # keystrokes once you memorize the commands. case $action in + a|alternatives) + if [ "$1" ]; then + pkg_swap "$@" + + else + log "Alternatives:" + + # Go to the choices directory and hide errors + # as there is nothing to list if the directory + # doesn't exist. + cd "$sys_db/../choices" 2>/dev/null + set +f + + # Go over each alternative and format the file + # name for listing. (pkg_name>usr>bin>ls) + for pkg in *; do + [ "$pkg" = '*' ] || + printf '%s\n' "$pkg" | sed 's|>| /|;s|>|/|g' + done + fi + ;; + b|build) # If no arguments were passed, rebuild all packages. [ "$1" ] || { @@ -1206,15 +1336,16 @@ args() { ;; h|help|-h|--help|'') - log 'kiss [b|c|i|l|r|s|u|v] [pkg] [pkg] [pkg]' - 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 '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' ;; *) die "'kiss $action' is not a valid command" ;;