From b9f9b1529fe6e6f0d20c3a6ebe3dcd4cf42b8728 Mon Sep 17 00:00:00 2001 From: loopyd Date: Wed, 27 Mar 2024 19:24:13 -0700 Subject: [PATCH 1/3] Competent build script --- build.sh | 300 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 277 insertions(+), 23 deletions(-) mode change 100644 => 100755 build.sh diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 5adaea6523e..7b0206de111 --- a/build.sh +++ b/build.sh @@ -1,36 +1,290 @@ -#!/bin/sh +#!/bin/bash +# @name FlatPakBuildScript +# @brief This script provides functionality to build and run this project as a flatpak package, from either a host, or a container environment. +# @example +# ./build.sh -run_flatpak() { - flatpak-spawn --host flatpak run org.flatpak.Builder build com.usebottles.bottles.yml --user --install --force-clean && flatpak-spawn --host flatpak run com.usebottles.bottles +set -eo pipefail + +# @description Colored output for error messages +# @arg $1 string Message +# @exitcode 0 +# @stderr Formatted message +function err() { + local C_RED_BOLD="\e[1;31m" + local C_RED="\e[0;31m" + local C_RESET="\e[0m" + local C_MESSAGE + C_MESSAGE="${1}" + echo -e "${C_RED_BOLD}ERROR: ${C_RESET}${C_RED}${C_MESSAGE}${C_RESET}" >&2 +} + +# @description Colored output for informational messages +# @arg $1 string Message +# @exitcode 0 +# @stdout Formatted message +function info() { + local C_BLUE="\e[0;34m" + local C_RESET="\e[0m" + local C_MESSAGE + C_MESSAGE="${1}" + echo -e "${C_BLUE}${C_MESSAGE}${C_RESET}" +} + +# @description Colored output for warning messages +# @arg $1 string Message +# @exitcode 0 +# @stdout Formatted message +function warning() { + local C_YELLOW_BOLD="\e[1;33m" + local C_YELLOW="\e[0;33m" + local C_RESET="\e[0m" + local C_MESSAGE + C_MESSAGE="${1}" + echo -e "${C_YELLOW_BOLD}WARNING: ${C_RESET}${C_YELLOW}${C_MESSAGE}${C_RESET}" } -run_host() { - flatpak run org.flatpak.Builder build com.usebottles.bottles.yml --user --install --force-clean && flatpak run com.usebottles.bottles +# @description Colored output for success messages +# @arg $1 string Message +# @exitcode 0 +# @stdout Formatted message +function success() { + local C_GREEN_BOLD="\e[1;32m" + local C_GREEN="\e[0;32m" + local C_RESET="\e[0m" + local C_MESSAGE + C_MESSAGE="${1}" + echo -e "${C_GREEN_BOLD}SUCCESS: ${C_RESET}${C_GREEN}${C_MESSAGE}${C_RESET}" +} + +# @description Trap function for error handling +# @arg $1 int Line number +# @arg $2 int Exit code +function error_trap() { + local line_number=$1 + local exit_code=$2 + error "Error at line ${line_number}; exit code: ${exit_code}" + exit ${exit_code} +} +trap 'error_trap ${LINENO} $?' ERR + +# @description Check if a command exists +# @arg $1 string Command name +# @exitcode 0 If the command exists +# @exitcode 1 If the command does not exist +function check_command() { + if ! command -v $1 &> /dev/null; then + return 1 + else + return 0 + fi +} + +# @description Execute a flatpak command in the appropriate environment +# @arg $1 string Command +# @exitcode 0 If the command is executed successfully +# @exitcode 1 If the command is not executed successfully +# @stderr Status of the function execution +function exec_flatpak() { + local command + command="${*}" + if [ -z "${command}" ]; then + err "No command provided" + return 1 + fi + local environment + if [ -f "/run/.containerenv" ]; then + environment="CONTAINER" + else + environment="HOST" + fi + info "[${environment}] Executing flatpak command: flatpak ${command}" + case "${environment}" in + HOST) + flatpak ${command} + return $? + ;; + CONTAINER) + if check_command "flatpak-spawn"; then + flatpak-spawn --host flatpak ${command} + return $? + elif check_command "host-spawn"; then + host-spawn flatpak ${command} + return $? + fi + ;; + *) + err "Invalid environment type provided (one of: HOST, CONTAINER)" + return 1 + ;; + esac +} + +# @description Check if a flatpak repository exists +# @arg $1 string Repository name +# @exitcode 0 If the repository exists +# @exitcode 1 If the repository does not exist +# @stderr Status of the function execution +function check_flatpak_repository() { + local repository + repository="${1}" + if [ -z "${repository}" ]; then + err "No repository provided" + return 1 + fi + local uri + uri="${2}" + if [ -z "${uri}" ]; then + err "No repository URI provided" + return 1 + fi + if ! exec_flatpak --user remote-list | grep -qi "${repository}"; then + warning "Flatpak repository ${repository} does not exist" + return 1 + fi + if ! exec_flatpak --user remote-list | grep -qi "${uri}"; then + warning "Flatpak repository ${repository} URI does not match" + return 1 + fi + info "Flatpak repository ${repository} is OK" + return 0 } -run_container() { - host-spawn flatpak run org.flatpak.Builder build com.usebottles.bottles.yml --user --install --force-clean && host-spawn flatpak run com.usebottles.bottles +# @description Check if a flatpak package is installed +# @arg $1 string Package name +# @exitcode 0 If the package is installed +# @exitcode 1 If the package is not installed +# @stderr Status of the function execution +function check_flatpak_package() { + local package + package="${1}" + if [ -z "${package}" ]; then + err "No package name provided" + return 1 + fi + if ! exec_flatpak --user list | grep -qi "${package}"; then + warning "Flatpak package ${package} is not installed" + return 1 + fi + info "Flatpak package ${package} is OK" + return 0 } -if [ -x "$(command -v flatpak-spawn)" ]; then - run_flatpak - exit $? -fi +# @description Install a flatpak repository +# @arg $1 string Repository name +# @arg $2 string Repository URI +# @exitcode 0 If the repository is installed +# @exitcode 1 If the repository is not installed +# @stderr Status of the function execution +function install_flatpak_repository() { + local repository + repository="${1}" + if [ -z "${repository}" ]; then + err "No repository provided" + return 1 + fi + local uri + uri="${2}" + if [ -z "${uri}" ]; then + err "No repository URI provided" + return 1 + fi + if check_flatpak_repository "${repository}" "${uri}"; then + return 0 + fi + info "Adding flatpak repository: ${repository}" + if ! exec_flatpak --user remote-add --if-not-exists ${repository} ${uri}; then + err "Failed to add flatpak repository ${repository}" + return 1 + else + success "Flatpak repository ${repository} added successfully" + return 0 + fi +} -if [ -f "/run/.containerenv" ]; then - if [ -x "$(command -v flatpak)" ]; then - run_host - exit $? +# @description Ensure a flatpak package is installed +# @arg $1 string Package name +# @exitcode 0 If the package is installed +# @exitcode 1 If the package is not installed +# @stderr Status of the function execution +function install_flatpak_package() { + local package + package="${1}" + if [ -z "${package}" ]; then + err "No package name provided" + return 1 + fi + if check_flatpak_package "${package}"; then + return 0 + fi + info "Installing flatpak package: ${package}" + if ! exec_flatpak install --user flathub "${package}" -y; then + err "Failed to install flatpak package ${package}" + return 1 + else + success "Flatpak package ${package} installed successfully" + return 0 fi +} - if [ -x "$(command -v host-spawn)" ]; then - run_container - exit $? +# @description Build a flatpak package +# @arg $1 string Package configuration file +# @exitcode 0 If the package is built successfully +# @exitcode 1 If the package is not built successfully +# @stderr Status of the function execution +function build_flatpak() { + local package_config + package_config="${1}" + if [ -z "${package_config}" ]; then + err "No package configuration file provided" + return 1 + fi + if [ ! -f "${package_config}" ]; then + err "Package configuration file: ${package_config} does not exist" + return 1 + fi + local environment + if [ -f "/run/.containerenv" ]; then + environment="CONTAINER" + else + environment="HOST" fi + # Ensure build dependencies are installed + install_flatpak_repository "flathub" "https://flathub.org/repo/flathub.flatpakrepo" + install_flatpak_package "org.flatpak.Builder" + info "Building flatpak package using configuration: ${package_config}..." + if ! exec_flatpak run org.flatpak.Builder --install --install-deps-from=flathub --default-branch=master --force-clean --user build-dir ${package_config}; then + err "Failed to build flatpak package" + return 1 + else + success "Flatpak package built successfully" + return 0 + fi +} - echo "Looks like you are running in a container, but you don't have flatpak or host-spawn installed." - echo "Nothing to do here." -fi +# @description Run a flatpak package +# @arg $1 string Package name +# @exitcode 0 If the package is run successfully +# @exitcode 1 If the package is not run successfully +# @stderr Status of the function execution +function run_flatpak() { + local package_name + package_name="${1}" + if [ -z "${package_name}" ]; then + err "No package name provided" + return 1 + fi + if ! check_flatpak_package "${package_name}"; then + return 1 + fi + if ! exec_flatpak run "${package_name}" ; then + err "Failed to test-run flatpak package" + return 1 + else + success "Flatpak package test-ran successfully" + return 0 + fi +} -run_host -exit $? +build_flatpak "com.usebottles.bottles.yml" +run_flatpak "com.usebottles.bottles" From 658dde6f7acd86900f3a33560ee7c1a09bce314c Mon Sep 17 00:00:00 2001 From: loopyd Date: Wed, 27 Mar 2024 21:20:38 -0700 Subject: [PATCH 2/3] Add command-line arguments This improvement adds command line arguments and help pages to the build script, extending its functionality. Arguments can now be passed through to the flatpak, to gain access to bottles-cli at runtime. Additionally, the script now has help pages: The help page can be accessed by running `./build.sh --help` or `./build.sh -h`. From there help on actions and options can be viewed by running `./build.sh --help. NOTE: This script is almost generic enough to be used for any flatpak builder project, but it is still tailored to bottles as the package name is hardcoded in the script. It does not fully support all arguments of flatpak itself (yet), but it is a good start. --- build.sh | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 202 insertions(+), 11 deletions(-) diff --git a/build.sh b/build.sh index 7b0206de111..615ab15dcaa 100755 --- a/build.sh +++ b/build.sh @@ -2,10 +2,27 @@ # @name FlatPakBuildScript # @brief This script provides functionality to build and run this project as a flatpak package, from either a host, or a container environment. # @example -# ./build.sh +# 1. This command will build, and run the flatpak package as the current user, and display the help message for the bottles-cli command: +# +# ./build.sh build -r -u -c bottles-cli -a --help +# +# 2. This command will run the flatpak package as the current user, and run bottles in GUI-interactive mode: +# +# ./build.sh run -r -u -c bottles +# +# 3. This command will display the help message for the build action: +# +# ./build.sh build -h set -eo pipefail +BUILD=false +RUN=false +USERMODE=false +AARGS=false +BARGS=() +COMMAND="" + # @description Colored output for error messages # @arg $1 string Message # @exitcode 0 @@ -68,6 +85,168 @@ function error_trap() { } trap 'error_trap ${LINENO} $?' ERR +# @description Display usage information +# @arg $1 string Action +# @exitcode 0 +# @stdout Usage information +function usage() { + local action + action="${1}" + action=$(echo "${action}" | tr '[:lower:]' '[:upper:]') + echo "Bottles Flatpak Build Script" + case "${action}" in + BUILD) + echo "Usage: $0 ACTION [OPTIONS] [ARGS]" + echo -e "\nHelp for build action:\n" + echo " This action builds the flatpak package" + echo -e "\nOptions:\n" + echo " -h, -help: Display this help message" + echo " -u, --user: Build the flatpak package as the current user" + echo " -r, --run: Run the flatpak package after building" + echo " -c, --command [COMMAND]: Pass a command to the run action" + echo " -a, --args [ARGS]: Pass additional arguments to the run action" + ;; + RUN) + echo "Usage: $0 ACTION [OPTIONS] [ARGS]" + echo -e "\nHelp for run action:\n" + echo " This action runs the flatpak package" + echo -e "\nOptions:\n" + echo " -h, --help: Display this help message" + echo " -u, --user: Run the flatpak package as the current user" + echo " -c, --command [COMMAND]: Pass a command to the run action" + echo " -a, --args [ARGS]: Pass additional arguments to the run action" + ;; + *) + echo "Usage: $0 ACTION [OPTIONS]" + echo -e "\nActions:\n" + echo " build: Build the flatpak package" + echo " run: Run the flatpak package" + echo -e "\nOptions:\n" + echo " -h, --help: Display this help message" + echo -e "\nYou can get help for a specific action by running: $0 ACTION -h|--help\n" + ;; + esac + exit 0 +} + +# @description Parse command line arguments +# @arg $@ string Command line arguments +# @exitcode 0 If the arguments are parsed successfully +# @stdout: Usage if the command line arguments fail to parse +function parse_args() { + if [ $# -eq 0 ]; then + usage + fi + if [ $# -lt 1 ]; then + error "No action provided" + usage + fi + local action + action="${1}" + if [ "${action}" = "-h" ] || [ "${action}" = "--help" ]; then + usage + fi + action=$(echo "${action}" | tr '[:lower:]' '[:upper:]') + shift 1 + case "${action}" in + BUILD) + BUILD=true + while [ $# -gt 0 ]; do + if [ "$AARGS" = true ]; then + BARGS+=("$1") + else + case "$1" in + -h|--help) + usage "BUILD" + ;; + -r|--run) + RUN=true + ;; + -u|--user) + USERMODE=true + ;; + -c|--command) + COMMAND="$2" + shift 1 + ;; + -a|--args) + AARGS=true + ;; + *) + error "Invalid argument: $1" + usage "BUILD" + ;; + esac + fi + shift 1 + done + ;; + RUN) + RUN=true + while [ $# -gt 0 ]; do + if [ "$AARGS" = true ]; then + BARGS+=("$1") + else + case "$1" in + -h|--help) + usage "RUN" + ;; + -r|--run) + RUN=true + ;; + -u|--user) + USERMODE=true + ;; + -c|--command) + COMMAND="$2" + shift 1 + ;; + -a | --args) + AARGS=true + ;; + *) + error "Invalid argument: $1" + usage "RUN" + ;; + esac + fi + shift 1 + done + ;; + *) + error "Invalid action: ${action}" + usage + ;; + esac +} + +# @description Get the user mode flag +# @exitcode 0 +# @stdout User mode flag +function __user_mode() { + if [ "${USERMODE}" = true ]; then + echo -ne "--user " + fi +} + +# @description Get the passed-through command line arguments +# @exitcode 0 +# @stdout Command line arguments +function __bargs() { + if [ ${#BARGS[@]} -gt 0 ]; then + echo -ne " ${BARGS[*]}" + fi +} + +# @description Get the passed-through command +# @exitcode 0 +# @stdout Command +function __command() { + if [ -n "${COMMAND}" ]; then + echo -ne "--command=${COMMAND} " + fi +} + # @description Check if a command exists # @arg $1 string Command name # @exitcode 0 If the command exists @@ -138,11 +317,13 @@ function check_flatpak_repository() { err "No repository URI provided" return 1 fi - if ! exec_flatpak --user remote-list | grep -qi "${repository}"; then + # shellcheck disable=SC2046 + if ! exec_flatpak $(__user_mode)remote-list | grep -qi "${repository}"; then warning "Flatpak repository ${repository} does not exist" return 1 fi - if ! exec_flatpak --user remote-list | grep -qi "${uri}"; then + # shellcheck disable=SC2046 + if ! exec_flatpak $(__user_mode)remote-list | grep -qi "${uri}"; then warning "Flatpak repository ${repository} URI does not match" return 1 fi @@ -162,7 +343,8 @@ function check_flatpak_package() { err "No package name provided" return 1 fi - if ! exec_flatpak --user list | grep -qi "${package}"; then + # shellcheck disable=SC2046 + if ! exec_flatpak $(__user_mode)list | grep -qi "${package}"; then warning "Flatpak package ${package} is not installed" return 1 fi @@ -193,7 +375,8 @@ function install_flatpak_repository() { return 0 fi info "Adding flatpak repository: ${repository}" - if ! exec_flatpak --user remote-add --if-not-exists ${repository} ${uri}; then + # shellcheck disable=SC2046 + if ! exec_flatpak $(__user_mode)remote-add --if-not-exists ${repository} ${uri}; then err "Failed to add flatpak repository ${repository}" return 1 else @@ -218,7 +401,8 @@ function install_flatpak_package() { return 0 fi info "Installing flatpak package: ${package}" - if ! exec_flatpak install --user flathub "${package}" -y; then + # shellcheck disable=SC2046 + if ! exec_flatpak install $(__user_mode)flathub "${package}" -y; then err "Failed to install flatpak package ${package}" return 1 else @@ -253,7 +437,8 @@ function build_flatpak() { install_flatpak_repository "flathub" "https://flathub.org/repo/flathub.flatpakrepo" install_flatpak_package "org.flatpak.Builder" info "Building flatpak package using configuration: ${package_config}..." - if ! exec_flatpak run org.flatpak.Builder --install --install-deps-from=flathub --default-branch=master --force-clean --user build-dir ${package_config}; then + # shellcheck disable=SC2046 + if ! exec_flatpak run org.flatpak.Builder --install --install-deps-from=flathub --default-branch=master --force-clean $(__user_mode) build-dir ${package_config}; then err "Failed to build flatpak package" return 1 else @@ -277,14 +462,20 @@ function run_flatpak() { if ! check_flatpak_package "${package_name}"; then return 1 fi - if ! exec_flatpak run "${package_name}" ; then + # shellcheck disable=SC2046 + if ! exec_flatpak run $(__command)$(__user_mode)"${package_name}"$(__bargs); then err "Failed to test-run flatpak package" return 1 else - success "Flatpak package test-ran successfully" + success "Flatpak package ran successfully" return 0 fi } -build_flatpak "com.usebottles.bottles.yml" -run_flatpak "com.usebottles.bottles" +parse_args "$@" +if [ "${BUILD}" = true ]; then + build_flatpak "com.usebottles.bottles.yml" +fi +if [ "${RUN}" = true ]; then + run_flatpak "com.usebottles.bottles" +fi From 3ac191317b6addf4b90177e54dee89905ea8df84 Mon Sep 17 00:00:00 2001 From: loopyd Date: Thu, 28 Mar 2024 16:27:33 -0700 Subject: [PATCH 3/3] Complex -f in ``list``, and additions This commit adds complex filters to the ``-f|--filter`` parameter of the list action in bottles-cli. The expanded functionality allows you to filter by multiple fields and values, and to use glob patterns to match. Additionally, added the missing ability to ``list dependencies``, and migrated the programs action over to the list action, where the complex filter ``bottle:`` can be used to target listing programs only found in a single bottle (or multiple with glob match) --- bottles/backend/managers/manager.py | 12 +- bottles/backend/state.py | 2 + bottles/backend/utils/generic.py | 66 ++++++++ bottles/frontend/cli/cli.py | 246 +++++++++++++++++++++++----- build.sh | 2 +- 5 files changed, 282 insertions(+), 46 deletions(-) diff --git a/bottles/backend/managers/manager.py b/bottles/backend/managers/manager.py index 5f99fb67827..f25684faed1 100644 --- a/bottles/backend/managers/manager.py +++ b/bottles/backend/managers/manager.py @@ -118,7 +118,7 @@ def __init__( self.is_cli = is_cli self.settings = g_settings or GSettingsStub self.utils_conn = ConnectionUtils( - force_offline=self.is_cli or self.settings.get_boolean("force-offline") + force_offline=self.settings.get_boolean("force-offline") ) self.data_mgr = DataManager() _offline = True @@ -141,7 +141,7 @@ def __init__( if self.repository_manager.aborted_connections > 0: self.utils_conn.status = False _offline = True - + times["RepositoryManager"] = time.time() self.versioning_manager = VersioningManager(self) times["VersioningManager"] = time.time() @@ -153,10 +153,10 @@ def __init__( self.steam_manager = SteamManager() times["SteamManager"] = time.time() - if not self.is_cli: - times.update(self.checks(install_latest=False, first_run=True).data) - else: + if self.is_cli is True: logging.set_silent() + + times.update(self.checks(install_latest=False, first_run=True).data) if "BOOT_TIME" in os.environ: _temp_times = times.copy() @@ -956,6 +956,8 @@ def process_bottle(bottle): ): self.steam_manager.update_bottles() self.local_bottles.update(self.steam_manager.list_prefixes()) + + EventManager.done(Events.BottlesFetching) # Update parameters in bottle config def update_config( diff --git a/bottles/backend/state.py b/bottles/backend/state.py index d07150b5189..9e76b9317b6 100644 --- a/bottles/backend/state.py +++ b/bottles/backend/state.py @@ -19,9 +19,11 @@ class Events(Enum): ComponentsFetching = "components.fetching" DependenciesFetching = "dependencies.fetching" InstallersFetching = "installers.fetching" + BottlesFetching = "bottles.fetching" ComponentsOrganizing = "components.organizing" DependenciesOrganizing = "dependencies.organizing" InstallersOrganizing = "installers.organizing" + BottlesOrganizing = "bottles.organizing" class Signals(Enum): diff --git a/bottles/backend/utils/generic.py b/bottles/backend/utils/generic.py index a6b607cbd05..52163d45a42 100644 --- a/bottles/backend/utils/generic.py +++ b/bottles/backend/utils/generic.py @@ -91,6 +91,7 @@ def is_glibc_min_available(): def sort_by_version(_list: list, extra_check: str = "async"): + """Sort a list of strings by version.""" def natural_keys(text): result = [int(re.search(extra_check, text) is None)] result.extend( @@ -112,6 +113,71 @@ def get_mime(path: str): def random_string(length: int): + """Generate a random string of given length.""" return "".join( random.choice(string.ascii_uppercase + string.digits) for _ in range(length) ) + +def glob_to_re(pat: str) -> re.Pattern[str]: + """Convert a glob (UNIX-like) match pattern to a regular expression.""" + i, n = 0, len(pat) + res = '' + while i < n: + c = pat[i] + i = i+1 + if c == '*': + j = i + if j < n and pat[j] == '*': + res = res + '.*' + i = j+1 + else: + res = res + '[^/]*' + elif c == '?': + res = res + '[^/]' + elif c == '[': + j = i + if j < n and pat[j] == '!': + j = j+1 + if j < n and pat[j] == ']': + j = j+1 + while j < n and pat[j] != ']': + j = j+1 + if j >= n: + res = res + '\\[' + else: + stuff = pat[i:j] + if '--' not in stuff: + stuff = stuff.replace('\\', r'\\') + else: + chunks = [] + k = i+2 if pat[i] == '!' else i+1 + while True: + k = pat.find('-', k, j) + if k < 0: + break + chunks.append(pat[i:k]) + i = k+1 + k = k+3 + chunks.append(pat[i:j]) + stuff = '-'.join(s.replace('\\', r'\\').replace('-', r'\-') + for s in chunks) + stuff = re.sub(r'([&~|])', r'\\\1', stuff) + i = j+1 + if stuff[0] == '!': + stuff = '^/' + stuff[1:] + elif stuff[0] in ('^', '['): + stuff = '\\' + stuff + res = '%s[%s]' % (res, stuff) + else: + res = res + re.escape(c) + return re.compile(r'(?s:%s)\Z' % res) + + +def glob_filter(objects: list | dict | str, pattern: str) -> list | dict | str | None: + """Filter objects by a glob (UNIX-like) pattern.""" + if isinstance(objects, dict): + return {k: v for k, v in objects.items() if re.match(glob_to_re(pattern), k)} + elif isinstance(objects, list): + return [i for i in objects if re.match(glob_to_re(pattern), i)] + elif isinstance(objects, str): + return objects if re.match(glob_to_re(pattern), objects) else None diff --git a/bottles/frontend/cli/cli.py b/bottles/frontend/cli/cli.py index ce27c615fed..e728ab0bd6e 100644 --- a/bottles/frontend/cli/cli.py +++ b/bottles/frontend/cli/cli.py @@ -19,12 +19,12 @@ import argparse +from math import e import os import signal import sys import uuid import warnings - import gi warnings.filterwarnings("ignore") # suppress GTK warnings @@ -44,6 +44,8 @@ from bottles.backend.globals import Paths from bottles.backend.health import HealthChecker from bottles.backend.managers.manager import Manager +from bottles.backend.state import Events, EventManager +from bottles.backend.utils.generic import glob_filter from bottles.backend.models.config import BottleConfig from bottles.backend.wine.cmd import CMD from bottles.backend.wine.control import Control @@ -78,12 +80,9 @@ def __init__(self): info_parser = subparsers.add_parser("info", help="Show information about Bottles") info_parser.add_argument('type', choices=['bottles-path', 'health-check'], help="Type of information") - list_parser = subparsers.add_parser("list", help="List entities") - list_parser.add_argument('type', choices=['bottles', 'components'], help="Type of entity") - list_parser.add_argument("-f", "--filter", help="Filter bottles and components (e.g. '-f 'environment:gaming')") - - programs_parser = subparsers.add_parser("programs", help="List programs") - programs_parser.add_argument("-b", "--bottle", help="Bottle name", required=True) + list_parser = subparsers.add_parser("list", help="List Entities", description="You can specify multiple filters, depending on the entity type, seperated by a comma (,). Glob expressions are also supported for fuzzy UNIX-like matching in your filter.\n\nFor example, if you'd like to filter a bottle by its name, and then by its windows version, use:\n\n -f name:mybottle*,winversion:win1*\n\nWhich should show you Windows 10, and Windows 11 variants of your bottle.") + list_parser.add_argument('type', choices=['bottles', 'components', 'dependencies', 'programs'], help="Type of entity") + list_parser.add_argument("-f", "--filter", help="Apply a complex filter to the results (e.g. '-f 'environment:gaming,arch:win64,name:mydep* ...')") add_parser = subparsers.add_parser("add", help="Add program") add_parser.add_argument("-b", "--bottle", help="Bottle name", required=True) @@ -169,10 +168,10 @@ def __process_args(self): self.list_bottles(c_filter=_filter) elif _type == "components": self.list_components(c_filter=_filter) - - # PROGRAMS parser - elif self.args.command == "programs": - self.list_programs() + elif _type == "dependencies": + self.list_dependencies(c_filter=_filter) + elif _type == "programs": + self.list_programs(c_filter=_filter) # TOOLS parser elif self.args.command == "tools": @@ -227,31 +226,65 @@ def show_info(self): # region LIST def list_bottles(self, c_filter=None): + """List bottles with optional complex filters.""" mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_bottles() - bottles = mng.local_bottles - if c_filter and c_filter.startswith("environment:"): - environment = c_filter.split(":")[1].lower() - bottles = [name for name, bottle in bottles.items() if bottle.Environment.lower() == environment] + connected = mng.utils_conn.status + if connected is False: + sys.stderr.write("No internet connection\n") + exit(1) + + EventManager.wait(Events.BottlesFetching) + bottles = mng.local_bottles + if c_filter is not None: + for a_filter in c_filter.split(","): + if a_filter.startswith("arch:"): + arch = str(a_filter.split(":")[1]).lower() + bottles = {name: bottle for name, bottle in bottles.items() + if glob_filter(bottle.Arch.lower(), arch)} + elif a_filter.startswith("environment:"): + environment = str(a_filter.split(":")[1]).lower() + bottles = {name: bottle for name, bottle in bottles.items() + if glob_filter(bottle.Environment.lower(), environment)} + elif a_filter.startswith("runner:"): + runner = str(a_filter.split(":")[1]).lower() + bottles = {name: bottle for name, bottle in bottles.items() + if glob_filter(bottle.Runner.lower(), runner)} + elif a_filter.startswith("winversion:"): + winversion = str(a_filter.split(":")[1]).lower() + bottles = {name: bottle for name, bottle in bottles.items() + if glob_filter(bottle.Windows.lower(), winversion)} + elif a_filter.startswith("name:"): + name = str(a_filter.split(":")[1]).lower() + bottles = {k: bottle for k, bottle in bottles.items() + if glob_filter(bottle.Name, name)} + else: + sys.stderr.write(f"Invalid filter: {a_filter}\n") + exit(1) + + EventManager.done(Events.BottlesOrganizing) + if self.args.json: sys.stdout.write(json.dumps(bottles)) exit(0) if len(bottles) > 0: - sys.stdout.write(f"Found {len(bottles)} bottles:\n") + sys.stdout.write(f"Found {len(bottles)} bottle(s):\n") for b in bottles: sys.stdout.write(f"- {b}\n") + def list_components(self, c_filter=None): + """List components with optional complex filters.""" mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_runners(False) - mng.check_dxvk(False) - mng.check_vkd3d(False) - mng.check_nvapi(False) - mng.check_latencyflex(False) + connected = mng.utils_conn.status + if connected is False: + sys.stderr.write("No internet connection\n") + exit(1) + + EventManager.wait(Events.ComponentsOrganizing) components = { "runners": mng.runners_available, "dxvk": mng.dxvk_available, @@ -260,44 +293,177 @@ def list_components(self, c_filter=None): "latencyflex": mng.latencyflex_available } - if c_filter and c_filter.startswith("category:"): - category = c_filter.split(":")[1].lower() - if category in components: - components = {category: components[category]} + # NEW: Apply multiple filters, with glob expression support + if c_filter is not None: + for a_filter in c_filter.split(",") if len(c_filter.split(",")) > 0 else [c_filter]: + if a_filter.startswith("category:"): + category = str(a_filter.split(":")[1]).lower() + components = {k: v for k, v in components.items() + if glob_filter(k.lower(), category) is not None} + elif a_filter.startswith("name:"): + name = str(a_filter.split(":")[1]).lower() + # NOTE: This has to be done because the components dict contains lists of strings or bools. + # If you want a less messy solutiion, commit one. + components = {k: glob_filter([vv for vv in v if not isinstance(v, bool) and len(v) > 0], name) for k, v in components.items() + if len(glob_filter([vv for vv in v if not isinstance(v, bool) and len(v) > 0], name)) > 0} + else: + sys.stderr.write(f"Invalid filter: {a_filter}\n") + exit(1) if self.args.json: sys.stdout.write(json.dumps(components)) exit(0) for c in components: - sys.stdout.write(f"Found {len(components[c])} {c}\n") + sys.stdout.write(f"Found {len(components[c])} {c}:\n") for i in components[c]: sys.stdout.write(f"- {i}\n") + + + def list_dependencies(self, c_filter=None): + """List dependencies with optional complex filters.""" + mng = Manager(g_settings=self.settings, is_cli=True) + + connected = mng.utils_conn.status + if connected is False: + sys.stderr.write("No internet connection\n") + exit(1) + + EventManager.wait(Events.DependenciesOrganizing) + catalog = mng.supported_dependencies + if len(catalog) == 0: + exit(1) + + # NEW: Apply multiple filters, with glob expression support + if c_filter is not None: + for a_filter in c_filter.split(",") if len(c_filter.split(",")) > 0 else [c_filter]: + if a_filter.startswith("arch:"): + arch = str(a_filter.split(":")[1]).lower() + catalog = dict(sorted( + {name: item for name, item in catalog.items() + if glob_filter(str(catalog[name].get('Arch', 'win64')).lower(), arch.lower()) is not None}.items(), + reverse=False + )) + catalog = dict(sorted(catalog.items())) + elif a_filter.startswith("category:"): + category = str(a_filter.split(":")[1]).lower() + catalog = dict(sorted( + {name: item for name, item in catalog.items() + if glob_filter(str(catalog[name].get('Category', 'uncategorized')).lower(), category.lower())}.items(), + reverse=False + )) + catalog = dict(sorted(catalog.items())) + elif a_filter.startswith("description:"): + description = str(a_filter.split(":")[1]).lower() + catalog = dict(sorted( + {name: item for name, item in catalog.items() + if glob_filter(str(catalog[name].get('Description', '')).lower(), description.lower())}.items(), + reverse=False + )) + catalog = dict(sorted(catalog.items())) + elif a_filter.startswith("name:"): + name = str(a_filter.split(":")[1]).lower() + catalog = dict(sorted( + {name: item for name, item in catalog.items() + if glob_filter(str(name).lower(), name)}.items(), + reverse=False + )) + catalog = dict(sorted(catalog.items())) + else: + sys.stderr.write(f"Invalid filter: {a_filter}\n") + exit(1) + + if self.args.json: + sys.stdout.write(json.dumps(catalog)) + exit(0) + + for cat in sorted(set([catalog[c]['Category'] for c in catalog])): + ccs = [c for c in catalog if catalog[c]['Category'] == cat] + cc_len = len(ccs) + sys.stdout.write(f"Found {cc_len} {cat}:\n") + for cc in ccs: + cc_name = str(cc).ljust(20) + cc_desc = str(catalog[cc]['Description']).ljust(50) + sys.stdout.write(f"- {cc_name} {cc_desc}\n") # endregion # region PROGRAMS - def list_programs(self): + def list_programs(self, c_filter=None): + """List programs with optional complex filters.""" mng = Manager(g_settings=self.settings, is_cli=True) - mng.check_bottles() - _bottle = self.args.bottle - - if _bottle not in mng.local_bottles: - sys.stderr.write(f"Bottle {_bottle} not found\n") + + connected = mng.utils_conn.status + if connected is False: + sys.stderr.write("No internet connection\n") exit(1) + + EventManager.wait(Events.BottlesFetching) + bottles = mng.local_bottles - bottle = mng.local_bottles[_bottle] - programs = mng.get_programs(bottle) - programs = [p for p in programs if not p.get("removed", False)] + w_filters = None + if c_filter is not None: + c_filters = c_filter.split(",") if len(c_filter.split(",")) > 0 else [c_filter] + w_filters = c_filters.copy() if len(c_filters) > 0 else [] + for a_filter in c_filters: + if a_filter.startswith("arch:"): + arch = str(a_filter.split(":")[1]).lower() + bottles = {name: bottle for name, bottle in bottles.items() + if glob_filter(bottle.Arch.lower(), arch)} + w_filters.remove(a_filter) + elif a_filter.startswith("environment:"): + environment = str(a_filter.split(":")[1]).lower() + bottles = {name: bottle for name, bottle in bottles.items() + if glob_filter(bottle.Environment.lower(), environment)} + w_filters.remove(a_filter) + elif a_filter.startswith("runner:"): + runner = str(a_filter.split(":")[1]).lower() + bottles = {name: bottle for name, bottle in bottles.items() + if glob_filter(bottle.Runner.lower(), runner)} + w_filters.remove(a_filter) + elif a_filter.startswith("winversion:"): + winversion = str(a_filter.split(":")[1]).lower() + bottles = {name: bottle for name, bottle in bottles.items() + if glob_filter(bottle.Windows.lower(), winversion)} + w_filters.remove(a_filter) + elif a_filter.startswith("bottle:"): + name = str(a_filter.split(":")[1]).lower() + bottles = {k: bottle for k, bottle in bottles.items() + if glob_filter(bottle.Name, name)} + w_filters.remove(a_filter) + else: + continue + + EventManager.done(Events.BottlesOrganizing) + + programs = {name: [p for p in mng.get_programs(bottle) if not p.get("removed", False)] for name, bottle in bottles.items()} + if w_filters is not None: + for a_filter in w_filters: + if a_filter.startswith("name:"): + name = str(a_filter.split(":")[1]).lower() + programs = {k: [p for p in v if glob_filter(p.get("name", "untitled").lower(), name)] for k, v in programs.items()} + if a_filter.startswith("path:"): + path = str(a_filter.split(":")[1]).lower() + programs = {k: [p for p in v if glob_filter(p.get("path", "").lower(), path)] for k, v in programs.items()} + if a_filter.startswith("executable:"): + executable = str(a_filter.split(":")[1]).lower() + programs = {k: [p for p in v if glob_filter(p.get("executable", "program.exe").lower(), executable)] for k, v in programs.items()} + else: + sys.stderr.write(f"Invalid filter: {a_filter}\n") + exit(1) if self.args.json: sys.stdout.write(json.dumps(programs)) exit(0) - if len(programs) > 0: - sys.stdout.write(f"Found {len(programs)} programs:\n") - for p in programs: - sys.stdout.write(f"- {p['name']}\n") + if len(programs.keys()) > 0: + for p, v in programs.items(): + sys.stdout.write(f"Found {len(programs[p])} program(s) in {p}:\n") + for i in v: + c_name = str(i.get("name", "untitled")).ljust(16) + c_exec = str(i.get("executable", "program.exe")).ljust(20) + c_path = str(i.get("path", "")).ljust(60) + sys.stdout.write(f"- {c_name} {c_exec} {c_path}\n") # endregion diff --git a/build.sh b/build.sh index 615ab15dcaa..857b3e91549 100755 --- a/build.sh +++ b/build.sh @@ -80,7 +80,7 @@ function success() { function error_trap() { local line_number=$1 local exit_code=$2 - error "Error at line ${line_number}; exit code: ${exit_code}" + err "Error at line ${line_number}; exit code: ${exit_code}" exit ${exit_code} } trap 'error_trap ${LINENO} $?' ERR