From ddf9bc6abc58a123f9c36c754d5d3183f35c8bbe Mon Sep 17 00:00:00 2001 From: jutty Date: Thu, 11 Jul 2024 12:03:03 -0300 Subject: [PATCH] Implement package conflict resolution by input --- docs/development/portability.md | 20 ++-- docs/development/roadmap.md | 6 + docs/development/validate_input_packages.md | 28 +++++ docs/usage/cache.md | 9 ++ src/index.sh | 7 +- src/package/package_conflict_resolution.sh | 121 +++++++++++++------- src/package/package_manager.sh | 11 +- src/package/update_package_cache.sh | 19 +++ src/package/validate_input_packages.sh | 22 ++++ src/utility.sh | 51 +++++---- tori | 12 +- 11 files changed, 225 insertions(+), 81 deletions(-) create mode 100644 docs/development/validate_input_packages.md create mode 100644 docs/usage/cache.md create mode 100644 src/package/update_package_cache.sh create mode 100644 src/package/validate_input_packages.sh diff --git a/docs/development/portability.md b/docs/development/portability.md index 6eec863..1a84171 100644 --- a/docs/development/portability.md +++ b/docs/development/portability.md @@ -4,22 +4,26 @@ Because the application is meant to manage the installation of packages for you, To achieve this portability and independence, it is meant to run on a POSIX-compatible shell where POSIX utilities are available. If your system does not provide this, it is very unlikely `tori` will function. +Note that while `tori` expects a POSIX _shell_, it is not meant as a universal tool able to run on any POSIX system. A POSIX shell is required because it is the interpreter for the whole source code in which tori was implemented, but for some of its purposes `tori` needs to be running in a supported operating system. For example, it has specific package management features that work by abstracting the actual package manager options behind a function that detects the operating system and then runs the apropriate command. + +While it strives to do so, in some situations, tori may perform tasks by relying on resources not specified by POSIX, such as when there is no option or the available option has readability or usability downsides. In these situations, tori tends to rely on specific functions that will switch their behavior depending on the operating system's support for the operation. + Below is a list of assumptions made about what your system supports: - shell - - `local` keyword - - `read` keyword with `read -r -p ` syntax - - `printf` - - `echo` - -- utilities + - `local` + - `read` with `read -r -p ` syntax + - `mkdir` with `-pggjj - `find` - `grep` - `sed` - `xargs` - `uname` - - `date` with nanoseconds as `%N` - - While nanoseconds support in `date` is not in the POSIX 2017 standard, it is used only when `$DEBUG` is set in the environment and is available on the currently supported systems (FreeBSD, Void Linux) and on the next operating system with planned support (Debian). + - `date` + - with nanoseconds as `%N` + - While nanoseconds support in `date` is not in the POSIX 2017 standard, it is used only when `$DEBUG` is set in the environment and is available on the currently supported systems (FreeBSD, Void Linux) and on the next operating system with planned support (Debian). + - with `-r` for getting a modification date + - This feature is not specified on POSIX. So far it was tested on FreeBSD and Void Linux. - `env` at `/usr/bin/env` - While this may be an issue from a portability standpoint, hardcoding the path where `sh` is also poses another portability issue. A more robust way to find it would be desirable. diff --git a/docs/development/roadmap.md b/docs/development/roadmap.md index 5ce6573..e4ac795 100644 --- a/docs/development/roadmap.md +++ b/docs/development/roadmap.md @@ -1,5 +1,11 @@ To do: +- When deciding to install/uninstall packages, there should be the option to also add/remove them from the configuration, both interactively and non-interactively + +- Make configurable: + - authorizer command (sudo/doas/su) + - package cache update frequency (currently defaults to daily) + - Add a `--break` flag to the `log` function to print a newline before any output - Add this flag to the `log debug "user:\n$user_packages"` call on `scan_packages()` in `configuration.sh` diff --git a/docs/development/validate_input_packages.md b/docs/development/validate_input_packages.md new file mode 100644 index 0000000..881297e --- /dev/null +++ b/docs/development/validate_input_packages.md @@ -0,0 +1,28 @@ +This function takes a list of package names separated by spaces and verifies that: + +- Characters are only: +- The package is a valid package name + +## Character validation + +An OS-specific pattern will be matched against the package name to decide which characters are valid or invalid by the naming standards its package repository uses. + +If no OS-specific documentation is found on what are the allowed characters in package names, the obtained package list containing all available packages in the operating system's main repository will be analyzed to determine what is the current range of characters it uses. + +The following character ranges have been determined so far: + +- FreeBSD `pkg` + - uppercase and lowercase letters + - numbers + - dashes + - underscores + - dots + - plus signs + +## Package validation + +To determine if a package really exists, there must be a quick way to query a list of all available packages that does not mean individually making requests with each package name. + +If the package manager provides a way to fetch a list of all available packages, tori will cache this list for each execution on a given day. + +The user may also manually trigger a cache refresh through the command line interface using `tori cache`. diff --git a/docs/usage/cache.md b/docs/usage/cache.md new file mode 100644 index 0000000..6d01865 --- /dev/null +++ b/docs/usage/cache.md @@ -0,0 +1,9 @@ +tori caches your package repository's list of all available packages in order to check if a given package name really corresponds to a package name available to install or uninstall. + +This works by asking your package manager for this list and then storing it in the configured cache directory. It defaults to `~/.cache/tori/${OS}_packages.cache`. For example, on FreeBSD this will be `FreeBSD_packages.cache`. + +The modification date of this file is accessed every time tori is launched. By default, if it differs from the current day it will trigger a refresh. Note that this is not a comparison on whether 24 hours have passed since the last refresh. + +For this reason, tori may ask you for your password on startup with the message "Updating package cache". + +If you would like to manually refresh the cache, you can use the `tori cache` command. diff --git a/src/index.sh b/src/index.sh index c6ec381..247e978 100644 --- a/src/index.sh +++ b/src/index.sh @@ -1,6 +1,9 @@ . "$TORI_ROOT/src/check.sh" . "$TORI_ROOT/src/configuration.sh" -. "$TORI_ROOT/src/package/package_manager.sh" -. "$TORI_ROOT/src/package/package_conflict_resolution.sh" . "$TORI_ROOT/src/utility.sh" . "$TORI_ROOT/src/system.sh" + +. "$TORI_ROOT/src/package/package_manager.sh" +. "$TORI_ROOT/src/package/package_conflict_resolution.sh" +. "$TORI_ROOT/src/package/validate_input_packages.sh" +. "$TORI_ROOT/src/package/update_package_cache.sh" diff --git a/src/package/package_conflict_resolution.sh b/src/package/package_conflict_resolution.sh index 4d0b44e..df8fff7 100644 --- a/src/package/package_conflict_resolution.sh +++ b/src/package/package_conflict_resolution.sh @@ -1,53 +1,88 @@ resolve_packages() { local strategy= + local input_packages= - echo "$system_packages" > "$TMP_DIR/system_packages" - echo "$user_packages" > "$TMP_DIR/user_packages" + # shellcheck disable=SC2154 + ( echo "$system_packages" > "$TMP_DIR/system_packages" + echo "$user_packages" > "$TMP_DIR/user_packages" ) - local not_on_configuration="$(grep -v -x -f "$TMP_DIR/user_packages" "$TMP_DIR/system_packages" | xargs)" - local not_installed=$(grep -v -x -f "$TMP_DIR/system_packages" "$TMP_DIR/user_packages" | xargs) + local packages_not_on_configuration="$(grep -v -x -f \ + "$TMP_DIR/user_packages" "$TMP_DIR/system_packages" | xargs)" - if [ -n "$not_on_configuration" ]; then - - printf "\nInstalled packages not on configuration: %s\n" "$not_on_configuration" - echo " [1] Uninstall all" - echo " [2] Enter packages to uninstall" - echo " [3] Add all to configuration" - echo " [4] Enter packages to add to configuration" - echo " [5] Decide on editor" - echo " [6] Cancel" - - read -r -p "Choose an option [1-6]: " strategy - log debug "Input: strategy = $strategy" - - if [ -z "$strategy" ] || [ "$strategy" -eq 6 ]; then - log debug "[resolve_packages] User choice: Cancel or empty" - elif [ "$strategy" = 1 ]; then - package_manager uninstall "$not_on_configuration" - else - log debug "[resolve_packages] Unexpected input: $strategy" - fi + if [ -n "$packages_not_on_configuration" ]; then + not_on_configuration_dialog "$packages_not_on_configuration" fi - if [ -n "$not_installed" ]; then + local packages_not_installed=$(grep -v -x -f \ + "$TMP_DIR/system_packages" "$TMP_DIR/user_packages" | xargs) - printf "\nPackages on configuration but not installed: %s\n" "$not_installed" - echo " [1] Install all" - echo " [2] Enter packages to install" - echo " [3] Remove all from configuration" - echo " [4] Enter packages to remove from configuration" - echo " [5] Decide on editor" - echo " [6] Cancel" - - read -r -p "Choose an option [1-6]: " strategy - log debug "Input: strategy = $strategy" - - if [ -z "$strategy" ] || [ "$strategy" -eq 6 ]; then - log debug "[resolve_packages] User choice: Cancel or empty" - elif [ "$strategy" -eq 1 ]; then - package_manager install "$not_installed" - else - log debug "[resolve_packages] Unexpected input: $strategy" - fi + if [ -n "$packages_not_installed" ]; then + not_installed_dialog "$packages_not_installed" + fi +} + +not_on_configuration_dialog() { + local conflicted_packages="$1" + local input_packages= + + printf "\nInstalled packages not on configuration: %s\n" "$conflicted_packages" + echo " [1] Uninstall all" + echo " [2] Enter packages to uninstall" + echo " [3] Add all to configuration" + echo " [4] Enter packages to add to configuration" + echo " [5] Decide on editor" + echo " [6] Cancel" + + read -r -p "Choose an option [1-6]: " strategy + log debug "Input: strategy = $strategy" + + if [ "$strategy" = 6 ]; then + log debug "[resolve_packages] User choice: Cancel or empty" + elif [ "$strategy" = 1 ]; then + package_manager uninstall "$conflicted_packages" + elif [ "$strategy" = 2 ]; then + read -r -p "Enter packages to uninstall separated by spaces: " input_packages + log debug "Input: input_packages = $input_packages" + if validate_input_packages "$input_packages"; then + package_manager uninstall "$input_packages" + else + not_on_configuration_dialog "$conflicted_packages" + fi + else + log debug "[resolve_packages] Unexpected input: $strategy" + not_on_configuration_dialog "$conflicted_packages" + fi +} + +not_installed_dialog() { + local conflicted_packages="$1" + local input_packages= + + printf "\nPackages on configuration but not installed: %s\n" "$conflicted_packages" + echo " [1] Install all" + echo " [2] Enter packages to install" + echo " [3] Remove all from configuration" + echo " [4] Enter packages to remove from configuration" + echo " [5] Decide on editor" + echo " [6] Cancel" + + read -r -p "Choose an option [1-6]: " strategy + log debug "Input: strategy = $strategy" + + if [ "$strategy" = 6 ]; then + log debug "[resolve_packages] User choice: Cancel or empty" + elif [ "$strategy" = 1 ]; then + package_manager install "$conflicted_packages" + elif [ "$strategy" = 2 ]; then + read -r -p "Enter packages to install separated by spaces: " input_packages + log debug "Input: input_packages = $input_packages" + if validate_input_packages "$input_packages"; then + package_manager install "$input_packages" + else + not_on_configuration_dialog "$conflicted_packages" + fi + else + log debug "[resolve_packages] Unexpected input: $strategy" + not_installed_dialog "$conflicted_packages" fi } diff --git a/src/package/package_manager.sh b/src/package/package_manager.sh index 9701f56..2f99321 100644 --- a/src/package/package_manager.sh +++ b/src/package/package_manager.sh @@ -3,20 +3,23 @@ package_manager() { local output local manager - local authorizer="sudo" # TODO: make configurable + local authorizer="sudo" local args__install local args__uninstall local args__get_manually_installed + local args__get_available set_opts + local args__user_args="$2" set_opts - - if [ $OS = "FreeBSD" ]; then + if [ "$OS" = "FreeBSD" ]; then manager="pkg" args__get_manually_installed='query -e "%a = 0" "%n"' args__install='install' args__uninstall='delete' + args__update='update' + args__get_available="rquery -a '%n'" fi # shellcheck disable=SC2086 @@ -26,6 +29,10 @@ package_manager() { $authorizer $manager $args__install $args__user_args elif [ "$command" = 'uninstall' ]; then $authorizer $manager $args__uninstall $args__user_args + elif [ "$command" = 'update' ]; then + $authorizer $manager $args__update + elif [ "$command" = 'get_available' ]; then + eval $manager "$args__get_available" else log debug "[package_manager] Unexpected command: $command" fi diff --git a/src/package/update_package_cache.sh b/src/package/update_package_cache.sh new file mode 100644 index 0000000..1169508 --- /dev/null +++ b/src/package/update_package_cache.sh @@ -0,0 +1,19 @@ +update_package_cache() { + set_opts + + local argument="$1" + set_opts - + + if [ -f "$PACKAGE_CACHE" ]; then + local last_update="$(date -r "$PACKAGE_CACHE" +%Y-%m-%d)" + fi + + if ! [ -f "$PACKAGE_CACHE" ] || [ "$last_update" != "$(date -I)" ] || [ "$argument" = --force ]; then + log user 'Updating package cache' + if [ "$OS" = FreeBSD ]; then + package_manager update + fi + package_manager get_available > "$PACKAGE_CACHE" + else + log debug "Skipping package cache refresh: last updated $last_update" + fi +} diff --git a/src/package/validate_input_packages.sh b/src/package/validate_input_packages.sh new file mode 100644 index 0000000..55d8360 --- /dev/null +++ b/src/package/validate_input_packages.sh @@ -0,0 +1,22 @@ +validate_input_packages() { + local package_list="$1" + local invalid_characters_pattern= + + if [ "$OS" = FreeBSD ]; then + invalid_characters_pattern='[^A-Za-z0-9\+_\.-]' + fi + + echo "$package_list" | xargs | sed 's/ /\n/g' | while read -r package; do + + if echo "$package" | grep -q "$invalid_characters_pattern"; then + log user "Invalid package: $package (contains invalid characters)" + return 1 + fi + if grep -qw "$package" "$PACKAGE_CACHE"; then + log debug "Found in package cache: $package" + else + log user "Invalid package: $package (not found in package cache)" + return 1 + fi + done +} diff --git a/src/utility.sh b/src/utility.sh index d280751..3a36b69 100644 --- a/src/utility.sh +++ b/src/utility.sh @@ -1,16 +1,16 @@ # utility functions log() { - local level="$1" - local message="$2" + local level="$1" + local message="$2" - if [ $level = fatal ]; then - printf "[tori] $(date "+%H:%M:%S"): $message\n" 1>&2 - elif [ $level = user ]; then - printf "[tori] $(date "+%H:%M:%S"): $message\n" 1>&2 - elif [ -n "$DEBUG" ] && [ $level = debug ]; then - printf "$(date "+%H:%M:%N") $message\n" 1>&2 - fi + if [ $level = fatal ]; then + printf "[tori] $(date "+%H:%M:%S"): $message\n" 1>&2 + elif [ $level = user ]; then + printf "[tori] $(date "+%H:%M:%S"): $message\n" 1>&2 + elif [ -n "$DEBUG" ] && [ $level = debug ]; then + printf "$(date "+%H:%M:%N") $message\n" 1>&2 + fi } set_opts() { @@ -22,22 +22,27 @@ set_opts() { } prepare_directories() { - if ! [ -d "$TMP_DIR" ]; then - mkdir "$TMP_DIR" - fi + if ! [ -d "$TMP_DIR" ]; then + mkdir "$TMP_DIR" + fi - if ! [ -d "$CONFIG_ROOT" ]; then - log fatal "Configuration root not found at $CONFIG_ROOT" - exit 1 - fi + if ! [ -d "$CACHE_DIR" ]; then + mkdir -p "$CACHE_DIR" + fi + + if ! [ -d "$CONFIG_ROOT" ]; then + log fatal "Configuration root not found at $CONFIG_ROOT" + exit 1 + fi } print_help() { - printf "\n tori: configuration managent and system replication tool\n" - printf "\n Options:\n\n" - printf "\tcheck\t\tcompare configuration to system state\n" - printf "\n" - printf "\tversion\t\tprint current version with release date\n" - printf "\thelp\t\tshow this help text\n" - printf "\n See 'man tori' or https://brew.bsd.cafe/jutty/tori for more\n\n" + printf "\n tori: configuration managent and system replication tool\n" + printf "\n Options:\n\n" + printf "\tcheck\t\tcompare configuration to system state\n" + printf "\tcache\t\trefresh the local package cache\n" + printf "\n" + printf "\tversion\t\tprint current version with release date\n" + printf "\thelp\t\tshow this help text\n" + printf "\n See 'man tori' or https://brew.bsd.cafe/jutty/tori for more\n\n" } diff --git a/tori b/tori index e448c61..77a5a6b 100755 --- a/tori +++ b/tori @@ -2,10 +2,11 @@ main() { # paths - VERSION="0.2.1 2024-07-10" + VERSION="0.3.0 2024-07-11" TORI_ROOT="$HOME/.local/share/tori" CONFIG_ROOT="$HOME/.config/tori" TMP_DIR="/tmp/tori" + CACHE_DIR="$HOME/.cache/tori" check_core_paths @@ -23,6 +24,7 @@ main() { ## global constants OS="$(get_operating_system)" + PACKAGE_CACHE="$CACHE_DIR/${OS}_packages.cache" ## global state base_files= @@ -30,12 +32,16 @@ main() { user_packages= system_packages= - # entry point - + # startup checks prepare_directories + update_package_cache + + # entry point if [ "$argument" = check ]; then check + elif [ "$argument" = cache ]; then + update_package_cache --force elif [ "$argument" = version ] || [ "$argument" = -v ] || [ "$argument" = --version ]; then echo "$VERSION" elif [ "$argument" = help ] || [ "$argument" = -h ] || [ "$argument" = --help ]; then