[arch] improvement -> AUR: upgrade checking now considers modification dates as well

This commit is contained in:
Vinicius Moreira
2020-12-17 18:35:34 -03:00
parent 4540fc938e
commit cf17a91b83
32 changed files with 432 additions and 149 deletions

View File

@@ -5,14 +5,19 @@ 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.11]
### New system requirements
- **python-dateutil**: better Python library for date handling (install the equivalent package for your Linux distribution before upgrading bauh)
### Improvements
- Arch
- AUR: upgrade checking now considers modification dates as well (needed because not all AUR packages follow versioning standards)
- the task responsible for generating a local AUR index is displayed on the initialization dialog
### Fixes
- AppImage
- some installed applications cannot be launched by their desktop entries (regression from **0.9.10**) [#155](https://github.com/vinifmor/bauh/issues/155). If any of your installed AppImages are affected by this issue, just reinstall them.
## [0.9.10] 2020-12-11
### Features
- Web

View File

@@ -31,9 +31,10 @@ To contribute have a look at [CONTRIBUTING.md](https://github.com/vinifmor/bauh/
#### Debian-based distros
- **python3.5** or above
- **pip3**
- **python3-requests**
- **python-yaml**
- **python3-dateutil**
- **python3-pyqt5.qtsvg**
- **python3-requests**
- **libqt5svg5**
- **qt5dxcb-plugin**
- **libappindicator3** (for the **tray mode** in GTK3 desktop environments)
@@ -42,6 +43,7 @@ To contribute have a look at [CONTRIBUTING.md](https://github.com/vinifmor/bauh/
#### Arch-based distros
- **python**
- **python-dateutil**
- **python-requests**
- **python-pip**
- **python-pyqt5**

View File

@@ -19,11 +19,12 @@ class DiskCacheLoader:
"""
pass
def fill(self, pkg: SoftwarePackage):
def fill(self, pkg: SoftwarePackage, sync: bool = False):
"""
fill cached data from the disk of a given package instance
If a cache mapping was previously done, then data retrieved will be cached to memory as well.
:param pkg:
:param sync: if the package data must be filled synchronously
:return:
"""
pass

View File

@@ -4,7 +4,7 @@ import sys
import time
from io import StringIO
from subprocess import PIPE
from typing import List, Tuple, Set, Dict
from typing import List, Tuple, Set, Dict, Optional
# default environment variables for subprocesses.
from bauh.api.abstract.handler import ProcessWatcher
@@ -322,6 +322,11 @@ 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) -> Tuple[int, str]:
p = subprocess.run(args=cmd.split(' ') if not shell else [cmd], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=shell)
def execute(cmd: str, shell: bool = False, cwd: Optional[str] = None) -> Tuple[int, str]:
p = subprocess.run(args=cmd.split(' ') if not shell else [cmd],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=shell,
cwd=cwd)
return p.returncode, p.stdout.decode()

View File

@@ -1,4 +1,4 @@
import collections
from datetime import datetime
def deep_update(source: dict, overrides: dict):
@@ -26,3 +26,7 @@ def size_to_byte(size: float, unit: str) -> int:
final_size = size * 1000000000000000
return int(final_size)
def datetime_as_milis(date: datetime) -> int:
return int(round(date.timestamp() * 1000))

View File

@@ -14,6 +14,7 @@ from threading import Thread
from typing import List, Set, Type, Tuple, Dict, Iterable, Optional
import requests
from dateutil.parser import parse as parse_date
from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext, UpgradeRequirements, \
TransactionResult, SoftwareAction
@@ -30,6 +31,7 @@ from bauh.commons.category import CategoriesDownloader
from bauh.commons.config import save_config
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, \
gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \
@@ -40,7 +42,7 @@ from bauh.gems.arch.config import read_config, get_build_dir
from bauh.gems.arch.dependencies import DependenciesAnalyser
from bauh.gems.arch.download import MultithreadedDownloadService, ArchDownloadException
from bauh.gems.arch.exceptions import PackageNotFoundException, PackageInHoldException
from bauh.gems.arch.mapper import ArchDataMapper
from bauh.gems.arch.mapper import AURDataMapper
from bauh.gems.arch.model import ArchPackage
from bauh.gems.arch.output import TransactionStatusHandler
from bauh.gems.arch.pacman import RE_DEP_OPERATORS
@@ -73,7 +75,7 @@ class TransactionContext:
missing_deps: List[Tuple[str, str]] = None, installed: Set[str] = None, removed: Dict[str, SoftwarePackage] = None,
disk_loader: DiskCacheLoader = None, disk_cache_updater: Thread = None,
new_pkg: bool = False, custom_pkgbuild_path: str = None,
pkgs_to_build: Set[str] = None):
pkgs_to_build: Set[str] = None, last_modified: Optional[int] = None):
self.name = name
self.base = base
self.maintainer = maintainer
@@ -103,13 +105,14 @@ class TransactionContext:
self.custom_pkgbuild_path = custom_pkgbuild_path
self.pkgs_to_build = pkgs_to_build
self.previous_change_progress = change_progress
self.last_modified = last_modified
@classmethod
def gen_context_from(cls, pkg: ArchPackage, arch_config: dict, root_password: str, handler: ProcessHandler) -> "TransactionContext":
return cls(name=pkg.name, base=pkg.get_base_name(), maintainer=pkg.maintainer, repository=pkg.repository,
arch_config=arch_config, watcher=handler.watcher, handler=handler, skip_opt_deps=True,
change_progress=True, root_password=root_password, dependency=False,
installed=set(), removed={}, new_pkg=not pkg.installed)
installed=set(), removed={}, new_pkg=not pkg.installed, last_modified=pkg.last_modified)
def get_base_name(self):
return self.base if self.base else self.name
@@ -189,7 +192,7 @@ class ArchManager(SoftwareManager):
self.aur_cache = context.cache_factory.new()
# context.disk_loader_factory.map(ArchPackage, self.aur_cache) TODO
self.mapper = ArchDataMapper(http_client=context.http_client, i18n=context.i18n)
self.aur_mapper = AURDataMapper(http_client=context.http_client, i18n=context.i18n, logger=context.logger)
self.i18n = context.i18n
self.aur_client = AURClient(http_client=context.http_client, logger=context.logger, x86_64=context.is_system_x86_64())
self.dcache_updater = None
@@ -334,7 +337,7 @@ class ArchManager(SoftwareManager):
pkg = installed_pkgs.get(apidata['Name'])
if not pkg:
pkg = self.mapper.map_api_data(apidata, None, self.categories)
pkg = self.aur_mapper.map_api_data(apidata, None, self.categories)
pkg.downgrade_enabled = downgrade_enabled
if pkg.installed:
@@ -342,7 +345,7 @@ class ArchManager(SoftwareManager):
else:
res.new.append(pkg)
Thread(target=self.mapper.fill_package_build, args=(pkg,), daemon=True).start()
Thread(target=self.aur_mapper.fill_package_build, args=(pkg,), daemon=True).start()
def _search_in_repos_and_fill(self, words: str, disk_loader: DiskCacheLoader, read_installed: Thread, installed: List[ArchPackage], res: SearchResult):
repo_search = pacman.search(words)
@@ -383,7 +386,7 @@ class ArchManager(SoftwareManager):
read_installed.join()
aur_installed = {p.name: p for p in installed if p.repository == 'aur'}
downgrade_enabled = git.is_enabled()
downgrade_enabled = git.is_installed()
for pkgdata in api_res['results']:
self._upgrade_search_result(pkgdata, aur_installed, downgrade_enabled, res, disk_loader)
@@ -407,7 +410,7 @@ class ArchManager(SoftwareManager):
if pkgsinfo:
read_installed.join()
aur_installed = {p.name: p for p in installed if p.repository == 'aur'}
downgrade_enabled = git.is_enabled()
downgrade_enabled = git.is_installed()
for pkgdata in pkgsinfo:
self._upgrade_search_result(pkgdata, aur_installed, downgrade_enabled, res, disk_loader)
@@ -452,7 +455,7 @@ class ArchManager(SoftwareManager):
def _fill_aur_pkgs(self, aur_pkgs: dict, output: List[ArchPackage], disk_loader: DiskCacheLoader, internet_available: bool,
arch_config: dict):
downgrade_enabled = git.is_enabled()
downgrade_enabled = git.is_installed()
if internet_available:
try:
@@ -461,14 +464,18 @@ class ArchManager(SoftwareManager):
if pkgsinfo:
editable_pkgbuilds = self._read_editable_pkgbuilds() if arch_config['edit_aur_pkgbuild'] is not False else None
for pkgdata in pkgsinfo:
pkg = self.mapper.map_api_data(pkgdata, aur_pkgs, self.categories)
pkg = self.aur_mapper.map_api_data(pkgdata, aur_pkgs, self.categories)
pkg.downgrade_enabled = downgrade_enabled
pkg.pkgbuild_editable = pkg.name in editable_pkgbuilds if editable_pkgbuilds is not None else None
if pkg.installed:
if disk_loader:
disk_loader.fill(pkg)
pkg.status = PackageStatus.READY
disk_loader.fill(pkg, sync=True)
pkg.update = self._check_aur_package_update(pkg=pkg,
installed_data=aur_pkgs.get(pkg.name, {}),
api_data=pkgdata)
pkg.status = PackageStatus.READY
output.append(pkg)
return
@@ -489,10 +496,24 @@ class ArchManager(SoftwareManager):
if disk_loader:
disk_loader.fill(pkg)
pkg.status = PackageStatus.READY
pkg.status = PackageStatus.READY
output.append(pkg)
def _check_aur_package_update(self, pkg: ArchPackage, installed_data: dict, api_data: dict) -> bool:
if pkg.last_modified is None: # if last_modified is not available, then the install_date will be used instead
install_date = installed_data.get('install_date')
if install_date:
try:
pkg.install_date = datetime_as_milis(parse_date(install_date))
except ValueError:
self.logger.error("Could not parse 'install_date' ({}) from AUR package '{}'".format(install_date, pkg.name))
else:
self.logger.error("AUR package '{}' install_date was not retrieved".format(pkg.name))
return self.aur_mapper.check_update(pkg=pkg, last_modified=api_data['LastModified'])
def _fill_repo_updates(self, updates: dict):
updates.update(pacman.list_repository_updates())
@@ -620,18 +641,21 @@ class ArchManager(SoftwareManager):
context.project_dir = clone_path
srcinfo_path = '{}/.SRCINFO'.format(clone_path)
commits = run_cmd("git log", cwd=clone_path)
logs = git.log_shas_and_timestamps(clone_path)
context.watcher.change_progress(40)
if commits:
commit_list = re.findall(r'commit (.+)\n', commits)
if commit_list:
if len(commit_list) > 1:
srcfields = {'pkgver', 'pkgrel'}
if not logs:
context.watcher.show_message(title=self.i18n['arch.downgrade.error'],
body=self.i18n['arch.downgrade.impossible'].format(
context.name),
type_=MessageType.ERROR)
return False
commit_found = None
for idx in range(1, len(commit_list)):
commit = commit_list[idx]
srcfields = {'pkgver', 'pkgrel', 'epoch'}
commit_found, commit_date = None, None
for idx in range(1, len(logs)):
commit, date = logs[idx][0], logs[idx][1]
with open(srcinfo_path) as f:
pkgsrc = aur.map_srcinfo(string=f.read(), pkgname=context.name, fields=srcfields)
@@ -640,9 +664,16 @@ class ArchManager(SoftwareManager):
context.handler.watcher.print('Could not downgrade anymore. Aborting...')
return False
if '{}-{}'.format(pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')) == context.get_version():
epoch, version, release = pkgsrc.get('epoch'), pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')
if epoch:
current_version = '{}:{}-{}'.format(epoch, version, release)
else:
current_version = '{}-{}'.format(version, release)
if current_version == context.get_version():
# current version found
commit_found = commit
commit_found, commit_date = commit, date
elif commit_found:
context.watcher.change_substatus(self.i18n['arch.downgrade.version_found'])
checkout_proc = new_subprocess(['git', 'checkout', commit_found], cwd=clone_path)
@@ -658,18 +689,8 @@ class ArchManager(SoftwareManager):
break
context.watcher.change_substatus(self.i18n['arch.downgrade.install_older'])
context.last_modified = commit_date
return self._build(context)
else:
context.watcher.show_message(title=self.i18n['arch.downgrade.error'],
body=self.i18n['arch.downgrade.impossible'].format(context.name),
type_=MessageType.ERROR)
return False
context.watcher.show_message(title=self.i18n['error'],
body=self.i18n['arch.downgrade.no_commits'],
type_=MessageType.ERROR)
return False
finally:
if os.path.exists(context.build_dir) and context.config['aur_remove_build_dir']:
context.handler.handle(SystemProcess(subproc=new_subprocess(['rm', '-rf', context.build_dir])))
@@ -1071,8 +1092,24 @@ class ArchManager(SoftwareManager):
if aur_pkgs:
watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_aur_pkgs']))
self.logger.info("Retrieving the 'last_modified' field for each package to upgrade")
pkgs_api_data = self.aur_client.get_info({p.name for p in aur_pkgs})
if not pkgs_api_data:
self.logger.warning("Could not retrieve the 'last_modified' fields from the AUR API during the upgrade process")
for pkg in aur_pkgs:
watcher.change_substatus("{} {} ({})...".format(self.i18n['manage_window.status.upgrading'], pkg.name, pkg.version))
if pkgs_api_data:
apidata = [p for p in pkgs_api_data if p.get('Name') == pkg.name]
if not apidata:
self.logger.warning("AUR API data from package '{}' could not be found".format(pkg.name))
else:
self.aur_mapper.fill_last_modified(pkg=pkg, api_data=apidata[0])
context = TransactionContext.gen_context_from(pkg=pkg, arch_config=arch_config,
root_password=root_password, handler=handler)
context.change_progress = False
@@ -1329,11 +1366,15 @@ class ArchManager(SoftwareManager):
def _get_info_aur_pkg(self, pkg: ArchPackage) -> dict:
if pkg.installed:
t = Thread(target=self.mapper.fill_package_build, args=(pkg,), daemon=True)
t = Thread(target=self.aur_mapper.fill_package_build, args=(pkg,), daemon=True)
t.start()
info = pacman.get_info_dict(pkg.name)
if pkg.last_modified:
info['last_modified'] = self._parse_timestamp(ts=pkg.last_modified,
error_msg="Could not parse AUR package '{}' 'last_modified' field ({})".format(pkg.name, pkg.last_modified))
t.join()
if pkg.pkgbuild:
@@ -1352,11 +1393,17 @@ class ArchManager(SoftwareManager):
'05_votes': pkg.votes,
'06_package_base': pkg.package_base,
'07_maintainer': pkg.maintainer,
'08_first_submitted': pkg.first_submitted,
'09_last_modified': pkg.last_modified,
'10_url': pkg.url_download
}
if pkg.first_submitted:
info['08_first_submitted'] = self._parse_timestamp(ts=pkg.first_submitted,
error_msg="Could not parse AUR package '{}' 'first_submitted' field".format(pkg.name, pkg.first_submitted))
if pkg.last_modified:
info['09_last_modified'] = self._parse_timestamp(ts=pkg.last_modified,
error_msg="Could not parse AUR package '{}' 'last_modified' field ({})".format(pkg.name, pkg.last_modified))
srcinfo = self.aur_client.get_src_info(pkg.name)
if srcinfo:
@@ -1383,6 +1430,14 @@ class ArchManager(SoftwareManager):
return info
def _parse_timestamp(self, ts: int, error_msg: str) -> datetime:
if ts:
try:
return datetime.fromtimestamp(ts)
except ValueError:
if error_msg:
self.logger.warning(error_msg)
def _get_info_repo_pkg(self, pkg: ArchPackage) -> dict:
info = pacman.get_info_dict(pkg.name, remote=not pkg.installed)
if pkg.installed:
@@ -2205,6 +2260,7 @@ class ArchManager(SoftwareManager):
cache_map = {context.name: ArchPackage(name=context.name,
repository=context.repository,
maintainer=pkg_maintainer,
last_modified=context.last_modified,
categories=self.categories.get(context.name))}
if context.missing_deps:
aur_deps = {dep[0] for dep in context.missing_deps if dep[1] == 'aur'}
@@ -2344,7 +2400,7 @@ class ArchManager(SoftwareManager):
watcher.change_substatus(self.i18n['arch.makepkg.optimizing'])
ArchCompilationOptimizer(arch_config=arch_config, i18n=self.i18n, logger=self.context.logger).optimize()
def install(self, pkg: ArchPackage, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher, context: TransactionContext = None) -> TransactionResult:
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):
@@ -2550,7 +2606,7 @@ class ArchManager(SoftwareManager):
if not self._is_wget_available():
warnings.append(self.i18n['arch.warning.disabled'].format(bold('wget')))
if not git.is_enabled():
if not git.is_installed():
warnings.append(self.i18n['arch.warning.git'].format(bold('git')))
return warnings
@@ -2580,7 +2636,7 @@ class ArchManager(SoftwareManager):
res = []
for pkg in api_res:
if pkg.get('Name') in suggestions:
res.append(PackageSuggestion(self.mapper.map_api_data(pkg, {}, self.categories), suggestions[pkg['Name']]))
res.append(PackageSuggestion(self.aur_mapper.map_api_data(pkg, {}, self.categories), suggestions[pkg['Name']]))
self.logger.info("Mapped {} suggestions".format(len(suggestions)))
return res

View File

@@ -1,10 +1,11 @@
from datetime import datetime
from typing import List
from typing import List, Tuple, Optional
from bauh.commons import system
from bauh.commons.system import new_subprocess
def is_enabled() -> bool:
def is_installed() -> bool:
try:
new_subprocess(['git', '--version'])
return True
@@ -27,3 +28,18 @@ def list_commits(proj_dir:str) -> List[dict]:
commit = {}
return commits
def log_shas_and_timestamps(repo_path: str) -> Optional[List[Tuple[str, int]]]:
code, output = system.execute(cmd='git log --format="%H %at"', shell=True, cwd=repo_path)
if code == 0:
logs = []
for line in output.strip().split('\n'):
line_strip = line.strip()
if line_strip:
line_split = line_strip.split(' ')
logs.append((line_split[0].strip(), int(line_split[1].strip())))
return logs

View File

@@ -1,6 +1,7 @@
import logging
import os
import traceback
from datetime import datetime
from typing import Optional
from colorama import Fore
from pkg_resources import parse_version
@@ -13,41 +14,56 @@ from bauh.view.util.translation import I18n
URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}'
class ArchDataMapper:
class AURDataMapper:
def __init__(self, http_client: HttpClient, i18n: I18n):
def __init__(self, http_client: HttpClient, i18n: I18n, logger: logging.Logger):
self.http_client = http_client
self.i18n = i18n
self.logger = logger
def fill_api_data(self, pkg: ArchPackage, package: dict, fill_version: bool = True):
def fill_last_modified(self, pkg: ArchPackage, api_data: dict):
last_modified = api_data.get('LastModified')
if last_modified is not None and isinstance(last_modified, int):
pkg.last_modified = last_modified
self.logger.info("'last_modified' field ({}) set to package '{}'".format(last_modified, pkg.name))
else:
self.logger.warning("Could not set the 'last_modified' field ({}) to package '{}'".format(last_modified, pkg.name))
version = package.get('Version')
def fill_api_data(self, pkg: ArchPackage, api_data: dict, fill_version: bool = True):
pkg.id = api_data.get('ID')
if not pkg.name:
pkg.name = api_data.get('Name')
if not pkg.description:
pkg.description = api_data.get('Description')
pkg.package_base = api_data.get('PackageBase')
pkg.popularity = api_data.get('Popularity')
pkg.votes = api_data.get('NumVotes')
pkg.maintainer = api_data.get('Maintainer')
pkg.url_download = URL_PKG_DOWNLOAD.format(api_data['URLPath']) if api_data.get('URLPath') else None
if api_data['FirstSubmitted'] and isinstance(api_data['FirstSubmitted'], int):
pkg.first_submitted = api_data['FirstSubmitted']
if not pkg.installed:
self.fill_last_modified(pkg=pkg, api_data=api_data)
version = api_data.get('Version')
if version:
version = version.split(':')
version = version[0] if len(version) == 1 else version[1]
pkg.id = package.get('ID')
pkg.name = package.get('Name')
if fill_version:
pkg.version = version
pkg.latest_version = version
pkg.description = package.get('Description')
pkg.package_base = package.get('PackageBase')
pkg.popularity = package.get('Popularity')
pkg.votes = package.get('NumVotes')
pkg.maintainer = package.get('Maintainer')
pkg.url_download = URL_PKG_DOWNLOAD.format(package['URLPath']) if package.get('URLPath') else None
pkg.first_submitted = datetime.fromtimestamp(package['FirstSubmitted']) if package.get('FirstSubmitted') else None
pkg.last_modified = datetime.fromtimestamp(package['LastModified']) if package.get('LastModified') else None
pkg.update = self.check_update(pkg.version, pkg.latest_version)
@staticmethod
def check_update(version: str, latest_version: str) -> bool:
if version and latest_version:
def check_version_update(version: str, latest_version: str) -> bool:
if version and latest_version and version != latest_version:
try:
ver_epoch, latest_epoch = version.split(':'), latest_version.split(':')
@@ -82,8 +98,8 @@ class ArchDataMapper:
if res and res.status_code == 200 and res.text:
pkg.pkgbuild = res.text
def map_api_data(self, apidata: dict, installed: dict, categories: dict) -> ArchPackage:
data = installed.get(apidata.get('Name')) if installed else None
def map_api_data(self, apidata: dict, pkgs_installed: Optional[dict], categories: dict) -> ArchPackage:
data = pkgs_installed.get(apidata.get('Name')) if pkgs_installed else None
app = ArchPackage(name=apidata.get('Name'), installed=bool(data), repository='aur', i18n=self.i18n)
app.status = PackageStatus.LOADING_DATA
@@ -95,5 +111,26 @@ class ArchDataMapper:
app.description = data.get('description')
self.fill_api_data(app, apidata, fill_version=not data)
return app
def check_update(self, pkg: ArchPackage, last_modified: Optional[int]) -> bool:
valid_last_modified = last_modified is not None and isinstance(last_modified, int)
if not valid_last_modified:
self.logger.warning("'last_modified' timestamp informed for package '{}' is invalid: {}".format(pkg.name, valid_last_modified))
pkg_last_modified_ts = pkg.last_modified if pkg.last_modified is not None else pkg.install_date
if pkg.last_modified is None:
self.logger.warning("AUR package '{}' has no 'last_modified' field set.".format(pkg.name))
if pkg.install_date is None:
self.logger.warning("AUR package '{}' has no 'install_date' field set".format(pkg.name))
self.logger.warning("Update checking for AUR package '{}' will only consider version strings".format(pkg.name))
else:
self.logger.warning("AUR package {} 'install_date' field will be used for update checking".format(pkg.name))
if pkg_last_modified_ts is not None and valid_last_modified and pkg_last_modified_ts < last_modified:
return True
else:
return self.check_version_update(pkg.version, pkg.latest_version)

View File

@@ -1,12 +1,11 @@
import datetime
from typing import List, Set
from typing import List, Set, Optional
from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction
from bauh.commons import resource
from bauh.gems.arch import ROOT_DIR, ARCH_CACHE_PATH
from bauh.view.util.translation import I18n
CACHED_ATTRS = {'command', 'icon_path', 'repository', 'maintainer', 'desktop_entry', 'categories'}
CACHED_ATTRS = {'command', 'icon_path', 'repository', 'maintainer', 'desktop_entry', 'categories', 'last_modified'}
ACTIONS_AUR_ENABLE_PKGBUILD_EDITION = [CustomSoftwareAction(i18n_label_key='arch.action.enable_pkgbuild_edition',
i18n_status_key='arch.action.enable_pkgbuild_edition.status',
@@ -27,11 +26,11 @@ class ArchPackage(SoftwarePackage):
def __init__(self, name: str = None, version: str = None, latest_version: str = None, description: str = None,
package_base: str = None, votes: int = None, popularity: float = None,
first_submitted: datetime.datetime = None, last_modified: datetime.datetime = None,
first_submitted: Optional[int] = None, last_modified: Optional[int] = None,
maintainer: str = None, url_download: str = None, pkgbuild: str = None, repository: str = None,
desktop_entry: str = None, installed: bool = False, srcinfo: dict = None, dependencies: Set[str] = None,
categories: List[str] = None, i18n: I18n = None, update_ignored: bool = False, arch: str = None,
pkgbuild_editable: bool = None):
pkgbuild_editable: bool = None, install_date: Optional[int] = None):
super(ArchPackage, self).__init__(name=name, version=version, latest_version=latest_version, description=description,
installed=installed, categories=categories)
@@ -55,6 +54,7 @@ class ArchPackage(SoftwarePackage):
self.update_ignored = update_ignored
self.view_name = name # name displayed on the view
self.pkgbuild_editable = pkgbuild_editable # if the PKGBUILD can be edited by the user (only for AUR)
self.install_date = install_date
@staticmethod
def disk_cache_path(pkgname: str):

View File

@@ -15,7 +15,7 @@ RE_DEPS = re.compile(r'[\w\-_]+:[\s\w_\-\.]+\s+\[\w+\]')
RE_OPTDEPS = re.compile(r'[\w\._\-]+\s*:')
RE_DEP_NOTFOUND = re.compile(r'error:.+\'(.+)\'')
RE_DEP_OPERATORS = re.compile(r'[<>=]')
RE_INSTALLED_FIELDS = re.compile(r'(Name|Description|Version|Validated By)\s*:\s*(.+)')
RE_INSTALLED_FIELDS = re.compile(r'(Name|Description|Version|Install Date|Validated By)\s*:\s*(.+)')
RE_INSTALLED_SIZE = re.compile(r'Installed Size\s*:\s*([0-9,\.]+)\s(\w+)\n?', re.IGNORECASE)
RE_DOWNLOAD_SIZE = re.compile(r'Download Size\s*:\s*([0-9,\.]+)\s(\w+)\n?', re.IGNORECASE)
RE_UPDATE_REQUIRED_FIELDS = re.compile(r'(\bProvides\b|\bInstalled Size\b|\bConflicts With\b)\s*:\s(.+)\n')
@@ -114,9 +114,11 @@ def map_installed(names: Iterable[str] = None) -> dict: # returns a dict with w
if field_tuple[0].startswith('N'):
current_pkg['name'] = field_tuple[1].strip()
elif field_tuple[0].startswith('Ve'):
current_pkg['version'] = field_tuple[1].split(':')[-1].strip()
current_pkg['version'] = field_tuple[1].strip()
elif field_tuple[0].startswith('D'):
current_pkg['description'] = field_tuple[1].strip()
elif field_tuple[0].startswith('I'):
current_pkg['install_date'] = field_tuple[1].strip()
elif field_tuple[0].startswith('Va'):
if field_tuple[1].strip().lower() == 'none':
pkgs['not_signed'][current_pkg['name']] = current_pkg
@@ -449,8 +451,7 @@ def search(words: str) -> Dict[str, dict]:
data_split = repo_split[1].split(' ')
current['name'] = data_split[0]
version = data_split[1].split(':')
current['version'] = version[0] if len(version) == 1 else version[1]
current['version'] = data_split[1]
return found

View File

@@ -144,6 +144,7 @@ arch.info.install script=script d'instal·lació
arch.info.install script.no=no
arch.info.installed files=arxius instal·lats
arch.info.installed size=mida instal·lat
arch.info.last_modified=última modificació
arch.info.license=llicència
arch.info.licenses=llicències
arch.info.licenses.custom=costum

View File

@@ -144,6 +144,7 @@ arch.info.install script=Installationsskript
arch.info.install script.no=Nein
arch.info.installed files=installierten Dateien
arch.info.installed size=installierte Größe
arch.info.last_modified=zuletzt bearbeitet
arch.info.license=Lizenz
arch.info.licenses=Lizenzen
arch.info.licenses.custom=Benutzerdefiniert

View File

@@ -144,6 +144,7 @@ arch.info.install script=install script
arch.info.install script.no=no
arch.info.installed files=installed files
arch.info.installed size=installed size
arch.info.last_modified=last modified
arch.info.license=license
arch.info.licenses=licenses
arch.info.licenses.custom=custom

View File

@@ -144,6 +144,7 @@ arch.info.install script=script de instalación
arch.info.install script.no=ninguno
arch.info.installed files=archivos instalados
arch.info.installed size=tamaño da instalación
arch.info.last_modified=última modificación
arch.info.license=licencia
arch.info.licenses=licencias
arch.info.licenses.custom=personalizada

View File

@@ -144,6 +144,7 @@ arch.info.install script=script d'installation
arch.info.install script.no=non
arch.info.installed files=fichiers installés
arch.info.installed size=taille installée
arch.info.last_modified=dernière modification
arch.info.license=licence
arch.info.licenses=licences
arch.info.licenses.custom=personnalisé

View File

@@ -144,6 +144,7 @@ arch.info.install script=script di installazione
arch.info.install script.no=nessuno
arch.info.installed files=file installati
arch.info.installed size=taglia installata
arch.info.last_modified=Ultima modifica
arch.info.license=licenza
arch.info.licenses=licenze
arch.info.licenses.custom=costume

View File

@@ -143,6 +143,7 @@ arch.info.install script=script de instalação
arch.info.install script.no=nenhum
arch.info.installed files=arquivos instalados
arch.info.installed size=tamanho da instalação
arch.info.last_modified=última modificação
arch.info.license=licença
arch.info.licenses=licenças
arch.info.licenses.custom=personalizada

View File

@@ -144,6 +144,7 @@ arch.info.install script=Скрипт установки
arch.info.install script.no=Нет
arch.info.installed files=установленные файлы
arch.info.installed size=Размер установки
arch.info.last_modified=Последнее изменение
arch.info.license=Лицензия
arch.info.licenses=Лицензии
arch.info.licenses.custom=таможенная

View File

@@ -144,6 +144,7 @@ arch.info.install script=kurulum betiği
arch.info.install script.no=hayır
arch.info.installed files=yüklü dosyalar
arch.info.installed size=kurulum boyutu
arch.info.last_modified=son değişiklik
arch.info.license=lisans
arch.info.licenses=lisanslar
arch.info.licenses.custom=özel

View File

@@ -390,6 +390,7 @@ style=estil
success=success
summary=resum
task.download_categories=Download de categories [ {} ]
task.waiting_task=Waiting for {}
type=tipus
uninstall=desinstal·la
uninstalled=desinstal·lada

View File

@@ -390,6 +390,7 @@ style=Stil
success=success
summary=Zusammenfassung
task.download_categories=Downloading categories [ {} ]
task.waiting_task=Waiting for {}
type=Typ
uninstall=Deinstallation
uninstalled=deinstalliert

View File

@@ -391,6 +391,7 @@ style=style
success=success
summary=summary
task.download_categories=Downloading categories [ {} ]
task.waiting_task=Waiting for {}
type=type
uninstall=uninstall
uninstalled=uninstalled

View File

@@ -393,6 +393,7 @@ style=estilo
success=éxito
summary=resumen
task.download_categories=Descargando categorías [ {} ]
task.waiting_task=Esperando por {}
type=tipo
uninstall=desinstalar
uninstalled=desinstalada

View File

@@ -387,6 +387,7 @@ style=style
success=succès
summary=résumé
task.download_categories=Téléchargement des catégories [ {} ]
task.waiting_task=Waiting for {}
type=type
uninstall=désinstaller
uninstalled=désinstallé

View File

@@ -393,6 +393,7 @@ style=stile
success=success
summary=riepilogo
task.download_categories=Download delle categorie [ {} ]
task.waiting_task=Waiting for {}
type=tipe
uninstall=Disinstalla
uninstalled=disinstallato

View File

@@ -391,6 +391,7 @@ style=estilo
success=sucesso
summary=resumo
task.download_categories=Baixando categorias [ {} ]
task.waiting_task=Aguardando {}
type=tipo
uninstall=desinstalar
uninstalled=desinstalado

View File

@@ -390,6 +390,7 @@ style=Стиль
success=success
summary=Суммарно
task.download_categories=Downloading categories [ {} ]
task.waiting_task=Waiting for {}
type=Тип
uninstall=Деинсталляция
uninstalled=Деинсталлировано

View File

@@ -390,6 +390,7 @@ style=Görünüm biçimi
success=başarılı
summary=özet
task.download_categories=Kategoriler indiriliyor [ {} ]
task.waiting_task=Waiting for {}
type=tür
uninstall=kaldır
uninstalled=kaldırıldı

View File

@@ -23,13 +23,17 @@ class AsyncDiskCacheLoader(Thread, DiskCacheLoader):
self.logger = logger
self.processed = 0
def fill(self, pkg: SoftwarePackage):
def fill(self, pkg: SoftwarePackage, sync: bool = False):
"""
Adds a package which data must be read from the disk to a queue.
Adds a package which data must be read from the disk to a queue (if not sync)
:param pkg:
:param sync:
:return:
"""
if pkg and pkg.supports_disk_cache():
if sync:
self._fill_cached_data(pkg)
else:
self.pkgs.append(pkg)
def stop_working(self):

View File

@@ -2,3 +2,4 @@ pyqt5>=5.12
requests>=2.18
colorama>=0.3.8
pyyaml>=3.13
python-dateutil>=2.7

View File

@@ -1,45 +0,0 @@
from unittest import TestCase
from bauh.gems.arch.mapper import ArchDataMapper
class ArchDataMapperTest(TestCase):
def test_check_update(self):
self.assertTrue(ArchDataMapper.check_update('1.0.0-1', '1.0.0-2'))
self.assertFalse(ArchDataMapper.check_update('1.0.0-2', '1.0.0-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.0-5', '1.0.1-1'))
self.assertFalse(ArchDataMapper.check_update('1.0.1-1', '1.0.0-1'))
self.assertTrue(ArchDataMapper.check_update('1.0.5-5', '1.1.0-2'))
self.assertFalse(ArchDataMapper.check_update('1.1.0-2', '1.0.5-5'))
self.assertTrue(ArchDataMapper.check_update('1.5.0-2', '1.5.1-1'))
self.assertFalse(ArchDataMapper.check_update('1.5.1-1', '1.5.0-2'))
self.assertTrue(ArchDataMapper.check_update('1.5.1-1', '1.5.1-2'))
self.assertTrue(ArchDataMapper.check_update('1.5.1-1', '2.0.0-1'))
self.assertFalse(ArchDataMapper.check_update('2.0.0-1', '1.5.1-1'))
self.assertTrue(ArchDataMapper.check_update('77.0.3865.90-1', '77.0.3865.120-1'))
self.assertTrue(ArchDataMapper.check_update('77.0.3865.90-1', '77.0.3865.90-2'))
self.assertFalse(ArchDataMapper.check_update('77.0.3865.900-1', '77.0.3865.120-1'))
self.assertTrue(ArchDataMapper.check_update('77.0.3865.120-1', '77.0.3865.900-1'))
self.assertFalse(ArchDataMapper.check_update('77.0.3865.120-1', '77.0.3865.90-1'))
self.assertTrue(ArchDataMapper.check_update('77.0.3865.a-1', '77.0.3865.b-1'))
self.assertFalse(ArchDataMapper.check_update('77.0.b.0-1', '77.0.a.1-1'))
self.assertFalse(ArchDataMapper.check_update('r25.e22697c-1', 'r8.19fe011-1'))
self.assertTrue(ArchDataMapper.check_update('0.9.7.RC-9', '0.9.7.RC-10'))
self.assertFalse(ArchDataMapper.check_update('1.1.0.r11.caacf30-1', 'r65.4c7144a-1'))
self.assertFalse(ArchDataMapper.check_update('1.2.16.r688.8b2c199-1', 'r2105.e91f0e9-3'))
def test_check_update__versions_with_epics(self):
self.assertTrue(ArchDataMapper.check_update('1.2-1', '1:1.1-1'))
self.assertFalse(ArchDataMapper.check_update('1:1.1-1', '1.2-1'))
self.assertTrue(ArchDataMapper.check_update('1:1.2-1', '2:0.1-1'))
self.assertFalse(ArchDataMapper.check_update('2:0.1-1', '1:1.2-1'))
self.assertTrue(ArchDataMapper.check_update('10:1.1-1', '10:1.2-1'))
self.assertFalse(ArchDataMapper.check_update('10:1.2-1', '10:1.2-1'))
self.assertTrue(ArchDataMapper.check_update('9:1.2-1', '10:0.1-1'))
self.assertTrue(ArchDataMapper.check_update('9:1.1.1.1-2', '10:0.0'))
self.assertFalse(ArchDataMapper.check_update('10:0.0', '9:1.1.1.1-2'))

View File

@@ -0,0 +1,178 @@
from unittest import TestCase
from unittest.mock import Mock
from bauh.gems.arch.mapper import AURDataMapper
from bauh.gems.arch.model import ArchPackage
class ArchDataMapperTest(TestCase):
def test_check_version_update(self):
self.assertTrue(AURDataMapper.check_version_update('1.0.0-1', '1.0.0-2'))
self.assertFalse(AURDataMapper.check_version_update('1.0.0-2', '1.0.0-1'))
self.assertTrue(AURDataMapper.check_version_update('1.0.0-5', '1.0.1-1'))
self.assertFalse(AURDataMapper.check_version_update('1.0.1-1', '1.0.0-1'))
self.assertTrue(AURDataMapper.check_version_update('1.0.5-5', '1.1.0-2'))
self.assertFalse(AURDataMapper.check_version_update('1.1.0-2', '1.0.5-5'))
self.assertTrue(AURDataMapper.check_version_update('1.5.0-2', '1.5.1-1'))
self.assertFalse(AURDataMapper.check_version_update('1.5.1-1', '1.5.0-2'))
self.assertTrue(AURDataMapper.check_version_update('1.5.1-1', '1.5.1-2'))
self.assertTrue(AURDataMapper.check_version_update('1.5.1-1', '2.0.0-1'))
self.assertFalse(AURDataMapper.check_version_update('2.0.0-1', '1.5.1-1'))
self.assertTrue(AURDataMapper.check_version_update('77.0.3865.90-1', '77.0.3865.120-1'))
self.assertTrue(AURDataMapper.check_version_update('77.0.3865.90-1', '77.0.3865.90-2'))
self.assertFalse(AURDataMapper.check_version_update('77.0.3865.900-1', '77.0.3865.120-1'))
self.assertTrue(AURDataMapper.check_version_update('77.0.3865.120-1', '77.0.3865.900-1'))
self.assertFalse(AURDataMapper.check_version_update('77.0.3865.120-1', '77.0.3865.90-1'))
self.assertTrue(AURDataMapper.check_version_update('77.0.3865.a-1', '77.0.3865.b-1'))
self.assertFalse(AURDataMapper.check_version_update('77.0.b.0-1', '77.0.a.1-1'))
self.assertFalse(AURDataMapper.check_version_update('r25.e22697c-1', 'r8.19fe011-1'))
self.assertTrue(AURDataMapper.check_version_update('0.9.7.RC-9', '0.9.7.RC-10'))
self.assertFalse(AURDataMapper.check_version_update('1.1.0.r11.caacf30-1', 'r65.4c7144a-1'))
self.assertFalse(AURDataMapper.check_version_update('1.2.16.r688.8b2c199-1', 'r2105.e91f0e9-3'))
def test_check_version_update__versions_with_epics(self):
self.assertTrue(AURDataMapper.check_version_update('1.2-1', '1:1.1-1'))
self.assertFalse(AURDataMapper.check_version_update('1:1.1-1', '1.2-1'))
self.assertTrue(AURDataMapper.check_version_update('1:1.2-1', '2:0.1-1'))
self.assertFalse(AURDataMapper.check_version_update('2:0.1-1', '1:1.2-1'))
self.assertTrue(AURDataMapper.check_version_update('10:1.1-1', '10:1.2-1'))
self.assertFalse(AURDataMapper.check_version_update('10:1.2-1', '10:1.2-1'))
self.assertTrue(AURDataMapper.check_version_update('9:1.2-1', '10:0.1-1'))
self.assertTrue(AURDataMapper.check_version_update('9:1.1.1.1-2', '10:0.0'))
self.assertFalse(AURDataMapper.check_version_update('10:0.0', '9:1.1.1.1-2'))
def test_check_update__pkg_no_last_modified_and_same_versions(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = None
pkg.version = '1.0.0'
pkg.latest_version = pkg.version
self.assertFalse(mapper.check_update(pkg=pkg, last_modified=1608143812))
def test_check_update__pkg_no_last_modified_and_latest_version_higher_than_version(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = None
pkg.version = '1.0.0'
pkg.latest_version = '1.1.0'
self.assertTrue(mapper.check_update(pkg=pkg, last_modified=1608143812))
def test_check_update__pkg_no_last_modified_and_no_install_date_and_version_higher_than_latest_version(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = None
pkg.install_date = None
pkg.version = '1.1.0'
pkg.latest_version = '1.0.0'
self.assertFalse(mapper.check_update(pkg=pkg, last_modified=1608143812))
def test_check_update__none_last_modified_and_latest_version_higher_than_version(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = 1608143812
pkg.version = '1.0.0'
pkg.latest_version = '1.1.0'
self.assertTrue(mapper.check_update(pkg=pkg, last_modified=None))
def test_check_update__none_last_modified_and_version_equal_latest_version(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = 1608143812
pkg.version = '1.0.0'
pkg.latest_version = pkg.version
self.assertFalse(mapper.check_update(pkg=pkg, last_modified=None))
def test_check_update__string_last_modified_and_latest_version_higher_than_version(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = 1608143812
pkg.version = '1.0.0'
pkg.latest_version = '1.1.0'
self.assertTrue(mapper.check_update(pkg=pkg, last_modified='abc'))
def test_check_update__pkg_last_modified_equal_last_modified_and_version_equal_latest_version(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = 1608143812
pkg.version = '1.0.0'
pkg.latest_version = pkg.version
self.assertFalse(mapper.check_update(pkg=pkg, last_modified=pkg.last_modified))
def test_check_update__pkg_last_modified_higher_than_last_modified_and_latest_version_higher_than_version(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = 1608143812
pkg.version = '1.0.0'
pkg.latest_version = '1.1.0'
self.assertTrue(mapper.check_update(pkg=pkg, last_modified=pkg.last_modified - 100))
def test_check_update__pkg_last_modified_less_than_last_modified_and_version_higher_than_latest_version(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = 1608143812
pkg.version = '2.0.0'
pkg.latest_version = '1.0.0'
# in this case, last modified is more relevant than the string version
self.assertTrue(mapper.check_update(pkg=pkg, last_modified=pkg.last_modified + 100))
def test_check_update__pkg_no_last_modified_and_install_date_less_than_last_modified_and_version_higher_than_latest(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = None
pkg.install_date = 1608143812
pkg.version = '3.0.0'
pkg.latest_version = '2.0.0'
# in this case, install_date will be considered instead of package's last_modified.
# even that 'version' is higher than 'latest_version', 'last_modified' is greater than 'install_date'
self.assertTrue(mapper.check_update(pkg=pkg, last_modified=pkg.install_date + 100))
def test_check_update__pkg_no_last_modified_and_install_date_higher_than_last_modified_and_version_equal_latest(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = None
pkg.install_date = 1608143812
pkg.version = '2.0.0'
pkg.latest_version = pkg.version
# in this case, install_date will be considered instead of package's last_modified.
# as 'install_date' is higher, only the string versions will be compared
self.assertFalse(mapper.check_update(pkg=pkg, last_modified=pkg.install_date - 100))
def test_check_update__pkg_no_last_modified_and_install_date_higher_than_last_modified_and_latest_higher(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = None
pkg.install_date = 1608143812
pkg.version = '1.0.0'
pkg.latest_version = '1.1.0'
# in this case, install_date will be considered instead of package's last_modified.
# as 'install_date' is higher, only the string versions will be compared
self.assertTrue(mapper.check_update(pkg=pkg, last_modified=pkg.install_date - 100))
def test_check_update__pkg_no_last_modified_and_install_date_and_no_last_modified_and_latest_higher(self):
mapper = AURDataMapper(i18n=Mock(), logger=Mock(), http_client=Mock())
pkg = ArchPackage(name='test')
pkg.last_modified = None
pkg.install_date = 1608143812
pkg.version = '1.0.0'
pkg.latest_version = '1.1.0'
# in this case, install_date will be considered instead of package's last_modified.
# as 'install_date' is higher, only the string versions will be compared
self.assertTrue(mapper.check_update(pkg=pkg, last_modified=None))