diff --git a/CHANGELOG.md b/CHANGELOG.md index 73989fa3..5b61a998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.9.23] 2021-12-10 +### Features +- General + - new configuration file `/etc/bauh/gems.forbidden` can be used by system administrators/distribution package managers to disable the management of supported packaging formats globally (more on that [here](https://github.com/vinifmor/bauh#forbidden_gems)) + +- Arch + - allowing AUR packages to be installed when bauh is launched by the root user [#196](https://github.com/vinifmor/bauh/issues/196) + - it creates a non-root user called **bauh-aur** for building the packages (`useradd` and `runuser` commands must be installed) + +### Improvements +- code refactoring (String formatting method) + +### Fixes +- Arch + - not updating the view (GUI) status correctly after uninstalling a package whose PKGBUILD file was edited (specifically the **name**) + - missing error handling when hard requirements for optional dependencies cannot be found by pacman + + ## [0.9.22] 2021-11-30 ### Improvements - General diff --git a/README.md b/README.md index 503bb38a..4eeb4b29 100644 --- a/README.md +++ b/README.md @@ -15,22 +15,23 @@ Key features ## Index -1. [Installation](#installation) +1. [Installation](#installation) - [Ubuntu-based distros (20.04)](#inst_ubuntu) - [Arch-based distros](#inst_arch) -2. [Isolated installation](#inst_iso) -3. [Desktop entry / menu shortcut](#desk_entry) -4. [Autostart: tray mode](#autostart) -5. [Distribution](#dist) -6. [Supported types](#types) +2. [Isolated installation](#inst_iso) +3. [Desktop entry / menu shortcut](#desk_entry) +4. [Autostart: tray mode](#autostart) +5. [Distribution](#dist) +6. [Supported types](#types) - [AppImage](#type_appimage) - [Arch packages/AUR](#type_arch) - [Flatpak](#type_flatpak) - [Snap](#type_snap) - [Native Web applications](#type_web) -7. [General settings](#settings) -8. [Directory structure, caching and logs](#dirs) -9. [Custom themes](#custom_themes) +7. [General settings](#settings) + - [Forbidden packaging formats](#forbidden_gems) +8. [Directory structure, caching and logs](#dirs) +9. [Custom themes](#custom_themes) 10. [Tray icons](#tray_icons) 11. [CLI (Command Line Interface)](#cli) 12. [Improving performance](#performance) @@ -240,6 +241,9 @@ suggestions: - If you have AUR added as a repository on you pacman configuration, make sure to disable bauh's support (through the settings described below) - AUR package compilation may require additional installed packages to work properly. Some of them are defined on the field `optdepends` of the [PKGBUILD](https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=bauh) + - for a **root** user the following additional applications must be installed: + - `useradd`: required to create a simple user named **bauh-aur** (since **makepkg** does not allow building packages as the **root** user) + - `runuser`: required to run commands as another user - **Repository packages currently do not support the following actions: Downgrade and History** - If some of your installed packages are not categorized, open a PullRequest to the **bauh-files** repository changing [categories.txt](https://github.com/vinifmor/bauh-files/blob/master/arch/categories.txt) - During bauh initialization a full AUR normalized index is saved at `~/.cache/bauh/arch/aur/index.txt` @@ -410,6 +414,16 @@ boot: load_apps: true # if the installed applications or suggestions should be loaded on the management panel after the initialization process. Default: true. ``` +##### Forbidden packaging formats +- System administrators and package managers of Linux distributions can disable the usage/management of supported packaging formats +by adding their ids to the file `/etc/bauh/gems.forbidden`. This will prevent their management code to be loaded. +- Example (one id per line): +``` +arch +appimage +# flatpak # 'sharps' can be used to ignore a given line (comment) +``` + #### Directory structure, caching and logs - `~/.config/bauh` (or `/etc/bauh` for **root**): stores configuration files - `~/.cache/bauh` (or `/var/cache/bauh` for **root**): stores data about your installed applications, databases, indexes, etc. Files are stored here to provide a faster initialization and data recovery. diff --git a/bauh/__init__.py b/bauh/__init__.py index 74d7da2c..606a7fd6 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.9.22' +__version__ = '0.9.23' __app_name__ = 'bauh' import os diff --git a/bauh/api/paths.py b/bauh/api/paths.py index fd7b5deb..a27f32a8 100644 --- a/bauh/api/paths.py +++ b/bauh/api/paths.py @@ -4,11 +4,16 @@ from bauh import __app_name__ from bauh.api import user + +def get_temp_dir(username: str) -> str: + return f'/tmp/{__app_name__}@{username}' + + CACHE_DIR = f'/var/cache/{__app_name__}' if user.is_root() else f'{Path.home()}/.cache/{__app_name__}' CONFIG_DIR = f'/etc/{__app_name__}' if user.is_root() else f'{Path.home()}/.config/{__app_name__}' USER_THEMES_DIR = f'/usr/share/{__app_name__}/themes' if user.is_root() else f'{Path.home()}/.local/share/{__app_name__}/themes' DESKTOP_ENTRIES_DIR = '/usr/share/applications' if user.is_root() else f'{Path.home()}/.local/share/applications' -TEMP_DIR = f'/tmp/{__app_name__}@{getuser()}' +TEMP_DIR = get_temp_dir(getuser()) LOGS_DIR = f'{TEMP_DIR}/logs' AUTOSTART_DIR = f'/etc/xdg/autostart' if user.is_root() else f'{Path.home()}/.config/autostart' BINARIES_DIR = f'/usr/local/bin' if user.is_root() else f'{Path.home()}/.local/bin' diff --git a/bauh/cli/app.py b/bauh/cli/app.py index d5ffd7fd..468da1c3 100644 --- a/bauh/cli/app.py +++ b/bauh/cli/app.py @@ -49,7 +49,8 @@ def main(): internet_checker=InternetChecker(offline=False), root_user=user.is_root()) - managers = gems.load_managers(context=context, locale=i18n.current_key, config=app_config, default_locale=DEFAULT_I18N_KEY) + managers = gems.load_managers(context=context, locale=i18n.current_key, config=app_config, + default_locale=DEFAULT_I18N_KEY, logger=logger) cli = CLIManager(GenericSoftwareManager(managers, context=context, config=app_config)) diff --git a/bauh/commons/system.py b/bauh/commons/system.py index eff0068b..9aa3e4f4 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -67,12 +67,15 @@ class SimpleProcess: def __init__(self, cmd: List[str], cwd: str = '.', expected_code: int = 0, global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG, root_password: str = None, extra_paths: Set[str] = None, error_phrases: Set[str] = None, wrong_error_phrases: Set[str] = None, - shell: bool = False, - success_phrases: Set[str] = None, extra_env: Optional[Dict[str, str]] = None): + shell: bool = False, success_phrases: Set[str] = None, extra_env: Optional[Dict[str, str]] = None, + custom_user: Optional[str] = None): pwdin, final_cmd = None, [] self.shell = shell - if root_password is not None: + + if custom_user: + final_cmd.extend(['runuser', '-u', custom_user, '--']) + elif root_password is not None: final_cmd.extend(['sudo', '-S']) pwdin = self._new(['echo', root_password], cwd, global_interpreter, lang).stdout @@ -219,14 +222,10 @@ def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, print_error: bool = True, - cwd: str = '.', global_interpreter: bool = USE_GLOBAL_INTERPRETER, extra_paths: Set[str] = None) -> str: + cwd: str = '.', global_interpreter: bool = USE_GLOBAL_INTERPRETER, extra_paths: Set[str] = None, + custom_user: Optional[str] = None) -> str: """ runs a given command and returns its default output - :param cmd: - :param expected_code: - :param ignore_return_code: - :param print_error: - :param global_interpreter :return: """ args = { @@ -239,13 +238,14 @@ def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, if not print_error: args["stderr"] = subprocess.DEVNULL - res = subprocess.run(cmd, **args) + final_cmd = f"runuser -u {custom_user} -- {cmd}" if custom_user else cmd + res = subprocess.run(final_cmd, **args) return res.stdout.decode() if ignore_return_code or res.returncode == expected_code else None def new_subprocess(cmd: List[str], cwd: str = '.', shell: bool = False, stdin = None, global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG, - extra_paths: Set[str] = None) -> subprocess.Popen: + extra_paths: Set[str] = None, custom_user: Optional[str] = None) -> subprocess.Popen: args = { "stdout": PIPE, "stderr": PIPE, @@ -255,7 +255,8 @@ def new_subprocess(cmd: List[str], cwd: str = '.', shell: bool = False, stdin = "stdin": stdin if stdin else subprocess.DEVNULL } - return subprocess.Popen(cmd, **args) + final_cmd = ['runuser', '-u', custom_user, '--', *cmd] if custom_user else cmd + return subprocess.Popen(final_cmd, **args) def new_root_subprocess(cmd: List[str], root_password: str, cwd: str = '.', @@ -302,8 +303,9 @@ def get_human_size_str(size) -> str: return str(int_size) -def run(cmd: List[str], success_code: int = 0) -> Tuple[bool, str]: - p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL) +def run(cmd: List[str], success_code: int = 0, custom_user: Optional[str] = None) -> Tuple[bool, str]: + final_cmd = ['runuser', '-u', custom_user, '--', *cmd] if custom_user else cmd + p = subprocess.run(final_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.DEVNULL) return p.returncode == success_code, p.stdout.decode() @@ -327,9 +329,13 @@ def check_enabled_services(*names: str) -> Dict[str, bool]: return {s: status[i].strip().lower() == 'enabled' for i, s in enumerate(names) if s} -def execute(cmd: str, shell: bool = False, cwd: Optional[str] = None, output: bool = True, custom_env: Optional[dict] = None, stdin: bool = True) -> Tuple[int, Optional[str]]: +def execute(cmd: str, shell: bool = False, cwd: Optional[str] = None, output: bool = True, custom_env: Optional[dict] = None, + stdin: bool = True, custom_user: Optional[str] = None) -> Tuple[int, Optional[str]]: + + final_cmd = f"runuser -u {custom_user} -- {cmd}" if custom_user else cmd + params = { - 'args': cmd.split(' ') if not shell else [cmd], + 'args': final_cmd.split(' ') if not shell else [final_cmd], 'stdout': subprocess.PIPE if output else subprocess.DEVNULL, 'stderr': subprocess.STDOUT if output else subprocess.DEVNULL, 'shell': shell diff --git a/bauh/gems/arch/__init__.py b/bauh/gems/arch/__init__.py index 05169a09..5fafbeaa 100644 --- a/bauh/gems/arch/__init__.py +++ b/bauh/gems/arch/__init__.py @@ -1,11 +1,11 @@ import os +from typing import Optional from bauh import __app_name__ -from bauh.api.paths import CONFIG_DIR, TEMP_DIR, CACHE_DIR +from bauh.api.paths import CONFIG_DIR, TEMP_DIR, CACHE_DIR, get_temp_dir from bauh.commons import resource ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -BUILD_DIR = f'{TEMP_DIR}/arch' ARCH_CACHE_DIR = f'{CACHE_DIR}/arch' CATEGORIES_FILE_PATH = f'{ARCH_CACHE_DIR}/categories.txt' URL_CATEGORIES_FILE = f'https://raw.githubusercontent.com/vinifmor/{__app_name__}-files/master/arch/categories.txt' @@ -21,6 +21,10 @@ IGNORED_REBUILD_CHECK_FILE = f'{ARCH_CONFIG_DIR}/aur/ignored_rebuild_check.txt' +def get_pkgbuild_dir(user: Optional[str] = None) -> str: + return f'{get_temp_dir(user) if user else TEMP_DIR}/arch' + + def get_icon_path() -> str: return resource.get_path('img/arch.svg', ROOT_DIR) diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index d002dc28..5716f346 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -42,10 +42,6 @@ 'conflicts') -def map_pkgbuild(pkgbuild: str) -> dict: - return {attr: val.replace('"', '').replace("'", '').replace('(', '').replace(')', '') for attr, val in re.findall(r'\n(\w+)=(.+)', pkgbuild)} - - def map_srcinfo(string: str, pkgname: Optional[str], fields: Set[str] = None) -> dict: subinfos, subinfo = [], {} @@ -260,3 +256,4 @@ def fill_update_data(self, output: Dict[str, dict], pkgname: str, latest_version def is_supported(arch_config: dict) -> bool: return arch_config['aur'] and git.is_installed() + diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index 72280920..5a88fd14 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -1,16 +1,15 @@ -from pathlib import Path +from typing import Optional from bauh.commons.config import YAMLConfigManager -from bauh.gems.arch import CONFIG_FILE, BUILD_DIR +from bauh.gems.arch import CONFIG_FILE, get_pkgbuild_dir -def get_build_dir(arch_config: dict) -> str: +def get_build_dir(arch_config: dict, user: Optional[str]) -> str: build_dir = arch_config.get('aur_build_dir') if not build_dir: - build_dir = BUILD_DIR + build_dir = get_pkgbuild_dir(user) - Path(build_dir).mkdir(parents=True, exist_ok=True) return build_dir diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 1e8329ed..251f4a51 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -10,12 +10,14 @@ from datetime import datetime from math import floor from pathlib import Path +from pwd import getpwnam from threading import Thread from typing import List, Set, Type, Tuple, Dict, Iterable, Optional, Collection import requests from dateutil.parser import parse as parse_date +from bauh import __app_name__ from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext, UpgradeRequirements, \ TransactionResult, SoftwareAction from bauh.api.abstract.disk import DiskCacheLoader @@ -25,20 +27,19 @@ from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextInputType, \ FileChooserComponent, TextComponent -from bauh.api.paths import TEMP_DIR from bauh.api.exception import NoInternetException +from bauh.api.paths import TEMP_DIR from bauh.commons import system -from bauh.api import user from bauh.commons.boot import CreateConfigFile from bauh.commons.category import CategoriesDownloader from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, SimpleProcess from bauh.commons.util import datetime_as_milis from bauh.commons.view_utils import new_select -from bauh.gems.arch import aur, pacman, makepkg, message, confirmation, disk, git, \ +from bauh.gems.arch import aur, pacman, message, confirmation, disk, git, \ gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ get_icon_path, database, mirrors, sorting, cpu_manager, UPDATES_IGNORED_FILE, \ - ARCH_CONFIG_DIR, EDITABLE_PKGBUILDS_FILE, URL_GPG_SERVERS, BUILD_DIR, rebuild_detector + ARCH_CONFIG_DIR, EDITABLE_PKGBUILDS_FILE, URL_GPG_SERVERS, rebuild_detector, makepkg, sshell from bauh.gems.arch.aur import AURClient from bauh.gems.arch.config import get_build_dir, ArchConfigManager from bauh.gems.arch.dependencies import DependenciesAnalyser @@ -48,6 +49,7 @@ from bauh.gems.arch.model import ArchPackage from bauh.gems.arch.output import TransactionStatusHandler from bauh.gems.arch.pacman import RE_DEP_OPERATORS +from bauh.gems.arch.proc_util import write_as_user from bauh.gems.arch.updates import UpdatesSummarizer from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer, RefreshMirrors, \ SyncDatabases @@ -247,6 +249,7 @@ def __init__(self, context: ApplicationContext, disk_cache_updater: Optional[Arc self.index_aur = None self.re_file_conflict = re.compile(r'[\w\d\-_.]+:') self.disk_cache_updater = disk_cache_updater + self.pkgbuilder_user: Optional[str] = f'{__app_name__}-aur' if context.root_user else None @staticmethod def get_aur_semantic_search_map() -> Dict[str, str]: @@ -673,32 +676,39 @@ def read_installed(self, disk_loader: Optional[DiskCacheLoader], limit: int = -1 return SearchResult(pkgs, None, len(pkgs)) - def _downgrade_aur_pkg(self, context: TransactionContext): + def _downgrade_aur_pkg(self, context: TransactionContext) -> bool: + if not self.add_package_builder_user(context.handler): + return False + if context.commit: self.logger.info("Package '{}' current commit {}".format(context.name, context.commit)) else: self.logger.warning("Package '{}' has no commit associated with it. Downgrading will only compare versions.".format(context.name)) - context.build_dir = '{}/build_{}'.format(get_build_dir(context.config), int(time.time())) + context.build_dir = f'{get_build_dir(context.config, self.pkgbuilder_user)}/build_{int(time.time())}' try: if not os.path.exists(context.build_dir): - build_dir = context.handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', context.build_dir]))) + build_dir, build_dir_error = sshell.mkdir(dir_path=context.build_dir, custom_user=self.pkgbuilder_user) - if build_dir: + if not build_dir: + context.watcher.print(build_dir_error) + else: context.handler.watcher.change_progress(10) base_name = context.get_base_name() context.watcher.change_substatus(self.i18n['arch.clone'].format(bold(context.name))) - cloned, _ = context.handler.handle_simple(git.clone_as_process(url=URL_GIT.format(base_name), cwd=context.build_dir)) + clone_dir = f'{context.build_dir}/{base_name}' + cloned, _ = context.handler.handle_simple(git.clone(url=URL_GIT.format(base_name), + target_dir=clone_dir, + custom_user=self.pkgbuilder_user)) context.watcher.change_progress(30) if cloned: context.watcher.change_substatus(self.i18n['arch.downgrade.reading_commits']) - clone_path = '{}/{}'.format(context.build_dir, base_name) - context.project_dir = clone_path - srcinfo_path = '{}/.SRCINFO'.format(clone_path) + context.project_dir = clone_dir + srcinfo_path = f'{clone_dir}/.SRCINFO' - logs = git.log_shas_and_timestamps(clone_path) + logs = git.log_shas_and_timestamps(clone_dir) context.watcher.change_progress(40) if not logs or len(logs) == 1: @@ -719,7 +729,7 @@ def _downgrade_aur_pkg(self, context: TransactionContext): self.logger.warning("Could not find '{}' target commit to revert to".format(context.name)) else: context.watcher.change_substatus(self.i18n['arch.downgrade.version_found']) - checkout_proc = new_subprocess(['git', 'checkout', target_commit], cwd=clone_path) + checkout_proc = new_subprocess(['git', 'checkout', target_commit], cwd=clone_dir, custom_user=self.pkgbuilder_user) if not context.handler.handle(SystemProcess(checkout_proc, check_error_output=False)): context.watcher.print("Could not rollback to current version's commit") return False @@ -738,7 +748,7 @@ def _downgrade_aur_pkg(self, context: TransactionContext): with open(srcinfo_path) as f: pkgsrc = aur.map_srcinfo(string=f.read(), pkgname=context.name, fields=srcfields) - reset_proc = new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path) + reset_proc = new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_dir, custom_user=self.pkgbuilder_user) if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): context.handler.watcher.print('Could not downgrade anymore. Aborting...') return False @@ -752,18 +762,18 @@ def _downgrade_aur_pkg(self, context: TransactionContext): if commit_found: context.watcher.change_substatus(self.i18n['arch.downgrade.version_found']) - checkout_proc = new_subprocess(['git', 'checkout', commit_found], cwd=clone_path) + checkout_proc = new_subprocess(['git', 'checkout', commit_found], cwd=clone_dir, custom_user=self.pkgbuilder_user) if not context.handler.handle(SystemProcess(checkout_proc, check_error_output=False)): context.watcher.print("Could not rollback to current version's commit") return False - reset_proc = new_subprocess(['git', 'reset', '--hard', commit_found], cwd=clone_path) + reset_proc = new_subprocess(['git', 'reset', '--hard', commit_found], cwd=clone_dir, custom_user=self.pkgbuilder_user) if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): context.watcher.print("Could not downgrade to previous commit of '{}'. Aborting...".format(commit_found)) return False break - elif current_version == context.get_version(): # current version found: + elif current_version == context.get_version(): commit_found, commit_date = commit, date context.watcher.change_substatus(self.i18n['arch.downgrade.install_older']) @@ -825,10 +835,11 @@ def _downgrade_repo_pkg(self, context: TransactionContext): return self._install(context) def downgrade(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher) -> bool: - self.aur_client.clean_caches() - if not self._check_action_allowed(pkg, watcher): + if not self.check_action_allowed(pkg, watcher): return False + self.aur_client.clean_caches() + handler = ProcessHandler(watcher) if self._is_database_locked(handler, root_password): @@ -857,14 +868,6 @@ def clean_cache_for(self, pkg: ArchPackage): if os.path.exists(pkg.get_disk_cache_path()): shutil.rmtree(pkg.get_disk_cache_path()) - def _check_action_allowed(self, pkg: ArchPackage, watcher: ProcessWatcher) -> bool: - if user.is_root() and pkg.repository == 'aur': - watcher.show_message(title=self.i18n['arch.install.aur.root_error.title'], - body=self.i18n['arch.install.aur.root_error.body'], - type_=MessageType.ERROR) - return False - return True - def _is_database_locked(self, handler: ProcessHandler, root_password: str) -> bool: if os.path.exists('/var/lib/pacman/db.lck'): handler.watcher.print('pacman database is locked') @@ -1155,16 +1158,13 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher pkg_sizes[req.pkg.name] = req.required_size - if aur_pkgs and not self._check_action_allowed(aur_pkgs[0], watcher): - return False - arch_config = self.configman.get_config() aur_supported = bool(aur_pkgs) or aur.is_supported(arch_config) self._sync_databases(arch_config=arch_config, aur_supported=aur_supported, root_password=root_password, handler=handler) - if repo_pkgs: + if repo_pkgs and self.check_action_allowed(repo_pkgs[0], watcher): if not self._upgrade_repo_pkgs(to_upgrade=[p.name for p in repo_pkgs], to_remove={r.pkg.name for r in requirements.to_remove} if requirements.to_remove else None, handler=handler, @@ -1177,7 +1177,7 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher elif requirements.to_remove and not self._remove_transaction_packages({r.pkg.name for r in requirements.to_remove}, handler, root_password): return False - if aur_pkgs: + if aur_pkgs and self.check_action_allowed(aur_pkgs[0], watcher) and self.add_package_builder_user(handler): watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_aur_pkgs'])) self.logger.info("Retrieving the 'last_modified' field for each package to upgrade") @@ -1457,7 +1457,16 @@ def uninstall(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatche names={pkg.name}, disk_loader=disk_loader) # to be able to return all uninstalled packages if success: - return TransactionResult(success=True, installed=None, removed=[*removed.values()] if removed else []) + removed_list = [] + + main_removed = removed.get(pkg.name) + if main_removed: + pkg.installed = False + pkg.url_download = main_removed.url_download # otherwise uninstalled AUR packages cannot be reinstalled on the same view + removed_list.append(pkg) + + removed_list.extend((inst for name, inst in removed.items() if name != pkg.name)) + return TransactionResult(success=not pkg.installed, installed=None, removed=removed_list) else: return TransactionResult.fail() @@ -1581,21 +1590,21 @@ def _get_history_aur_pkg(self, pkg: ArchPackage) -> PackageHistory: self.logger.warning("Package '{}' has no commit associated with it. Current history status may not be correct.".format(pkg.name)) arch_config = self.configman.get_config() - temp_dir = '{}/build_{}'.format(get_build_dir(arch_config), int(time.time())) + temp_dir = f'{get_build_dir(arch_config, self.pkgbuilder_user)}/build_{int(time.time())}' try: Path(temp_dir).mkdir(parents=True) base_name = pkg.get_base_name() run_cmd('git clone ' + URL_GIT.format(base_name), print_error=False, cwd=temp_dir) - clone_path = '{}/{}'.format(temp_dir, base_name) + clone_dir = f'{temp_dir}/{base_name}' - srcinfo_path = '{}/.SRCINFO'.format(clone_path) + srcinfo_path = f'{clone_dir}/.SRCINFO' if not os.path.exists(srcinfo_path): return PackageHistory.empyt(pkg) - logs = git.log_shas_and_timestamps(clone_path) + logs = git.log_shas_and_timestamps(clone_dir) if logs: srcfields = {'epoch', 'pkgver', 'pkgrel'} @@ -1622,7 +1631,7 @@ def _get_history_aur_pkg(self, pkg: ArchPackage) -> PackageHistory: '3_date': datetime.fromtimestamp(timestamp)}) # the number prefix is to ensure the rendering order if idx + 1 < len(logs): - if not run_cmd('git reset --hard ' + logs[idx + 1][0], cwd=clone_path): + if not run_cmd('git reset --hard ' + logs[idx + 1][0], cwd=clone_dir): break return PackageHistory(pkg=pkg, history=history, pkg_status_idx=status_idx) @@ -1738,7 +1747,7 @@ def _request_conflict_resolution(self, pkg: str, conflicting_pkg: str, context: context.restabilish_progress() return res - def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]]) -> Iterable[str]: + def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]]) -> Optional[Iterable[str]]: progress_increment = int(100 / len(deps)) progress = 0 self._update_progress(context, 1) @@ -1785,7 +1794,7 @@ def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]] pkg_sizes = pacman.map_download_sizes(repo_dep_names) downloaded = self._download_packages(repo_dep_names, context.handler, context.root_password, pkg_sizes, multithreaded=True) except ArchDownloadException: - return False + return None status_handler = TransactionStatusHandler(watcher=context.watcher, i18n=self.i18n, names={*repo_dep_names}, logger=self.logger, percentage=len(repo_deps) > 1, downloading=downloaded) @@ -1827,7 +1836,8 @@ def _map_repos(self, pkgnames: Collection[str]) -> dict: return pkg_repos def _pre_download_source(self, pkgname: str, project_dir: str, watcher: ProcessWatcher) -> bool: - if self.context.file_downloader.is_multithreaded(): + # TODO: multi-threaded download client cannot be run as another user at the moment + if not self.context.root_user and self.context.file_downloader.is_multithreaded(): with open('{}/.SRCINFO'.format(project_dir)) as f: srcinfo = aur.map_srcinfo(string=f.read(), pkgname=pkgname) @@ -1839,7 +1849,7 @@ def _pre_download_source(self, pkgname: str, project_dir: str, watcher: ProcessW continue else: for f in srcinfo[attr]: - if RE_PRE_DOWNLOAD_WL_PROTOCOLS.match(f) and not RE_PRE_DOWNLOAD_BL_EXT.match(f): + if RE_PRE_DOWNLOAD_WL_PROTOCOLS.match(f) and not RE_PRE_DOWNLOAD_BL_EXT.match(f): pre_download_files.append(f) if pre_download_files: @@ -1872,10 +1882,11 @@ def _display_pkgbuild_for_editing(self, pkgname: str, watcher: ProcessWatcher, p deny_button=False) if pkgbuild_input.get_value() != pkgbuild: - with open(pkgbuild_path, 'w+') as f: - f.write(pkgbuild_input.get_value()) + if not write_as_user(content=pkgbuild_input.get_value(), file_path=pkgbuild_path, user=self.pkgbuilder_user): + return False - return makepkg.update_srcinfo('/'.join(pkgbuild_path.split('/')[0:-1])) + return makepkg.update_srcinfo(project_dir='/'.join(pkgbuild_path.split('/')[0:-1]), + custom_user=self.pkgbuilder_user) return False @@ -1897,9 +1908,10 @@ def _edit_pkgbuild_and_update_context(self, context: TransactionContext): if self._ask_for_pkgbuild_edition(pkgname=context.name, arch_config=context.config, watcher=context.watcher, - pkgbuild_path='{}/PKGBUILD'.format(context.project_dir)): + pkgbuild_path=f'{context.project_dir}/PKGBUILD'): context.pkgbuild_edited = True - srcinfo = aur.map_srcinfo(string=makepkg.gen_srcinfo(context.project_dir), pkgname=context.name) + srcinfo = aur.map_srcinfo(string=makepkg.gen_srcinfo(build_dir=context.project_dir, custom_user=self.pkgbuilder_user), + pkgname=context.name) if srcinfo: context.name = srcinfo['pkgname'] @@ -1915,12 +1927,14 @@ def _edit_pkgbuild_and_update_context(self, context: TransactionContext): setattr(context.pkg, pkgattr, srcinfo.get(srcattr, getattr(context.pkg, pkgattr))) def _read_srcinfo(self, context: TransactionContext) -> str: - src_path = '{}/.SRCINFO'.format(context.project_dir) + src_path = f'{context.project_dir}/.SRCINFO' + if not os.path.exists(src_path): - srcinfo = makepkg.gen_srcinfo(context.project_dir, context.custom_pkgbuild_path) + srcinfo = makepkg.gen_srcinfo(build_dir=context.project_dir, + custom_pkgbuild_path=context.custom_pkgbuild_path, + custom_user=self.pkgbuilder_user) - with open(src_path, 'w+') as f: - f.write(srcinfo) + write_as_user(content=srcinfo, file_path=src_path, user=self.pkgbuilder_user) else: with open(src_path) as f: srcinfo = f.read() @@ -1947,10 +1961,11 @@ def _build(self, context: TransactionContext) -> bool: cpus_changed, cpu_prev_governors = cpu_manager.set_all_cpus_to('performance', context.root_password, self.logger) try: - pkgbuilt, output = makepkg.make(pkgdir=context.project_dir, - optimize=optimize, - handler=context.handler, - custom_pkgbuild=context.custom_pkgbuild_path) + pkgbuilt, output = makepkg.build(pkgdir=context.project_dir, + optimize=optimize, + handler=context.handler, + custom_pkgbuild=context.custom_pkgbuild_path, + custom_user=self.pkgbuilder_user) finally: if cpus_changed and cpu_prev_governors: self.logger.info("Restoring CPU governors") @@ -1998,7 +2013,10 @@ def _update_aur_index(self, watcher: ProcessWatcher): def __fill_aur_output_files(self, context: TransactionContext): self.logger.info("Determining output files of '{}'".format(context.name)) context.watcher.change_substatus(self.i18n['arch.aur.build.list_output']) - output_files = {f for f in makepkg.list_output_files(context.project_dir, context.custom_pkgbuild_path) if os.path.isfile(f)} + + output_files = {f for f in makepkg.list_output_files(project_dir=context.project_dir, + custom_pkgbuild_path=context.custom_pkgbuild_path, + custom_user=self.pkgbuilder_user) if os.path.isfile(f)} if output_files: context.install_files = output_files @@ -2013,7 +2031,8 @@ def __fill_aur_output_files(self, context: TransactionContext): file_to_install = gen_file[0] if len(gen_file) > 1: - srcinfo = aur.map_srcinfo(string=makepkg.gen_srcinfo(context.project_dir), pkgname=context.name) + srcinfo = aur.map_srcinfo(string=makepkg.gen_srcinfo(build_dir=context.project_dir, custom_user=self.pkgbuilder_user), + pkgname=context.name) pkgver = '-{}'.format(srcinfo['pkgver']) if srcinfo.get('pkgver') else '' pkgrel = '-{}'.format(srcinfo['pkgrel']) if srcinfo.get('pkgrel') else '' arch = '-{}'.format(srcinfo['arch']) if srcinfo.get('arch') else '' @@ -2134,11 +2153,12 @@ def _handle_aur_package_deps_and_keys(self, context: TransactionContext) -> bool if not handled_deps: return False - check_res = makepkg.check(context.project_dir, + check_res = makepkg.check(project_dir=context.project_dir, optimize=bool(context.config['optimize']), missing_deps=False, handler=context.handler, - custom_pkgbuild=context.custom_pkgbuild_path) + custom_pkgbuild=context.custom_pkgbuild_path, + custom_user=self.pkgbuilder_user) if check_res: if check_res.get('gpg_key'): @@ -2502,23 +2522,30 @@ def _import_pgp_keys(self, pkgname: str, root_password: str, handler: ProcessHan return False def _install_from_aur(self, context: TransactionContext) -> bool: + if not context.dependency and not self.add_package_builder_user(context.handler): + return False + self._optimize_makepkg(context.config, context.watcher) - context.build_dir = '{}/build_{}'.format(get_build_dir(context.config), int(time.time())) + context.build_dir = f'{get_build_dir(context.config, self.pkgbuilder_user)}/build_{int(time.time())}' try: if not os.path.exists(context.build_dir): - build_dir = context.handler.handle(SystemProcess(new_subprocess(['mkdir', '-p', context.build_dir]))) + build_dir, build_dir_error = sshell.mkdir(dir_path=context.build_dir, custom_user=self.pkgbuilder_user) self._update_progress(context, 10) - if build_dir: + if not build_dir: + context.watcher.print(build_dir_error) + else: base_name = context.get_base_name() context.watcher.change_substatus(self.i18n['arch.clone'].format(bold(base_name))) - cloned = context.handler.handle_simple(git.clone_as_process(url=URL_GIT.format(base_name), cwd=context.build_dir, depth=1)) + clone_dir = f'{context.build_dir}/{base_name}' + cloned = context.handler.handle_simple(git.clone(url=URL_GIT.format(base_name), target_dir=clone_dir, + depth=1, custom_user=self.pkgbuilder_user)) if cloned: self._update_progress(context, 40) - context.project_dir = '{}/{}'.format(context.build_dir, base_name) + context.project_dir = clone_dir return self._build(context) finally: if os.path.exists(context.build_dir) and context.config['aur_remove_build_dir']: @@ -2544,10 +2571,10 @@ def _optimize_makepkg(self, arch_config: dict, watcher: Optional[ProcessWatcher] ArchCompilationOptimizer(i18n=self.i18n, logger=self.context.logger, taskman=TaskManager()).optimize() def install(self, pkg: ArchPackage, root_password: str, disk_loader: Optional[DiskCacheLoader], watcher: ProcessWatcher, context: TransactionContext = None) -> TransactionResult: - self.aur_client.clean_caches() + if not self.check_action_allowed(pkg, watcher): + return TransactionResult.fail() - if not self._check_action_allowed(pkg, watcher): - return TransactionResult(success=False, installed=[], removed=[]) + self.aur_client.clean_caches() handler = ProcessHandler(watcher) if not context else context.handler @@ -2567,11 +2594,11 @@ def install(self, pkg: ArchPackage, root_password: str, disk_loader: Optional[Di root_password=root_password, handler=handler) if pkg.repository == 'aur': - res = self._install_from_aur(install_context) + pkg_installed = self._install_from_aur(install_context) else: - res = self._install_from_repository(install_context) + pkg_installed = self._install_from_repository(install_context) - if res: + if pkg_installed: pkg.name = install_context.name # changes the package name in case the PKGBUILD was edited if os.path.exists(pkg.get_disk_data_path()): @@ -2589,7 +2616,7 @@ def install(self, pkg: ArchPackage, root_password: str, disk_loader: Optional[Di installed = [] - if res and disk_loader and install_context.installed: + if pkg_installed and disk_loader and install_context.installed: installed.append(pkg) installed_to_load = [] @@ -2610,7 +2637,7 @@ def install(self, pkg: ArchPackage, root_password: str, disk_loader: Optional[Di self.logger.warning("Could not load all installed packages. Missing: {}".format(missing)) removed = [*install_context.removed.values()] if install_context.removed else [] - return TransactionResult(success=res, installed=installed, removed=removed) + return TransactionResult(success=pkg_installed, installed=installed, removed=removed) def _install_from_repository(self, context: TransactionContext) -> bool: try: @@ -2936,7 +2963,7 @@ def get_settings(self, screen_width: int, screen_height: int) -> Optional[ViewCo capitalize_label=False), FileChooserComponent(id_='aur_build_dir', label=self.i18n['arch.config.aur_build_dir'], - tooltip=self.i18n['arch.config.aur_build_dir.tip'].format(BUILD_DIR), + tooltip=self.i18n['arch.config.aur_build_dir.tip'].format(get_build_dir(arch_config, self.pkgbuilder_user)), max_width=max_width, file_path=arch_config['aur_build_dir'], capitalize_label=False, @@ -3397,7 +3424,8 @@ def _gen_custom_pkgbuild_if_required(self, context: TransactionContext) -> Optio deny_label=self.i18n['arch.aur.sync.several_names.popup.bt_selected']): context.pkgs_to_build = {context.name, *select.get_selected_values()} - pkgbuild_path = '{}/PKGBUILD'.format(context.project_dir) + pkgbuild_path = f'{context.project_dir}/PKGBUILD' + with open(pkgbuild_path) as f: current_pkgbuild = f.read() @@ -3407,16 +3435,22 @@ def _gen_custom_pkgbuild_if_required(self, context: TransactionContext) -> Optio names = context.name context.pkgs_to_build = {context.name} - new_pkgbuild = RE_PKGBUILD_PKGNAME.sub("pkgname={}".format(names), current_pkgbuild) - custom_pkgbuild_path = pkgbuild_path + '_CUSTOM' + new_pkgbuild = RE_PKGBUILD_PKGNAME.sub(f"pkgname={names}", current_pkgbuild) + custom_pkgbuild_path = f'{pkgbuild_path}_CUSTOM' - with open(custom_pkgbuild_path, 'w+') as f: - f.write(new_pkgbuild) + if not write_as_user(content=new_pkgbuild, + file_path=custom_pkgbuild_path, + user=self.pkgbuilder_user): + self.logger.error(f"Could not write edited PKGBUILD to '{custom_pkgbuild_path}'") + return - new_srcinfo = makepkg.gen_srcinfo(context.project_dir, custom_pkgbuild_path) + new_srcinfo = makepkg.gen_srcinfo(build_dir=context.project_dir, + custom_pkgbuild_path=custom_pkgbuild_path, + custom_user=self.pkgbuilder_user) - with open('{}/.SRCINFO'.format(context.project_dir), 'w+') as f: - f.write(new_srcinfo) + srcinfo_path = f'{context.project_dir}/.SRCINFO' + if not write_as_user(content=new_srcinfo, file_path=srcinfo_path, user=self.pkgbuilder_user): + self.logger.warning(f"Could not write the updated .SRCINFO content to '{srcinfo_path}'") return custom_pkgbuild_path @@ -3449,6 +3483,8 @@ def _list_opt_deps_with_no_hard_requirements(self, source_pkgs: Set[str], instal except PackageInHoldException: self.logger.warning("There is a requirement in hold for opt dep '{}'".format(p)) continue + except PackageNotFoundException: + self.logger.warning(f"No hard requirements found for optional {p}. Reason: package not found") return res @@ -3499,3 +3535,47 @@ def set_rebuild_check(self, pkg: ArchPackage, root_password: str, watcher: Proce pkg.update_state() return True + + def check_action_allowed(self, pkg: ArchPackage, watcher: Optional[ProcessWatcher]) -> bool: + if self.context.root_user and pkg.repository == 'aur': + if not shutil.which('useradd'): + if watcher: + watcher.show_message(title=self.i18n['error'].capitalize(), + type_=MessageType.ERROR, + body=self.i18n['arch.aur.error.missing_root_dep'].format(dep=bold('useradd'), + aur=bold('AUR'), + root=bold('root'))) + return False + + if not shutil.which('runuser'): + if watcher: + watcher.show_message(title=self.i18n['error'].capitalize(), + type_=MessageType.ERROR, + body=self.i18n['arch.aur.error.missing_root_dep'].format(dep=bold('runuser'), + aur=bold('AUR'), + root=bold('root'))) + return False + + return True + + def add_package_builder_user(self, handler: ProcessHandler) -> bool: + if self.context.root_user and self.pkgbuilder_user: + try: + getpwnam(self.pkgbuilder_user) + return True + except KeyError: + self.logger.warning(f"Package builder user '{self.pkgbuilder_user}' does not exist") + self.logger.info(f"Adding the package builder user '{self.pkgbuilder_user}'") + added, output = handler.handle_simple(SimpleProcess(cmd=['useradd', self.pkgbuilder_user], shell=True)) + + if not added: + output_log = "Command output: {}".format(output.replace('\n', ' ') if output else '(no output)') + self.logger.error(f"Could not add the package builder user '{self.pkgbuilder_user}'. {output_log}") + handler.watcher.show_message(title=self.i18n['error'].capitalize(), + type_=MessageType.ERROR, + body=self.i18n['arch.aur.error.add_builder_user'].format(user=bold(self.pkgbuilder_user), + aur=bold('AUR'))) + + return added + + return True diff --git a/bauh/gems/arch/git.py b/bauh/gems/arch/git.py index 086e8112..8320002d 100644 --- a/bauh/gems/arch/git.py +++ b/bauh/gems/arch/git.py @@ -53,10 +53,13 @@ def log_shas_and_timestamps(repo_path: str) -> Optional[List[Tuple[str, int]]]: return logs -def clone_as_process(url: str, cwd: Optional[str], depth: int = -1) -> SimpleProcess: +def clone(url: str, target_dir: Optional[str], depth: int = -1, custom_user: Optional[str] = None) -> SimpleProcess: cmd = ['git', 'clone', url] if depth > 0: - cmd.append('--depth={}'.format(depth)) + cmd.append(f'--depth={depth}') - return SimpleProcess(cmd=cmd, cwd=cwd) + if target_dir: + cmd.append(target_dir) + + return SimpleProcess(cmd=cmd, custom_user=custom_user) diff --git a/bauh/gems/arch/makepkg.py b/bauh/gems/arch/makepkg.py index 424697ef..3c59bb7b 100644 --- a/bauh/gems/arch/makepkg.py +++ b/bauh/gems/arch/makepkg.py @@ -1,26 +1,44 @@ import os import re -from typing import Tuple, Optional, Set +from typing import Optional, Set, Tuple -from bauh.commons.system import SimpleProcess, ProcessHandler, run_cmd +from bauh.commons import system +from bauh.commons.system import ProcessHandler, SimpleProcess from bauh.gems.arch import CUSTOM_MAKEPKG_FILE +from bauh.gems.arch.proc_util import write_as_user -RE_DEPS_PATTERN = re.compile(r'\n?\s+->\s(.+)\n') RE_UNKNOWN_GPG_KEY = re.compile(r'\(unknown public key (\w+)\)') +RE_DEPS_PATTERN = re.compile(r'\n?\s+->\s(.+)\n') -def gen_srcinfo(build_dir: str, custom_pkgbuild_path: Optional[str] = None) -> str: - return run_cmd('makepkg --printsrcinfo{}'.format(' -p {}'.format(custom_pkgbuild_path) if custom_pkgbuild_path else ''), - cwd=build_dir) +def gen_srcinfo(build_dir: str, custom_pkgbuild_path: Optional[str] = None, custom_user: Optional[str] = None) -> str: + cmd = f"makepkg --printsrcinfo{' -p {}'.format(custom_pkgbuild_path) if custom_pkgbuild_path else ''}" + return system.run_cmd(cmd, cwd=build_dir, custom_user=custom_user) -def check(pkgdir: str, optimize: bool, missing_deps: bool, handler: ProcessHandler, custom_pkgbuild: Optional[str] = None) -> dict: - res = {} +def update_srcinfo(project_dir: str, custom_user: Optional[str] = None) -> bool: + updated_src = system.run_cmd('makepkg --printsrcinfo', cwd=project_dir, custom_user=custom_user) - cmd = ['makepkg', '-ALcfm', '--check', '--noarchive', '--nobuild', '--noprepare'] + if updated_src: + return write_as_user(content=updated_src, file_path=f"{project_dir}/.SRCINFO", user=custom_user) - if not missing_deps: - cmd.append('--nodeps') + return False + + +def list_output_files(project_dir: str, custom_pkgbuild_path: Optional[str] = None, + custom_user: Optional[str] = None) -> Set[str]: + cmd = f"makepkg --packagelist{' -p {}'.format(custom_pkgbuild_path) if custom_pkgbuild_path else ''}" + output = system.run_cmd(cmd=cmd, print_error=False, cwd=project_dir, custom_user=custom_user) + + if output: + return {p.strip() for p in output.split('\n') if p} + + return set() + + +def build(pkgdir: str, optimize: bool, handler: ProcessHandler, custom_pkgbuild: Optional[str] = None, + custom_user: Optional[str] = None) -> Tuple[bool, str]: + cmd = ['makepkg', '-ALcsmf', '--skipchecksums', '--nodeps'] if custom_pkgbuild: cmd.append('-p') @@ -28,29 +46,22 @@ def check(pkgdir: str, optimize: bool, missing_deps: bool, handler: ProcessHandl if optimize: if os.path.exists(CUSTOM_MAKEPKG_FILE): - handler.watcher.print('Using custom makepkg.conf -> {}'.format(CUSTOM_MAKEPKG_FILE)) - cmd.append('--config={}'.format(CUSTOM_MAKEPKG_FILE)) + handler.watcher.print(f'Using custom makepkg.conf -> {CUSTOM_MAKEPKG_FILE}') + cmd.append(f'--config={CUSTOM_MAKEPKG_FILE}') else: - handler.watcher.print('Custom optimized makepkg.conf ( {} ) not found'.format(CUSTOM_MAKEPKG_FILE)) - - success, output = handler.handle_simple(SimpleProcess(cmd, cwd=pkgdir, shell=True)) + handler.watcher.print(f'Custom optimized makepkg.conf ({CUSTOM_MAKEPKG_FILE}) not found') - if missing_deps and 'Missing dependencies' in output: - res['missing_deps'] = RE_DEPS_PATTERN.findall(output) - - gpg_keys = RE_UNKNOWN_GPG_KEY.findall(output) - - if gpg_keys: - res['gpg_key'] = gpg_keys[0] + return handler.handle_simple(SimpleProcess(cmd, cwd=pkgdir, shell=True, custom_user=custom_user)) - if 'One or more files did not pass the validity check' in output: - res['validity_check'] = True - return res +def check(project_dir: str, optimize: bool, missing_deps: bool, handler: ProcessHandler, + custom_pkgbuild: Optional[str] = None, custom_user: Optional[str] = None) -> dict: + res = {} + cmd = ['makepkg', '-ALcfm', '--check', '--noarchive', '--nobuild', '--noprepare'] -def make(pkgdir: str, optimize: bool, handler: ProcessHandler, custom_pkgbuild: Optional[str] = None) -> Tuple[bool, str]: - cmd = ['makepkg', '-ALcsmf', '--skipchecksums', '--nodeps'] + if not missing_deps: + cmd.append('--nodeps') if custom_pkgbuild: cmd.append('-p') @@ -58,31 +69,22 @@ def make(pkgdir: str, optimize: bool, handler: ProcessHandler, custom_pkgbuild: if optimize: if os.path.exists(CUSTOM_MAKEPKG_FILE): - handler.watcher.print('Using custom makepkg.conf -> {}'.format(CUSTOM_MAKEPKG_FILE)) - cmd.append('--config={}'.format(CUSTOM_MAKEPKG_FILE)) + handler.watcher.print(f'Using custom makepkg.conf -> {CUSTOM_MAKEPKG_FILE}') + cmd.append(f'--config={CUSTOM_MAKEPKG_FILE}') else: - handler.watcher.print('Custom optimized makepkg.conf ( {} ) not found'.format(CUSTOM_MAKEPKG_FILE)) - - return handler.handle_simple(SimpleProcess(cmd, cwd=pkgdir, shell=True)) + handler.watcher.print(f'Custom optimized makepkg.conf ({CUSTOM_MAKEPKG_FILE}) not found') + success, output = handler.handle_simple(SimpleProcess(cmd, cwd=project_dir, shell=True, custom_user=custom_user)) -def update_srcinfo(project_dir: str) -> bool: - updated_src = run_cmd('makepkg --printsrcinfo', cwd=project_dir) - - if updated_src: - with open('{}/.SRCINFO'.format(project_dir), 'w+') as f: - f.write(updated_src) - return True - - return False + if missing_deps and 'Missing dependencies' in output: + res['missing_deps'] = RE_DEPS_PATTERN.findall(output) + gpg_keys = RE_UNKNOWN_GPG_KEY.findall(output) -def list_output_files(project_dir: str, custom_pkgbuild_path: Optional[str] = None) -> Set[str]: - output = run_cmd(cmd='makepkg --packagelist{}'.format(' -p {}'.format(custom_pkgbuild_path) if custom_pkgbuild_path else ''), - print_error=False, - cwd=project_dir) + if gpg_keys: + res['gpg_key'] = gpg_keys[0] - if output: - return {p.strip() for p in output.split('\n') if p} + if 'One or more files did not pass the validity check' in output: + res['validity_check'] = True - return set() + return res diff --git a/bauh/gems/arch/proc_util.py b/bauh/gems/arch/proc_util.py new file mode 100644 index 00000000..4ef77ce8 --- /dev/null +++ b/bauh/gems/arch/proc_util.py @@ -0,0 +1,51 @@ +import multiprocessing +import os +import traceback +from pwd import getpwnam +from typing import Callable, Optional, TypeVar + +R = TypeVar('R') + + +class CallAsUser: + + def __init__(self, target: Callable[[], R], user: str): + self._target = target + self._user = user + + def __call__(self, *args, **kwargs) -> R: + try: + os.setuid(getpwnam(self._user).pw_uid) + return self._target() + except: + traceback.print_exc() + + +class WriteToFile: + + def __init__(self, file_path: str, content: str): + self._file_path = file_path + self._content = content + + def __call__(self, *args, **kwargs) -> bool: + try: + with open(self._file_path, 'w+') as f: + f.write(self._content) + + return True + except: + traceback.print_exc() + return False + + +def exec_as_user(target: Callable[[], R], user: Optional[str] = None) -> R: + if user: + with multiprocessing.Pool(1) as pool: + return pool.apply(CallAsUser(target, user)) + else: + return target() + + +def write_as_user(content: str, file_path: str, user: Optional[str] = None) -> bool: + return exec_as_user(target=WriteToFile(file_path=file_path, content=content), + user=user) diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 4b4f9337..ec8381d5 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -126,6 +126,8 @@ arch.downgrade.reading_commits=S’estan llegint les revisions del dipòsit arch.downgrade.repo_pkg.no_versions=No s'ha trobat cap versió antiga al disc arch.downgrade.searching_stored=Cerqueu versions antigues al disc arch.downgrade.version_found=S’ha trobat la versió actual del paquet +arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user +arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=PKGBUILD arch.info.01_id=identificació arch.info.02_name=nom diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 4f92a835..f46459cc 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -126,6 +126,8 @@ arch.downgrade.reading_commits=Repository Commits lesen arch.downgrade.repo_pkg.no_versions=No old version found on disk arch.downgrade.searching_stored=Looking for old versions on disk arch.downgrade.version_found=Aktuelle Paketversion gefunden +arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user +arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild arch.info.01_id=Ich würde arch.info.02_name=Name diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 26935450..a3137e06 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -126,6 +126,8 @@ arch.downgrade.reading_commits=Reading the repository commits arch.downgrade.repo_pkg.no_versions=No old version found on disk arch.downgrade.searching_stored=Looking for old versions on disk arch.downgrade.version_found=Current package version found +arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user +arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild arch.info.01_id=id arch.info.02_name=name diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 674343ab..951cf926 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -126,6 +126,8 @@ arch.downgrade.reading_commits=Leyendo los commits del repositorio arch.downgrade.repo_pkg.no_versions=No se encontró una versión anterior en el disco arch.downgrade.searching_stored=Buscando versiones antiguas en el disco arch.downgrade.version_found=Version actual del paquete encontrada +arch.aur.error.missing_root_dep={dep} no está instalado y es necesario para la instalación de paquetes del {aur} como el usuario {root} +arch.aur.error.add_builder_user=No fue posible crear el usuario {user} para construir paquetes del {aur} arch.info.00_pkg_build=pkgbuild arch.info.01_id=id arch.info.02_name=nombre diff --git a/bauh/gems/arch/resources/locale/fr b/bauh/gems/arch/resources/locale/fr index 23db1bee..b7fe4b39 100644 --- a/bauh/gems/arch/resources/locale/fr +++ b/bauh/gems/arch/resources/locale/fr @@ -126,6 +126,8 @@ arch.downgrade.reading_commits=Lecture des commits du dépôt arch.downgrade.repo_pkg.no_versions=Aucune version antérieure trouvée sur le disque arch.downgrade.searching_stored=Recherche de version antérieure sur le disque arch.downgrade.version_found=Version actuelle du paquet trouvée +arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user +arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild arch.info.01_id=id arch.info.02_name=nom diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 8af43cc5..eb106094 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -126,6 +126,8 @@ arch.downgrade.reading_commits=Reading the repository commits arch.downgrade.repo_pkg.no_versions=Nessuna versione precedente trovata sul disco arch.downgrade.searching_stored=Ricerca di versioni precedenti su disco arch.downgrade.version_found=Trovata la versione del pacchetto corrente +arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user +arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild arch.info.01_id=id arch.info.02_name=nome diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 4037df26..da9a8c5d 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -125,6 +125,8 @@ arch.downgrade.reading_commits=Lendo os commits do repositório arch.downgrade.repo_pkg.no_versions=Nenhuma versão antiga encontrada no disco arch.downgrade.searching_stored=Procurando versões antigas no disco arch.downgrade.version_found=Versão atual do pacote encontrada +arch.aur.error.missing_root_dep={dep} não está instalado e é necessário para a instalação de pacotes do {aur} como o usuário {root} +arch.aur.error.add_builder_user=Não foi possível criar o usuário {user} para a construção de pacotes do {aur} arch.info.00_pkg_build=pkgbuild arch.info.01_id=id arch.info.02_name=nome diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 36b9f4d2..107fb6bc 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -126,6 +126,8 @@ arch.downgrade.reading_commits=Чтение коммитов репозитор arch.downgrade.repo_pkg.no_versions=No old version found on disk arch.downgrade.searching_stored=Looking for old versions on disk arch.downgrade.version_found=Найдена текущая версия пакета +arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user +arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=PKGBUILD arch.info.01_id=Идентификатор arch.info.02_name=Имя diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index b90b42de..a6520a28 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -126,6 +126,8 @@ arch.downgrade.reading_commits=Depo çalışmalarını oku arch.downgrade.repo_pkg.no_versions=Diskte eski sürüm bulunamadı arch.downgrade.searching_stored=Diskteki eski sürümlere bakılıyor arch.downgrade.version_found=Geçerli paket sürümü bulundu +arch.aur.error.missing_root_dep={dep} is not installed and is required for installing {aur} packages as the {root} user +arch.aur.error.add_builder_user=It was not possible to create the user {user} for building {aur} packages arch.info.00_pkg_build=pkgbuild arch.info.01_id=kimlik arch.info.02_name=isim diff --git a/bauh/gems/arch/sshell.py b/bauh/gems/arch/sshell.py new file mode 100644 index 00000000..7e4852c1 --- /dev/null +++ b/bauh/gems/arch/sshell.py @@ -0,0 +1,8 @@ +from typing import Optional, Tuple + +from bauh.commons.system import execute + + +def mkdir(dir_path: str, parent: bool = True, custom_user: Optional[str] = None) -> Tuple[bool, Optional[str]]: + code, output = execute(f'mkdir {"-p " if parent else ""}"{dir_path}"', shell=True, custom_user=custom_user) + return code == 0, output diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index d2aca52b..36e3a249 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -124,7 +124,7 @@ def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWa return ProcessHandler(watcher).handle_simple(snap.downgrade_and_stream(pkg.name, root_password))[0] def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> SystemProcess: - raise Exception("'upgrade' is not supported by {}".format(SnapManager.__class__.__name__)) + raise Exception(f"'upgrade' is not supported by {SnapManager.__class__.__name__}") def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher, disk_loader: DiskCacheLoader) -> TransactionResult: if snap.is_installed() and snapd.is_running(): @@ -173,7 +173,7 @@ def get_info(self, pkg: SnapApplication) -> dict: return info def get_history(self, pkg: SnapApplication) -> PackageHistory: - raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__)) + raise Exception(f"'get_history' is not supported by {pkg.__class__.__name__}") def install(self, pkg: SnapApplication, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher) -> TransactionResult: # retrieving all installed so it will be possible to know the additional installed runtimes after the operation succeeds @@ -210,20 +210,20 @@ def install(self, pkg: SnapApplication, root_password: str, disk_loader: DiskCac if channels: opts = [InputOption(label=c[0], value=c[1]) for c in channels] channel_select = SingleSelectComponent(type_=SelectViewType.RADIO, label='', options=opts, default_option=opts[0]) - body = '

{}.

'.format(self.i18n['snap.install.available_channels.message'].format(bold(self.i18n['stable']), bold(pkg.name))) - body += '

{}:

'.format(self.i18n['snap.install.available_channels.help']) + body = f"

{self.i18n['snap.install.available_channels.message'].format(bold(self.i18n['stable']), bold(pkg.name))}.

" + body += f"

{self.i18n['snap.install.available_channels.help']}:

" if watcher.request_confirmation(title=self.i18n['snap.install.available_channels.title'], body=body, components=[channel_select], confirmation_label=self.i18n['continue'], deny_label=self.i18n['cancel']): - self.logger.info("Installing '{}' with the custom command '{}'".format(pkg.name, channel_select.value)) + self.logger.info(f"Installing '{pkg.name}' with the custom command '{channel_select.value}'") res = ProcessHandler(watcher).handle(SystemProcess(new_root_subprocess(channel_select.value.value.split(' '), root_password=root_password))) return self._gen_installation_response(success=res, pkg=pkg, installed=installed_names, disk_loader=disk_loader) else: - self.logger.error("Could not find available channels in the installation output: {}".format(output)) + self.logger.error(f"Could not find available channels in the installation output: {output}") return self._gen_installation_response(success=res, pkg=pkg, installed=installed_names, disk_loader=disk_loader) @@ -321,14 +321,13 @@ def list_warnings(self, internet_available: bool) -> Optional[List[str]]: if not snapd.is_running(): snap_bold = bold('Snap') return [self.i18n['snap.notification.snapd_unavailable'].format(bold('snapd'), snap_bold), - self.i18n['snap.notification.snap.disable'].format(snap_bold, bold( - '{} > {}'.format(self.i18n['settings'].capitalize(), - self.i18n['core.config.tab.types'])))] + self.i18n['snap.notification.snap.disable'].format(snap_bold, + bold(f"{self.i18n['settings'].capitalize()} > {self.i18n['core.config.tab.types']}"))] elif internet_available: available, output = snap.is_api_available() if not available: - self.logger.warning('It seems Snap API is not available. Search output: {}'.format(output)) + self.logger.warning(f'It seems Snap API is not available. Search output: {output}') return [self.i18n['snap.notifications.api.unavailable'].format(bold('Snaps'), bold('Snap'))] def _fill_suggestion(self, name: str, priority: SuggestionPriority, snapd_client: SnapdClient, out: List[PackageSuggestion]): @@ -347,7 +346,7 @@ def _fill_suggestion(self, name: str, priority: SuggestionPriority, snapd_client out.append(sug) return - self.logger.warning("Could not retrieve suggestion '{}'".format(name)) + self.logger.warning(f"Could not retrieve suggestion '{name}'") def _map_to_app(self, app_json: dict, installed: bool, disk_loader: Optional[DiskCacheLoader] = None, is_application: bool = False) -> SnapApplication: app = SnapApplication(id=app_json.get('id'), @@ -382,11 +381,11 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSu res = [] if snapd.is_running(): - self.logger.info('Downloading suggestions file {}'.format(SUGGESTIONS_FILE)) + self.logger.info(f'Downloading suggestions file {SUGGESTIONS_FILE}') file = self.http_client.get(SUGGESTIONS_FILE) if not file or not file.text: - self.logger.warning("No suggestion found in {}".format(SUGGESTIONS_FILE)) + self.logger.warning(f"No suggestion found in {SUGGESTIONS_FILE}") return res else: self.logger.info('Mapping suggestions') @@ -437,7 +436,7 @@ def launch(self, pkg: SnapApplication): else: cmd = commands[0]['name'] - self.logger.info("Running '{}': {}".format(pkg.name, cmd)) + self.logger.info(f"Running '{pkg.name}': {cmd}") snap.run(cmd) def get_screenshots(self, pkg: SnapApplication) -> List[str]: @@ -482,19 +481,19 @@ def _request_channel_installation(self, pkg: SnapApplication, snap_config: Optio try: data = [r for r in snapd_client.find_by_name(pkg.name) if r['name'] == pkg.name] except: - self.logger.warning("snapd client could not retrieve channels for '{}'".format(pkg.name)) + self.logger.warning(f"snapd client could not retrieve channels for '{pkg.name}'") return if not data: - self.logger.warning("snapd client could find a match for name '{}' when retrieving its channels".format(pkg.name)) + self.logger.warning(f"snapd client could find a match for name '{pkg.name}' when retrieving its channels") else: if not data[0].get('channels'): - self.logger.info("No channel available for '{}'. Skipping selection.".format(pkg.name)) + self.logger.info(f"No channel available for '{pkg.name}'. Skipping selection.") else: if pkg.channel: - current_channel = pkg.channel if '/' in pkg.channel else 'latest/{}'.format(pkg.channel) + current_channel = pkg.channel if '/' in pkg.channel else f'latest/{pkg.channel}' else: - current_channel = 'latest/{}'.format(data[0].get('channel', 'stable')) + current_channel = f"latest/{data[0].get('channel', 'stable')}" opts = [] def_opt = None @@ -510,7 +509,7 @@ def _request_channel_installation(self, pkg: SnapApplication, snap_config: Optio def_opt = op if not opts: - self.logger.info("No different channel available for '{}'. Skipping selection.".format(pkg.name)) + self.logger.info(f"No different channel available for '{pkg.name}'. Skipping selection.") return select = SingleSelectComponent(label='', diff --git a/bauh/gems/snap/model.py b/bauh/gems/snap/model.py index 58e787af..d873b54e 100644 --- a/bauh/gems/snap/model.py +++ b/bauh/gems/snap/model.py @@ -70,7 +70,7 @@ def is_application(self) -> bool: return self.type == 'app' def get_disk_cache_path(self): - return super(SnapApplication, self).get_disk_cache_path() + '/installed/' + self.name + return f'{super(SnapApplication, self).get_disk_cache_path()}/installed/{self.name}' def is_trustable(self) -> bool: return self.verified_publisher diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index d6e5cc5b..82cba496 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -27,7 +27,7 @@ def install_and_stream(app_name: str, confinement: str, root_password: str, chan install_cmd.append('--classic') if channel: - install_cmd.append('--channel={}'.format(channel)) + install_cmd.append(f'--channel={channel}') return SimpleProcess(install_cmd, root_password=root_password, shell=True) @@ -42,7 +42,7 @@ def refresh_and_stream(app_name: str, root_password: str, channel: Optional[str] cmd = [BASE_CMD, 'refresh', app_name] if channel: - cmd.append('--channel={}'.format(channel)) + cmd.append(f'--channel={channel}') return SimpleProcess(cmd=cmd, root_password=root_password, @@ -51,7 +51,7 @@ def refresh_and_stream(app_name: str, root_password: str, channel: Optional[str] def run(cmd: str): - subprocess.Popen(['snap run {}'.format(cmd)], shell=True, env={**os.environ}) + subprocess.Popen([f'{BASE_CMD} run {cmd}'], shell=True, env={**os.environ}) def is_api_available() -> Tuple[bool, str]: diff --git a/bauh/gems/snap/snapd.py b/bauh/gems/snap/snapd.py index 2f2f0959..4a8b0f63 100644 --- a/bauh/gems/snap/snapd.py +++ b/bauh/gems/snap/snapd.py @@ -55,7 +55,7 @@ def query(self, query: str) -> Optional[List[dict]]: final_query = query.strip() if final_query and self.session: - res = self.session.get(url='{}/find'.format(URL_BASE), params={'q': final_query}) + res = self.session.get(url=f'{URL_BASE}/find', params={'q': final_query}) if res.status_code == 200: json_res = res.json() @@ -65,7 +65,7 @@ def query(self, query: str) -> Optional[List[dict]]: def find_by_name(self, name: str) -> Optional[List[dict]]: if name and self.session: - res = self.session.get('{}/find?name={}'.format(URL_BASE, name)) + res = self.session.get(f'{URL_BASE}/find?name={name}') if res.status_code == 200: json_res = res.json() @@ -75,7 +75,7 @@ def find_by_name(self, name: str) -> Optional[List[dict]]: def list_all_snaps(self) -> List[dict]: if self.session: - res = self.session.get('{}/snaps'.format(URL_BASE)) + res = self.session.get(f'{URL_BASE}/snaps') if res.status_code == 200: json_res = res.json() @@ -87,7 +87,7 @@ def list_all_snaps(self) -> List[dict]: def list_only_apps(self) -> List[dict]: if self.session: - res = self.session.get('{}/apps'.format(URL_BASE)) + res = self.session.get(f'{URL_BASE}/apps') if res.status_code == 200: json_res = res.json() @@ -98,7 +98,7 @@ def list_only_apps(self) -> List[dict]: def list_commands(self, name: str) -> List[dict]: if self.session: - res = self.session.get('{}/apps?names={}'.format(URL_BASE, name)) + res = self.session.get(f'{URL_BASE}/apps?names={name}') if res.status_code == 200: json_res = res.json() diff --git a/bauh/manage.py b/bauh/manage.py index 14458a17..2a4ea28b 100644 --- a/bauh/manage.py +++ b/bauh/manage.py @@ -45,7 +45,8 @@ def new_manage_panel(app_args: Namespace, app_config: dict, logger: logging.Logg internet_checker=InternetChecker(offline=app_args.offline), root_user=user.is_root()) - managers = gems.load_managers(context=context, locale=i18n.current_key, config=app_config, default_locale=DEFAULT_I18N_KEY) + managers = gems.load_managers(context=context, locale=i18n.current_key, config=app_config, + default_locale=DEFAULT_I18N_KEY, logger=logger) if app_args.reset: util.clean_app_files(managers) diff --git a/bauh/view/core/gems.py b/bauh/view/core/gems.py index f0dd57f8..334f8592 100644 --- a/bauh/view/core/gems.py +++ b/bauh/view/core/gems.py @@ -1,12 +1,15 @@ import inspect import os import pkgutil -from typing import List +from logging import Logger +from typing import List, Generator -from bauh import ROOT_DIR +from bauh import __app_name__, ROOT_DIR from bauh.api.abstract.controller import SoftwareManager, ApplicationContext from bauh.view.util import translation +FORBIDDEN_GEMS_FILE = f'/etc/{__app_name__}/gems.forbidden' + def find_manager(member): if not isinstance(member, str): @@ -19,12 +22,34 @@ def find_manager(member): return manager_found -def load_managers(locale: str, context: ApplicationContext, config: dict, default_locale: str) -> List[SoftwareManager]: +def read_forbidden_gems() -> Generator[str, None, None]: + try: + with open(FORBIDDEN_GEMS_FILE) as f: + forbidden_lines = f.readlines() + + for line in forbidden_lines: + clean_line = line.strip() + + if clean_line and not clean_line.startswith('#'): + yield clean_line + + except FileNotFoundError: + pass + + +def load_managers(locale: str, context: ApplicationContext, config: dict, default_locale: str, logger: Logger) -> List[SoftwareManager]: managers = [] - for f in os.scandir(ROOT_DIR + '/gems'): + forbidden_gems = {gem for gem in read_forbidden_gems()} + + for f in os.scandir(f'{ROOT_DIR}/gems'): if f.is_dir() and f.name != '__pycache__': - loader = pkgutil.find_loader('bauh.gems.{}.controller'.format(f.name)) + + if f.name in forbidden_gems: + logger.warning(f"gem '{f.name}' could not be loaded because it was marked as forbidden in '{FORBIDDEN_GEMS_FILE}'") + continue + + loader = pkgutil.find_loader(f'bauh.gems.{f.name}.controller') if loader: module = loader.load_module() @@ -33,7 +58,7 @@ def load_managers(locale: str, context: ApplicationContext, config: dict, defaul if manager_class: if locale: - locale_path = '{}/resources/locale'.format(f.path) + locale_path = f'{f.path}/resources/locale' if os.path.exists(locale_path): context.i18n.current.update(translation.get_locale_keys(locale, locale_path)[1]) @@ -51,4 +76,3 @@ def load_managers(locale: str, context: ApplicationContext, config: dict, defaul managers.append(man) return managers -