Implement package conflict resolution by input

This commit is contained in:
Juno Takano 2024-07-11 12:03:03 -03:00
parent 2e545832a8
commit ddf9bc6abc
11 changed files with 225 additions and 81 deletions

View file

@ -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 <prompt> <variable_with_user_input>` syntax
- `printf`
- `echo`
- utilities
- `local`
- `read` with `read -r -p <prompt> <variable_with_user_input>` syntax
- `mkdir` with `-pggjj
- `find`
- `grep`
- `sed`
- `xargs`
- `uname`
- `date` with nanoseconds as `%N`
- `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.

View file

@ -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`

View file

@ -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`.

9
docs/usage/cache.md Normal file
View file

@ -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.

View file

@ -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"

View file

@ -1,15 +1,31 @@
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
if [ -n "$packages_not_on_configuration" ]; then
not_on_configuration_dialog "$packages_not_on_configuration"
fi
printf "\nInstalled packages not on configuration: %s\n" "$not_on_configuration"
local packages_not_installed=$(grep -v -x -f \
"$TMP_DIR/system_packages" "$TMP_DIR/user_packages" | xargs)
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"
@ -20,18 +36,29 @@ resolve_packages() {
read -r -p "Choose an option [1-6]: " strategy
log debug "Input: strategy = $strategy"
if [ -z "$strategy" ] || [ "$strategy" -eq 6 ]; then
if [ "$strategy" = 6 ]; then
log debug "[resolve_packages] User choice: Cancel or empty"
elif [ "$strategy" = 1 ]; then
package_manager uninstall "$not_on_configuration"
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
fi
}
if [ -n "$not_installed" ]; then
not_installed_dialog() {
local conflicted_packages="$1"
local input_packages=
printf "\nPackages on configuration but not installed: %s\n" "$not_installed"
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"
@ -42,12 +69,20 @@ resolve_packages() {
read -r -p "Choose an option [1-6]: " strategy
log debug "Input: strategy = $strategy"
if [ -z "$strategy" ] || [ "$strategy" -eq 6 ]; then
if [ "$strategy" = 6 ]; then
log debug "[resolve_packages] User choice: Cancel or empty"
elif [ "$strategy" -eq 1 ]; then
package_manager install "$not_installed"
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"
fi
not_installed_dialog "$conflicted_packages"
fi
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -26,6 +26,10 @@ prepare_directories() {
mkdir "$TMP_DIR"
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
@ -36,6 +40,7 @@ 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 "\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"

12
tori
View file

@ -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