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 old mode 100644 new mode 100755 index 5adaea6523e..857b3e91549 --- a/build.sh +++ b/build.sh @@ -1,36 +1,481 @@ -#!/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 +# 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 -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 + +BUILD=false +RUN=false +USERMODE=false +AARGS=false +BARGS=() +COMMAND="" + +# @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 } -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 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}" } -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 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}" } -if [ -x "$(command -v flatpak-spawn)" ]; then - run_flatpak - exit $? -fi +# @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 + err "Error at line ${line_number}; exit code: ${exit_code}" + exit ${exit_code} +} +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 +} -if [ -f "/run/.containerenv" ]; then - if [ -x "$(command -v flatpak)" ]; then - run_host - exit $? +# @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 +} - if [ -x "$(command -v host-spawn)" ]; then - run_container - exit $? +# @description Get the user mode flag +# @exitcode 0 +# @stdout User mode flag +function __user_mode() { + if [ "${USERMODE}" = true ]; then + echo -ne "--user " 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 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 +# @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 + # shellcheck disable=SC2046 + if ! exec_flatpak $(__user_mode)remote-list | grep -qi "${repository}"; then + warning "Flatpak repository ${repository} does not exist" + return 1 + fi + # 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 + info "Flatpak repository ${repository} is OK" + return 0 +} + +# @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 + # shellcheck disable=SC2046 + if ! exec_flatpak $(__user_mode)list | grep -qi "${package}"; then + warning "Flatpak package ${package} is not installed" + return 1 + fi + info "Flatpak package ${package} is OK" + return 0 +} -run_host -exit $? +# @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}" + # 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 + success "Flatpak repository ${repository} added successfully" + return 0 + fi +} + +# @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}" + # shellcheck disable=SC2046 + if ! exec_flatpak install $(__user_mode)flathub "${package}" -y; then + err "Failed to install flatpak package ${package}" + return 1 + else + success "Flatpak package ${package} installed successfully" + return 0 + fi +} + +# @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}..." + # 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 + success "Flatpak package built successfully" + return 0 + 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 + # 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 ran successfully" + return 0 + fi +} + +parse_args "$@" +if [ "${BUILD}" = true ]; then + build_flatpak "com.usebottles.bottles.yml" +fi +if [ "${RUN}" = true ]; then + run_flatpak "com.usebottles.bottles" +fi