From 19cd85bc98df3be5604c37499440c8f740c6a7db Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 4 Aug 2020 09:55:42 -0300 Subject: [PATCH 01/99] [ui] -> fix: crashing when nothing can be upgraded --- CHANGELOG.md | 6 ++++++ bauh/api/abstract/handler.py | 4 +++- bauh/view/qt/confirmation.py | 22 +++++++++++++++------- bauh/view/qt/thread.py | 23 ++++++++++++++++------- bauh/view/qt/window.py | 1 + 5 files changed, 41 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea9145d..9c979da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ 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.7] 2020 +### Fixes +- UI + - crashing when nothing can be upgraded + + ## [0.9.6] 2020-06-26 ### Improvements - AppImage diff --git a/bauh/api/abstract/handler.py b/bauh/api/abstract/handler.py index d4c9f4ed..e87ed0da 100644 --- a/bauh/api/abstract/handler.py +++ b/bauh/api/abstract/handler.py @@ -18,7 +18,8 @@ def print(self, msg: str): pass def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, confirmation_label: str = None, - deny_label: str = None, deny_button: bool = True, window_cancel: bool = False) -> bool: + deny_label: str = None, deny_button: bool = True, window_cancel: bool = False, + confirmation_button: bool = True) -> bool: """ request a user confirmation. In the current GUI implementation, it shows a popup to the user. :param title: popup title @@ -28,6 +29,7 @@ def request_confirmation(self, title: str, body: str, components: List[ViewCompo :param deny_label: optional deny button label (default to 'no') :param deny_button: if the deny button should be displayed :param window_cancel: if the window cancel button should be visible + :param confirmation_button: if the confirmation button should be displayed :return: if the request was confirmed by the user """ pass diff --git a/bauh/view/qt/confirmation.py b/bauh/view/qt/confirmation.py index 4d30cd97..85bc8410 100644 --- a/bauh/view/qt/confirmation.py +++ b/bauh/view/qt/confirmation.py @@ -12,7 +12,8 @@ class ConfirmationDialog(QMessageBox): def __init__(self, title: str, body: str, i18n: I18n, screen_size: QSize, components: List[ViewComponent] = None, - confirmation_label: str = None, deny_label: str = None, deny_button: bool = True, window_cancel: bool = True): + confirmation_label: str = None, deny_label: str = None, deny_button: bool = True, window_cancel: bool = True, + confirmation_button: bool = True): super(ConfirmationDialog, self).__init__() if not window_cancel: @@ -20,12 +21,19 @@ def __init__(self, title: str, body: str, i18n: I18n, screen_size: QSize, compo self.setWindowTitle(title) self.setStyleSheet('QLabel { margin-right: 25px; }') - self.bt_yes = self.addButton(i18n['popup.button.yes'] if not confirmation_label else confirmation_label.capitalize(), QMessageBox.YesRole) - self.bt_yes.setStyleSheet(css.OK_BUTTON) - self.setDefaultButton(self.bt_yes) + + self.bt_yes = None + if confirmation_button: + self.bt_yes = self.addButton(i18n['popup.button.yes'] if not confirmation_label else confirmation_label.capitalize(), QMessageBox.YesRole) + self.bt_yes.setStyleSheet(css.OK_BUTTON) + self.setDefaultButton(self.bt_yes) if deny_button: - self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole) + self.bt_no = self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole) + + if not confirmation_button: + self.bt_no.setStyleSheet(css.OK_BUTTON) + self.setDefaultButton(self.bt_no) label = None if body: @@ -68,5 +76,5 @@ def __init__(self, title: str, body: str, i18n: I18n, screen_size: QSize, compo self.exec_() - def is_confirmed(self): - return self.clickedButton() == self.bt_yes + def is_confirmed(self) -> bool: + return bool(self.bt_yes and self.clickedButton() == self.bt_yes) diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 9d2d638e..6915b7d8 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -50,9 +50,14 @@ def __init__(self): self.stop = False def request_confirmation(self, title: str, body: str, components: List[ViewComponent] = None, - confirmation_label: str = None, deny_label: str = None, deny_button: bool = True, window_cancel: bool = False) -> bool: + confirmation_label: str = None, deny_label: str = None, deny_button: bool = True, + window_cancel: bool = False, + confirmation_button: bool = True) -> bool: self.wait_confirmation = True - self.signal_confirmation.emit({'title': title, 'body': body, 'components': components, 'confirmation_label': confirmation_label, 'deny_label': deny_label, 'deny_button': deny_button, 'window_cancel': window_cancel}) + self.signal_confirmation.emit({'title': title, 'body': body, 'components': components, + 'confirmation_label': confirmation_label, 'deny_label': deny_label, + 'deny_button': deny_button, 'window_cancel': window_cancel, + 'confirmation_button': confirmation_button}) self.wait_user() return self.confirmation_res @@ -399,10 +404,13 @@ def run(self): if requirements.to_remove: comps.append(self._gen_to_remove_form(requirements.to_remove)) - updates_form, updates_size = self._gen_to_update_form(requirements.to_upgrade) - required_size += updates_size[0] - extra_size += updates_size[1] - comps.append(updates_form) + can_upgrade = False + if requirements.to_upgrade: + can_upgrade = True + updates_form, updates_size = self._gen_to_update_form(requirements.to_upgrade) + required_size += updates_size[0] + extra_size += updates_size[1] + comps.append(updates_form) extra_size_text = '{}: {}'.format(self.i18n['action.update.total_size'].capitalize(), get_human_size_str(extra_size)) req_size_text = '{}: {}'.format(self.i18n['action.update.required_size'].capitalize(), @@ -411,7 +419,8 @@ def run(self): comps.insert(1, TextComponent('')) if not self.request_confirmation(title=self.i18n['action.update.summary'].capitalize(), body='', components=comps, - confirmation_label=self.i18n['proceed'].capitalize(), deny_label=self.i18n['cancel'].capitalize()): + confirmation_label=self.i18n['proceed'].capitalize(), deny_label=self.i18n['cancel'].capitalize(), + confirmation_button=can_upgrade): self.notify_finished({'success': success, 'updated': updated, 'types': updated_types, 'id': None}) self.pkgs = None return diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 08c85c04..33631520 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -548,6 +548,7 @@ def _ask_confirmation(self, msg: dict): deny_label=msg['deny_label'], deny_button=msg['deny_button'], window_cancel=msg['window_cancel'], + confirmation_button=msg.get('confirmation_button', True), screen_size=self.screen_size) res = diag.is_confirmed() self.thread_animate_progress.animate() From be7652ccf434d1cb9717d7c221f1c230b4b6e844 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 4 Aug 2020 10:53:42 -0300 Subject: [PATCH 02/99] [arch] fix -> not able to upgrade a package that explicitly defines a conflict with itself --- CHANGELOG.md | 2 ++ bauh/gems/arch/updates.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c979da3..b052b3ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.9.7] 2020 ### Fixes +- Arch + - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) - UI - crashing when nothing can be upgraded diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index 9a6a8ad7..3187d11d 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -144,7 +144,7 @@ def _filter_and_map_conflicts(self, context: UpdateRequirementsContext) -> Dict[ for p, data in context.pkgs_data.items(): if data['c']: for c in data['c']: - if c and c in context.installed_names: + if c and c != p and c in context.installed_names: # source = provided_map[c] root_conflict[c] = p From ec614ee849feb60448854bdb9f6e9c36d4cc5502 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 4 Aug 2020 11:51:11 -0300 Subject: [PATCH 03/99] changing version --- bauh/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/__init__.py b/bauh/__init__.py index 0fc6cee2..d4d3d035 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.9.6' +__version__ = '0.9.7' __app_name__ = 'bauh' import os From d288c30484bc7af16c81575283b302c3925de797 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 4 Aug 2020 12:27:33 -0300 Subject: [PATCH 04/99] [arch] fix -> Downloading some AUR packages sources twice when multi-threaded download is enabled --- CHANGELOG.md | 1 + bauh/gems/arch/controller.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b052b3ba..fbeaf601 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) + - Downloading some AUR packages sources twice when multi-threaded download is enabled - UI - crashing when nothing can be upgraded diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index fa884ab9..752ae501 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1487,7 +1487,7 @@ def _pre_download_source(self, project_dir: str, watcher: ProcessWatcher) -> boo if len(fdata) > 1: args.update({'file_url': fdata[1], 'output_path': fdata[0]}) else: - args.update({'file_url': fdata[0], 'output_path': None}) + args.update({'file_url': fdata[0], 'output_path': fdata[0].split('/')[-1]}) if not self.context.file_downloader.download(**args): watcher.print('Could not download source file {}'.format(args['file_url'])) From 4400e60622f3d97a534be0736ff2085333335326 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 4 Aug 2020 15:35:15 -0300 Subject: [PATCH 05/99] [arch] improvement -> upgrade summary: displaying the reason a given package must be installed --- CHANGELOG.md | 7 +++++++ bauh/gems/arch/updates.py | 10 ++++++++-- bauh/view/qt/thread.py | 2 +- pictures/releases/0.9.7/arch_install_reason.png | Bin 0 -> 32027 bytes 4 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 pictures/releases/0.9.7/arch_install_reason.png diff --git a/CHANGELOG.md b/CHANGELOG.md index fbeaf601..3dc1effb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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.7] 2020 +### Improvements +- Arch + - upgrade summary: displaying the reason a given package must be installed +

+ +

+ ### Fixes - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index 3187d11d..ee010f8d 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -334,7 +334,7 @@ def __fill_aur_index(self, context: UpdateRequirementsContext): context.aur_index.update(names) self.logger.info("AUR index loaded on the context") - def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, installed_sizes: Dict[str, int] = None) -> UpgradeRequirement: + def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, installed_sizes: Dict[str, int] = None, to_install: bool = False, to_upgrade: Set[str] = None) -> UpgradeRequirement: requirement = UpgradeRequirement(pkg) if pkg.repository != 'aur': @@ -347,6 +347,11 @@ def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, if current_size is not None and data['s']: requirement.extra_size = data['s'] - current_size + if to_install and context.pkgs_data and context.to_update: + names = context.pkgs_data[pkg.name].get('p', {pkg.name}) + required_by = ','.join([p for p in to_upgrade if p != pkg.name and context.pkgs_data[p]['d'] and any([n for n in names if n in context.pkgs_data[p]['d']])]) + requirement.reason = '{}: {}'.format(self.i18n['arch.info.required by'].capitalize(), required_by if required_by else '?') + return requirement def summarize(self, pkgs: List[ArchPackage], root_password: str, arch_config: dict) -> UpgradeRequirements: @@ -422,6 +427,7 @@ def summarize(self, pkgs: List[ArchPackage], root_password: str, arch_config: di res.cannot_upgrade = [d for d in context.cannot_upgrade.values()] if context.to_install: - res.to_install = [self._map_requirement(p, context) for p in context.to_install.values()] + to_upgrade = {r.pkg.name for r in res.to_upgrade} if res.to_upgrade else None + res.to_install = [self._map_requirement(p, context, to_install=True, to_upgrade=to_upgrade) for p in context.to_install.values()] return res diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 6915b7d8..7a561002 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -236,7 +236,7 @@ def _gen_cannot_update_form(self, reqs: List[UpgradeRequirement]) -> FormCompone return FormComponent(label=self.i18n['action.update.cannot_update_label'], components=comps) def _gen_to_install_form(self, reqs: List[UpgradeRequirement]) -> Tuple[FormComponent, Tuple[int, int]]: - opts = [self._req_as_option(r) for r in reqs] + opts = [self._req_as_option(r, custom_tooltip=r.reason) for r in reqs] comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))] required_size, extra_size = self._sum_pkgs_size(reqs) diff --git a/pictures/releases/0.9.7/arch_install_reason.png b/pictures/releases/0.9.7/arch_install_reason.png new file mode 100644 index 0000000000000000000000000000000000000000..0085061469b0febe57bc922096073b6d1f5a11ab GIT binary patch literal 32027 zcmd?RWmJ{h8};7HWBurUR3KRV`Iua5Rri}D^RV1WG0pRQX zXQ<%68pkS;NJz+EttBLsWh5ltd~$ZQu(mTtLZXlJixZLVeoNA8B$od{_wmu2jo4g4 zotJn{Q;fK|;>CEnZtTtH80p;946M{>IBN&vSHoA5FYiMs={eMLaC*6g`)5UbecR;z zF+*JfqKj48iPmsJEz=yddx(1`Gm=NWa`^j(;>CRxg)RO*2_tW2ZN2Mj-m1>A?2ELs z$mW-1PHfRWHqqo&-(c8i`eBm@9cwQCw6amIc~I`B1n-i5r>CSo3JKaB}Wa2&WFpCRl_MB8#`a zEi+zR?Qd8XM1C~ZnKW}Uo8CE0w;vUaFZKx55EO9`O$$X%5$aq^U?qyDdSV5O_^@^# z+0KCVeCbh}t%Ku{U#2LM4usJUg@oi$9?g*9m8VBjYh%Dz-R&*%YMZm2Z)^R~XVx>a-i|kn-<_^C3)Kx~|!_%#2kFj46+8G|1fo~DH ze$aN6aJ08KcW_0La5gt~H8*?XVeM-5MoLCOSu5xT0TR+1B$@Z`)IALkm)*40=ZQN{ zpvv*+l$4=iCLSM+jRTjLms={!4ob^fTWlLm4i-plJ)XLI5Ku@Lbq+(Z%*--_#tmlO z0?zz>J;`>mud;obxps$zb~A+-OK0r!#h+1peJM`(n92|NKVNPz83Qw#t4^}YL%(AyCkGWo2dZdyTt6Q#RM)XcX@gFCZ~fJE(EMp6dbPtX)%ADR2tyO zVPGiU8oo`-pW>$*gOJr(Eg{)5F_DS+d>e}w^Tfd|cRSW#()w>ZDb=7?DSzg4a-z;`^ew-Knio3vLEV+x zUXQ!CuP@yPi;Tl`Mcl;1WDpV?e3BP(wwsN@ZL5xjfq^la)zJg*?%!Lkry3odfA;*j z$!J%Bu+Is0U?Jwgy3R5Rlbqa-A9*_M-XdC6tB*g9A2P_N9ZAF}9(@`~2=R(Y8Lu$a z^L|b!Yhr?pL=!=<-Rj@A$MLZk)`E6YA`2KiMu`fwM{833JIu&k@xnDY(}^1D&tu#+0z06 z`{+o$5%`5_8Of1kVqvrXa969z&(Nor<~?JZJki!RCgKl<%Y+;j!efTNsPm#Q@$q+j zOBwP0aQXbk#%Ix!DZ%a~6tH90s$BWFH&BBVNzV0Ikf*HH^hy+oHE4SLwtfhY-)lw-TkJ$Z)??wS??^~KjVI^p;}L=? zYJc%(Cr3@5H;mwh-xsJl9e2@Wiw4M~#rGl?>*oqDx*t<;GNyn1`gq21KDYiM>lSOy zXN~>j&?MjB?c(1)O$B{E>qo zEjH>E!!>G_UN;L^1+u-PZBGZG=^?}J>9zz3@$q1i%C`w-^PAPsQF6w2^1bsd{l3TCj0iPfVW>Kj*Xetj5Wyr?1xuOw-Jt0yc-WezX4A*f?nZ#9f$(W5s;MTb2 z)lYMN=Q~_+--rp)8@agHSfs#`lCb|g8@%Gg!H2U#@!Y=#qahDzT*8#%d58AW&)nV9 zkg97(-v3*C>87LY$uA=#^ZLXej{G|2taT^X$d7z zJ^7a>ZqG0=COtC~6W?olHo=_0@}TYOd(B~7aw((A80ML?MAJ`aVwWjQW@2Y2U;FKX zUgsA*EiG+oDxodu!-drB;ocxuox}L!+hKQH#gqquMOc`){FMHIM4CMKp~;n98Lzu!@LxeV<{i;F)RR~d)cr-`*t@n@K7IxMwg zP}6;LcW;2a@0sX&v>6jmsZsi{zTDIEwT3F>q(421M8F9>Nw)WIkl)m{vGFTik2j0j zdSwAvm>8BOl0e>N+`r3dnMyuWH+33*(w+P4u(pMAdcGYsX(g ziq#o^3Yz6=uZu&RS0hqNj#a`AM-}OD(U<-t(|U}OKu*DuRGY08d~&)gk*oaC!X{j= z{g-Y(2M9zTY6tFFz#7j+|?u-W4C*dIlqi@af_ldE9V({w{;ow-nbuJAg$}#-<+{ppZ@#;Md}zO6Si2hDD+ySFEr;;-z>f zo7G5P1=tb-UrC^T%$bj_{`X8V>%zQSbEJTePBACXGy}hBXJlkCR+)AzvxtmEztM|W zM6j{na)%ll2WP$w`VzXrwmnY6D40G?+`#Uz#V+36_3W8%_xdK?^maP|Tf#B}@uuVp zDhX7s(ahG)ngq+K?ODeai(Uxk`N4c}xz1r&z*RIxO4aGwI8M|icrJns)?sq)mf-Se z;5whpnUI^@H}Nff+1TB>#@J^K7lYPH~UyA#>fGt$f?xuY2aJ9@hUy6Yi6+Q&!eVxeW< z1%Gc*!S|bUfgI;|4-c>nrHOjv&WyYf_q8=!&_alsuE2r1s{cp`fw3c-K0*tWu&56YsU$8~1pV(cmykP^eWqbL4fjTf}zA zcH`eee|aeMoh|YPBZYk@1r65KLw8-Yu&A?o=E-MM$;Pbk;GV-Bu#PPmee=O+ZuOKW z`o!^8^*;LL1CJ5&4e{ls6+Wv@W&Qm`QrXnGO|fZAfFh{leoy@omv?d>lFZWOQK83$ zi9a-4UwhKZr^{X}T)E`-=!HEmJizcfE zr%1lPdoscUitU;V75VfGONp)T4wf-`8l~5;4nF4AeywYAfIZ8io94qv_6nx;Y_pJP zL^3uWUcswfm1fEt{j)-qh-*6Ek;`OVd3k1*1^11i`U99aX4i%}6QK~?!_Qck*I`}~ z+(5I_nP!U<969Lu!@i}>tfP3hMMYZUM-DW!^!q=%=axtm6uHBC)v3Ist?T>Cu`mU_ ziQ3gSm*m2k$Nu7kk8u2uA3j))Wl7xxOQEasTa3u2L?2xQ-`iIi-g0;^c^-0E!sWKv z4BJgQW67;%3u*j(F5cutxwYicfeS}o`#%fDz*Bbw+e0y4qsy3RcAIBO{k`d^;yl00 zOiE~|G^a(~IQ^o>TI^^Gp6@c>^%U&0jH1-G-~GJo`4 zCJ)uy=9SUHtc@eNUX3>9faiR)0B&V#;Pj@q9NV8=QaIz0RvYcqbkOK z3Sy?09yGgLk%9RbQsi;E>a_OvmD~J&?XK0*Py2MWM6F80?p6nTZ`Y&ql%bSzZJEhF zOzXwcC*Z+XH#GG1Nphjdu~2g-ntX_ey`BNNxCJYH9J@@kHw1d&XfkNOuwk{N-dr4c9$u# zXfhiH78KA#Bv&f;=Vw!T6JvJ`aCEFKH$8EC%{}dj>2G(jfLV9^H~8;g_KKh9kekE? z=dC$3?@QwMS*QxuQ$H}lT(?PFZE;C~OMFxL)A|ZCqkbo5+IHGJXMxPb)U-tRPX_o| zRIJ;$bVJN$-|h4jL3KvFfO12*?j8GLjVg`~rR>pBk=rS?Ffj2wJ-ye-UluzFgJpIh zJ>EZAD*}751(x-2CRfhFMMkynw#}RS2Gma*Q$!1{hms0a-_FT7BO4thq~-2fJr3tR zl&R)tXX)OZ)*lP~6SDE<$+s@MWY%eq2ZI!!DEo_;XLne~$gODvA5pC z@6}rGBUHrQYPOQ`IjsD!Tn&PV?twX{+b-+t`--79f^N!aDEtCnrXQ>RS!8lL9nTL1WM{TKP=bm8IrXE&V+$X42`8SNsiYE8=T6iMgDaz%uY z_}zAxMKMxN?_-1P3|qsU&E>cb`^RaXI=z{S!hoD?uzck13k#)hivHf?a@#1!%&??` zWQ!7MsaV{&7(B3|qGHrrRZdp+hvD+Vq0lF3)7$L(rzLueI0dq|zh>NHGAA%a2d2wA{#~&`-E)T!E?G{?H zN^s~tS{C^&pHC+k>LDZgg4OKhxnB-69S?%Zq^+05MF;nHDA&g!ak!uU^mlpH5{fx+ z;|-;th*pWbb#`VqIoB@LtPCB?B*JChD{PGJmWru$*u?*aMcT6FAFI_u8o3AUB#Zmm%H_4?R*ys`wb$2m zq72N;kx;3-_xq|eI<>wWrcw_B>!$rW)t*Fsl9Gw^hKaU(4yBu8SsiaRpFZ_vqZQMF zuF!nCf}>m%r>mBjou2EQAJ0eC2QU}tPBY4hRhLKXV+M@{w$D-6R0HxlR&CO72K(L z#oXK+Kd3%L_T@FTwFSQOoZhqEM+>bv1XYIPRZ2=o?ogq(xu2g{O%x-8PDs5({neGT zxxhdF$2HCc=!bK^UVvV7U*k6A>({T?UQ^># ztZbaYfHO}*T4GR(Ix{mv{s4)Sp?vrO9gggyAB8Ruf4!g;55~o54g5 zst*_hKfdevXN&s0=v=vfI(U58^WEQXU^hJ_zBfXpI*|CE*Ac~s?eJr6$A#3-4mUMF z>!$SjqqAC9b6jWu8z^v#umA&*V{xA|G{cJ{@f7R(M`XUA7H)qRXWXHSO4&=zdvldM z(K@xNC7*>rpJhmg0cN2edee6Q+#Dg8$kw-`BK$%ErbD1vnQbWgM{_d11{$xp7 zR#r8JkTq2(;rIRRKacH^1Y*z)&RvSmM}~!&AAjzpPm%?Fy;!T5&&eqsSYe>S%3Eph zydhVwGKhiHAB-1&Adb3rtXO@uVSBALbGJR1CAV3l2frf`j9*f@y}8*;oqpFgivGs2 zRPy=DyGs_Z^Yvd4^Myr40qz&{m^ACHjZ(Jg=E|24yOkD+HWpgJ^zUmQE9Smkv;jVd zUm@t)Th>5lzoNvGPeXsr0;)5+!?|=hGYhgO?2rg-R)y~-H6cxNP1+Y$U1AK(!X$y@ zUy|c5k*_IbxXI6%@Q!BsF zD9a@9FE*9-6AXX(mbgWJezFt_`cbW_dwPSPkHM39Ns7}AdWJ-1mFHmH&R2U8%gEBq zR<7oog_#f*y)KbmR*Z~_s$u7&qZRxX^Wl6J=T~V}cXw7x z)$M(7axpD@e0&6E9m#kTRsTC6C8>bMRO)^J?43GIu1|Y;+!u@uh&RRHIMBkWR2KRM*#U_l@X1e=V4KUJOd3oRp1bS!vDk0<4?Z z9^J*or7wnb1ne(59o876Etgxnaor_`BN9ybA0d(R>KDb4J~uxh_Mql4CNXkWJF-P- z*FLu(hr)Z!2V#+tFkDFZY8%X}X6~Ct?Py@b^xtJ35yE3^Na#3ITzDQoPp;VQ!;raP6v1I18 z!{)Q{A2Gt+1#oZN$3^Ue0~uRlPzC_dAPVnc=M|&O4Lio z1Y=U?w98d#)^aB3XlQ9|jB>6}wh9Yrw}#_Th>3|EmRr6+gCo>k*<$qIzJ$cYF1yDf zm8(rM+p>7vmrBe2lV>LeH8$wMEzs#%OMi%qxwUzTVaOGY#wWJ^%XUV%VlXv2ULnDe50U+bbfx`#L}{i3{PpSWBI>hZM~{5;PMHSxQZh_ z$7PD5h@HgQe$0O-33O&-$Ik;3)Oc4OKd~+s@xD1tf?dtcX&)~Kye->Nn6@3eyF=lI zNFWd8Q%5N@yKHaVM5OTZn~h)JTo-`7V>g%2wbPkPCG2~yeGo;b zbi5?&fBM+xcKCJwWRkS6^?&+@pBtyswh((26&0{)`JZy10lqq&6G||e^*g**lsVwx zY_ZA|S^JPHq%ewG3PV)0(#~2wGU~?V%Fto8EyOa)#=6h{uEq2GnqzcS(fa*EEzqg+ zKmDFgmwD%>y=A~fXVfa~{G~HFkij~ zQe~K@Jx;0qiLHo?1O3_cUwr+amP%)|(H#?HqJ>q_0JQ1y{T2?mFXkdPR`5Ho=dq+aI`fPxRk}$H4t`R@Q3L6R!Ked&d7XX|o5sXPx*`|uoSMS5q@x4t zu|PI?ZRbcxzs>!NEZ!36`_<`<@K?_Wn*DZJ&1LsHA~aJD=d(EY_-31aqpX2V9L&zM z1em@Af^yw43@pSwx0L+NryR5 znVA9yW$m?zp=?5_H;)* z7Z~h7Sm`84UNajG)f4l1T`a^5a#^T-56tjBGI^lrP6bE;iGbC>qPwsH%soOeu>kj@ z;PEf_bdRZwm*U zd`4I<>#C>Tzs9VwtjD#a@?a~7x8vF!#lgKxUiP_iIf8{`ka3z1hk`a4V5vaa0!lzk z3z#$u-I}=fZEY{w4rs5WG8(aQafb_-v?dKEw_|&=`F@jE=CzBW#ixWne%x-nAFlEE zF{<XcfpuK?I+LqRV;nlU8$0hNm@oyQ>*^rcHp zUJ-$vZ<8yvgL68>K+|%aFUqyuEO_9P{r&sLie2>hgmWO;f6mbbw5dZxw_I2Z;ZH?w zU#N?{K=+pe+zZSK?O%6TFUUko4aM#m?sY(Q`@Aj@^6C6q#-D`i#VQ7Y*CqTO$f;J{ zlLw0qs-BMR2!&gV5lH7}0WR$(SAo2~XgXfrM z=AvaH`5%l(SKMrIuwM^F3Y>+8hW6w+e|SB0M#=s!swH}yDum0n5(_iEp0YNJlqG|2 z+UIPhAJov4!%=w>vE*R6Ea$Hvz)1GQ5;l){%-~rRxPN88$#Q4Dd|I7kd+L@ip z_^IOi`0U8%3?JTL%vD=s;3p@0PG9Exq(nnfIl-9N&*%#_*jB9U7*;;t48TIPlmQ13 z{x-kYJ94V`_7nwk9Ms;6@p(d=s(L5-~t_ol0jva*sXTj1dR^6 zC93U=Ou zufATZrq?@+Ev2aq9+!)#-Wr`f%xUhh>2{TA`OA}3-30CHVG|R}C1@U9U+k(hrQB9W z&;A{`(CIl#VN2sxfggxdzz^Ez>Y%&ICmLr86!2O&bYUo9ZLFrc`g^vPf%{=%)vfd^ z&>#Npma-nNt3iYl0y;YoHb75fIe2)&p68(ld=n31)|kEN7a87a)JKSiKD1Z0@BGJL5Wn;JU*^OA9joDgPoLm_0#{{W=#FVHIG z1cww6fQD?6fD!&9#~+>qSUWJ0zq6e`^sRa+!1bP=cuj0?!aqDadFX#{)G3y$62bfR z+cyw#llLoS|BzUm0S*MGni>v3a39<) zE&X!;Lmz%MyQt)UqVUT#QixZTfx^EG0w#vG*|!&Iy_|~yjA-==)v#6n8y(Bx#x<8J zXq+kCT(uXIQjRESA>%H}k~a?%Sw^nGo?tPe*9qDV8F~4hX&x*9J^-#z-5Oqh6bL{; z`t5@T&`D;|W0b(bj;yF)Q6T2|4*Gad9iy76tJx7hz0EYs&dK{jwio^R-XchWU;vSu zDli)Y)-uA524s4Fr66yn2EJ4uVqLrQsJh4U#S@ICl0Ui)2l8(}z5ziG)8pX|xbYs4 zt*(0uI7pzw0RH)=PTuR~R#%u*rIF3U)>*#X`}fpxTIE~k0!Cfk-3+?<nTe_RO4 zsWQ(0G8bySI_ejDg9P*(>iJQyPxDVm`?=|vr9z%(%@0U_NtJEKiip)>X>iM7rqt_tp0O<#$OMwn{ zX7}JiU_0w5%O`1keg*dh_gQ!cXEG3~pCw4Itp4}OVmP}W zu$XI9`eYWq0|;};uEtZ~lxkKj4zyrqTpXP?-Y1MDuo5@enct2sZw+K;p>O|j+J^A> z-`K}%E^RYurFB@LAw*lTCLW+}l2N2eV6ax)Y$l7}iCn~FWw8Jb!f!5j`(-H2r5&;g zsPpKJO^4yyWOgf=f#4}EeJb$A_A&6peI?ol238MdtH6Y$2702W=Y2T-{l73SmP)U) zf7gR*tVV5;1?a~;{rxWc6L`Rr*aVLW(xkZTvq}DP070Zv1uR7u;Y+<;&0Gjnp624m z?*(}d@=RNUSrNt(sLuC0EDa713DgDHz%mFYVClM>pR6CH_>x9cHdARi0#g7<6wB+s z6p8fNzQJBAvxWU60atpxEz}=is<)My0&dCVBL3KctfY$^U|+W!y-{7PMMXLzU^RvY zeK|jv{-R+!3;ZvsqjVm6&3yKVJcNRtNU_4`oF2a^!8=lNPz2d%l(APE zIRbnn;J<-u>VF>vZ}XWb)NtwQ>b9OM=Q-XU;RJT?i^f!Sm5x^0gf2TQW^D`<&3!)YL*D>uXpTnrwlp2y6yaUNRBy;S&iT zon{MCu+S&wJmzA9xE-`}zJ$b__CxA_p`jNB>iM@5&&BvwdI`9mr;fTPfc#5zbfISr zrK`AP*wTz12l4 zL*E@|5GWz4O&^i_B1tMWs=!bkZ~ysexr!|4VO3NgVAJTZEZeC6?BwFEVv9{sFuh1A zC!R{`1qiWJS66rTY)zKB4j#*!spDFXW)-PE@fbDwB@aBdL^eH1OS=3&Cu&9W^_Jus zW$Hh)<0$ppy)paCUu?NL{qF1g26zVQk`e|sqqeRg5d3|D@eh~@HtJ=vX{s~>NnCUwDD_@aQgt~G{Svrf?ar8{ z0ry#1dVG6A!X0qlG&!xxiCR4Zad8R6yaX??27PJywyk+yI1j zdVr(oHriFzlB``1P69@d;O%D11yTsnBx1RW@HYavdH@5u*-o4N59g@u7E5)tl@%C# zA3i?}iHKes`@T&q|xz$FX)4aA^*Q^8xOd2M&YTHo1Ox!K$i`0Hin- zm7ZQdLN8P>)I7*RK~38p1x^tSpfV``vU)}PaESrnzBLIE=#YBao?;R4I&-7I&SlTs zRNL){z!XK&?r_2YlMGP#u_(R9G%rc_i2 zhyZZ|tj&hG@pviMYF%9Ys`!W75^(1@Z4Oz06(_6jE39*PWIg?Z2oOZAD#J}kIsw=u zL5cujw-~m2S7=CuPZY>jn=j`4!bJNm>SCdoB_Nd~i^uP`lc{-FDdJxMD~_ z5=~fG7^sF~%YuO6MoHlNfCY^3eqzYP))kc5jn~%^C9tJlm?2h-eEC*?ryP2tBnvOq zs_6Qc`vtU6;t9z|p76UTNSd0OZ%<(v4p~CQ2aIZ^)IL|_`Brh_uXi)NF_$YAJp_QK zYizupnqm2d1$ajKEzU_GNbtAi1DAR+zwZ_yol;h=bsp-qVN)n5-;=b_{R<1}O>_J& zNJR=Y#z2f2*yb04#WBn6E-3&@fQb_g5L|Br-g;N>pZ6c6Oz+{)$9wY?Tu~4oQ&6lH zFf;b1a#`&4>fc1PiJTFztZ$cTfyulL7Amj=w6hM~j>kg)g~TDg-B%lIscZ$bOR&nZ z8@A#(T~fesm9e;Nl#%=R)-3FOWpr;Fca**q+ z7E+P@e5@cAC~&uI-^jMVEjOW}Sic_$;nn--LvYNy`$IkuMSjbzmLWVxH|w-oR%7VIp4fsdwM zJh9Bi!60+*Q-Hp`zm!W#%xk~iKW@sP+w?La;5I3e$bO<^9R-9PldtkVE6$T3gX}96 zH8*Y`kSw(}N8WC}!0b7ab(P+KZeN}-M#TZ5@S~&)*e?kOP7;a){4Pl_4}7@n2Hs}5 z0ep=RGRI48Qt!=hot&IXAhJR?7?_w6AnFV_n{XR(X=72QkF*tG8mEPNhf_Y!5iuza zN?=mKalHeBT00iGm<425;A@CfET};HT)RM3%aQKJi`-Zc+8%6uWjAM09eUjYf+7IT zqr{GktF+|+1PJtBp$ocNTE35gweGA4OG|u7e$Upd7MQOHZxo~zUXTd>a1mtX0o@Y9 zkO47jglYvbH#(1FeSmig@NRjjzv{U74P?+Y!urBvW4C6PuBarfdr&(9R$Q}e)zxuD zZZT=|qIf!@h(~tpg|08I22;~hmD|!oBo7D1H=6()!Rd=y2eBt+gBtW;@!zpQ_v`>* zYx@9;BjtF*Pj69v%Vp$G^BF-{bl7iTSO8@9MYi=w(5&X^8<6is%HpHjLquZ%zeXf@ zfLpGq)K=t?CEgu>FbaqBq)Ze}#-F1dB%E!JYh*&-)(B*1jIu|MUmG;AFWg@)w&NK6247_p~bKxPWv{#d*=2e6ugSvd33UeDTZ zO_~L`{{Y6%gH{cQaO!^}#KMePX7_~^x-~kr)Ji9>iHO!yd6?e)b_CAoH0)E{sWBU) zpl;aBcFk$u@|arMM#ROvf+HhC>;V7(rkF9HU$ljTY?FyD zCvtcxy1KIlvlqczd|feDGBPB0cL_#Vui{jY=6)$9yB(o`=tkrh34D+o8vX|j`CAL^ zT+wq~Wy7_VhilGKtdBPVi?N^-?7Q<;VT3Nc0gqK;G zpBvr!6**JWR94Ggz<@J2*2l7_4H}z~Rl>iDa8=3Q;M4jeBbyDxzS}MS*^U)1 z9_lz|qzHHQdvvG2v$K=Ps`D9WhxNn5_bbB9_3<6R??ae479$rF;IK%FS`tW$hGsN6 zB#7mdwQzAaN|V)8H;xU1EEs|^`Rxp!?=6JIq@|~G?xd^^w3X7$Bap~2HZ$vqNU5qDa#(4XP*!FqOJX{?ts*Zh)N43E7#?Ba;S&pVB1BvZ z${^YSw5{pV*Dz{oYJ?JpOqGTKc`l$IUzB|XVg!ZqMAJrcCnc^1tCnNgT&APGl2L?` z73tH{HLUYl42I?ObV@nqAe01oJenu_0nuw$`G5I$S=xOCbySxbZ?-gY-uXlU)EmSP zF`4@jh73S12$4H|z}Mt`j|vuBzG6kX-D-12g^;@{7*Z(kW&i#APyP+$HwDLdLgd_m zH8B6n{wXLU2xAS2PfH8zdlf>iG!oyQm+RCJ0ych~-7%1UK^ztn(2wv)`LL!djt2E} zJ|M!l7kg2_2_?^I`35kIHYjr*&}F4YgG^yRd>l13ht+0`w*}$Hy1U8v0JXFePv!W3 z`NrutqhF|y9((M@XjI9JfEWPQ7aLB0k&ueEnM(Qj)ws8`ULBaFg=4=Yc-k7w?d*m( z%mb8vc&cn{BqYdzAj-tO9s!HNZ_xjJ`ZUm04?eK_Jt~rcklV-Ybhk`?w{J;u1gCes z7_>?m;FF=rgvmglcojgpYA|YSfs`>qnE=R8T{A9rpaQPT%)-*-eDN9+;#H6q81nTS zhx0HSH7zY*e5G#Pdf_S1+fWjLp9jnY;=Vrc)4v8LCL46COL5uYLn9TZ_hki(w73_3 z%KZ#7ccDyLRpYIed~P}xLVA_@ZGFJsYxdknk9s}TeS2T52BOEG+}$^T-(~F|9O;Gw_iGKcD}eLR<-F@q>h1k zsgM7Xn*q}hJP-a7)AbHfF-&q<`?1%EnGvPuVWm740Y$xd8ZU-)emIi`np{wLq&{3Z zgV|VB)Q9D2U<4#Uc}2NvWe&;189nfk26CzTpJBy(4}fNa8~6&AhUSdsYundqQ>7|V zeDB_o2s%fCgFUcI2*N+A-%xe<2;^xl?06GIBkA>v-)#0rOBSfE*Lo7!^4U@D-(!c= zjq-q234}WVc6^-EE7$D0&1kR7Q)G~l3_z}(UHRa-KU&XqO&)`CC&H!%+L>3lRNZ7M zraN%!A^^ndzzI2nySqE<*%lh$A|+&lF8U*gkoEQTsh28?jx-tdM{lg>^*=^!&sF*2 zjSw(y4*Lj6XIO8G2>b!P6MyVhs-7!+q}xc%&727$DC;_TEs_Q+ZU@SFx*EqTtz!+O zt$T~D{gwemM!NQY0H;A%nyf~H-xTzm*o=}_L|O}P8w;s>5GJsov-~~I>(}A+^*q4p zNhKt9fx=P1`UKe+51?w{vdI2{USSPF10ImVm-3!@{ z0C}ye*|ujV6@nnNy-BC25-=TceTu+gGc71t3yryQ{T)|+MdG1VyKFIcVGjWpzaYxx z(#D{x&SO)5W*j(EzKC#&qaHZkgX4F%w`UC-56M7>fOdEf14o>&81yF`Z>C4sfQy5g zc8T}jGa;NuO#hdsME?J3-TZ%GYxTT03cP;EQ)fVCSCu|y`}%T~5$_W#efVaOig0)& zmGC|x?{}ur+@G+_z<$QQa=U8#|{}NLrm1S()C&VK`Eu zne06FXiTx@pjQV*;2@i@L`J{mk`GM8GD__D>-}`{vAm7RsMb5)bd^~{u9|!tB8BZE zeP6+wrhid`iD1o$f!zIfWQFE3mC)dKy6!T~)ToYo@ zA1GvGWKpq5B|SYK)5VbW3=cCeUt7i@GdHywm}WGG5v_PJ#{Zs+G||y-Cc4u$XjnUT z9}>W&Zd7Q9DT9qIHv6yvreg}}Ingx-s?`{_C7!Akf4@|RHKE}MILiyY zb6EaCgaW&qSjgT0D*v4LPQ+C5MSrbiJ@@7hQ(_-EyZI-Z#rh?mVKuS^ zCIBfTasXlvNZ=?ziT>{_3Y1$@3k#Ue7$RFqblNTb;%Gj1r2+72UT`2`>~No4D_0Ag z{#d?!{&6bQ7?)LDInVBY1T6xe6LH2a&xh?OydDP_#6ZJOg(jS!msd^F&)6ujefLal z5mNY$;eTcUs5*lrq_o>@FxQROK>)Vf;|nZdjE4;gq)$ywPQ}2E^$M^NM05evx_vvo zZhsPm`H2Fn$x;C%fSk_Bg(FcfAQOe=E)~~xpmT&_!Lv0-GE_l(+x{ubA*A0KqQG6O zUYsaXUaE(UfkR~%iKm^OcB=S%7$uw zFutsljv0ZrtCQZ+vRh5>vhLDy*2c^u9!Np@uYj-L3S|=91DM&EuUr(zD7ZY1zs7B) z?TT_S98~ZsRVr8S&#S;~f8?2J|MbN}xxE-qCBP#c*>l1{@DKQ{`^NIp7hu_0ji^VH z3Ph?&y(oo8-$MHb$KxOxWma_xXD6Fd;JAjwu|8{B10uvLAmV+|D@{8&Uum-~yH!_f z3K)yvqP5HZsm4s@>Og8-H^1{oJ~{kh#BMBIFccG%Q>xM1)g`XP_As0Ue!qam4}Lwq za^tIpsML%!4%-#+w>QB8-b?S(dE6{#XQIjt8$u^7$I2enFE8?z*k54=RCIQMOpUp0 zNrePu+)I42(P}Z&wGNM`2U={qAVP1q(m;A`dCN8e_T3wwGv1w%^zaJ%7u)Y;X5^@6 z?%M!r54U~nZ;gPF^P3_3O+LhJ-tMNq|$m7-+gfal~N0ANN+snjp9wqUa7_JM zB%wr>u7gcb4mZZF9%bBL=#}BHVRdSLzM0=hy&ztB`K=~BF1@NMy_M;x*8>VYwW_~} ze+u2Jn&*re1Ye#*cU?b4HXJ{G$MovXuN!y&QR9@l07eRPZ%WpHTUI9MbHwkf z=|X|@`H4JN7j*62N1ZGl0<~lnALFX`8YEPG5_isJMIPclRzjxDDpu4o1Z6+pTS^V2B z=fP^!j2fuVYI#%2Q)jmrS7;~-Z1M!qvXv#vNlPPsl>oQ(y2PIb(s_@+tUh3igyV5OoMyIu zL_>)2KFO<>ZGa_!shaDB|5JoTIOFF5`QrczW8#M#34nF@*sJ6PA7r+;V&8yE^U zv(B3U^ah^tKU|+s1!Lep?(AE54Ges2oTLjyRHBAkz_qMqdJJs8FO>M{g%dD^2L&M_ zo>b+_5RUhx9r2sBq7z9#LQO(^!;l{Bio9-V733IMZF8Jg0nNH z57zbibeEc`p(H}ImZ_#ax-syASql~B=U02q%L?n&=cZ}dT8ySCnW)Cwq_He|l#W&i zoDp!CUJ4`5!DTku(}6BbL$CjDoauanngC_2)Ad4;f#oAIv0SaypegIA$k_Lu;0PON6V0%y5(7K z?Kf24HTSk}agP0wX`%$`zU>3jkA#g-8C9bfiD_XA^-HM&9>~tknGBPbla&g#;rI;l zF-C$o;5S8pZ4&L_U(l0?6b|p}m%}5&NYV-l+aPmmXV{T0vdBvjCp%Z|6j2JDppmOK zgWFv~mp*&AVC^t#KkfvP3V!zuTtux|J3f-n;EwKkBKEY*KO||bCm|r!6cp18^`P=n==Gy0UPFAqYFmF&Zx;4{7WW(!At(u~07aXdInaGJ2 zuvgtKiOdnk6cW;dUBER=;U%@Z+QI?&w;uKd`tzKTS?om3`jrKA#j4GeAf!7fsFd?> zt1eInc^~$Fly;U;S+-xB2LS^R1Vp73BqT(-1QZaEPU-HJlm+#kkOl#fZUh18 z?(Qz>yl3Cf|9Rh;Su<GE13SCiVz47MwN zoi8ikjk(x7o^jm;D<6&zH2hmz6Lj_5Myy0$p~T(sw@HZj^f1Z0h!rzS&cZcYovgy! z3?C4+om9GVl;$l1(x_)(fcE0hTv<8p@p&RoU`WtH3PNvwK`@dp7Aqzu87+udP&v&s zq7emb@avLTSkvfcp1bv~m&7U~?!*JA5QG3+xnhE`wXvvS`u6NBSYHB0(5dwVt z_P@$rEXKd(YIVHFKFiso8956%6hSM<^I6YU8Bs9|8FwEp__b<(ogizU*gUaxYKtAA|| z@ML>u4#5-Ehk__qVxj%1`OGmfF~G<4HWtU3^Ml0qtM(=T-|FS27xc&vQcHdPtmMF4 z#?Q_O)cmEX>SLtOx2?ser>pB7R7}7z<+iiLt?d=yQ3@OD>OzzfSg*31!d5yd7- z-$!7_pR@6;7&QMiLT1IlHG$lJ2G@$=06{Y_=vpwoZ0Uk5klx?5sVz0^8f824M@ep= zbeVa~bn(aQS{eDhSCW9AAE^{Tc#-+qEzF84mA{oQEA~W*5j9qA7x4OslihngM?^wM0>ALv z%pNCaaQ1Ii{m%jl(|h(TrO^>QF{0}fb!MPMnk`NFFpLxobdQKJW!rYe#=+_O)~L@HXsH9Jc0`W)t-eVXMZ;I9OmOc=4A?9~IUg)0s zKEqUC9${#~w)wBBL60j?%UeIP*%OO}Ibiga@bfq<-~Wq2JaB1Bd<7E5_@Ua|EM zE6mg6m1c4V9Ijki89r|6Z}i!H+jV(cSI@d&Ty)HcR&#Um)LqU|$ zUN|WB#71?A4-KO(Coud98}lk@H#jZz7nbuaCu=w+)C6zg=kmoYiq9(3bKlMImVQd3 zqu#MMl5An3zDXwP>mF^UGnsU**HcM$_Q4#78boOwKzcw-OwUlUk73k#4bJT|PR{F5 z!n#m}WjJ*aGiv@!>}v|hW=a?f*v`!7!9trJ18lNfC(6v8AleB9@m zGAExY*Zg1D%v1)%#l@}5tb*X#TW4i~b4JmUW@eyV4ZeY5kvq>{lgMUiS;~mn`FSa< zo=mAY7BQ=@b(gU;OantO=?4>9e-6~_<4#sqTE7`$9|#Ex7ZsOwjovmkHqJd$@xZ+I zN7mQX5vdO-($HBP!h+L2L)!0LQr3SXa0kt`KfO5GG3O(2{8 z>INdfmsOX8}BjyN_G94fI;Lxn@i;JT9sm4n{%!)N7{57XPm z%d>WeUS}u(N0#ZDl=NLE-U)-`VYK*1+1CyB$9Kw}^Gd9?_HfwhQBaMMzKvRT^N;)z z|3Qf|L+cj^o?KSTr0XL&UEVWp@;_dn-g~NrO2@|aIkU?UU^;BF!DJX3v+zjw=fsP^ zgTN2)vq^^1c>B=(oXy;onSgB&f#Q(9F?v- zo_z%KG{4gylRw8sc?i>0uLq2g4ocq)6kldG|6C zvC?^D;?&bxB851g)fEj(mb&CERf_A0_R3#?mxc--hbm6Wv2#taK+3Gj$-F0-fF|#9 zzJJ@}28^ye=HQ4fCE<)<*3k~(?PZ5atws(V?fm_NoULp`=y_Ds-Aw5Oe_@AbR1Ab7 zl(HF_zOQ1!SYFtPE){htSxS7O~`a~Wr}Tj_2dyycZ!FrNI}t>EarD|qXG-M2^Wd6^$X4-fbD?(Q$V5Qrcm z*0;94dPSaLx4&#<2OEXX^d^Rv%}l+)lz-O;e?LmLDCBSepmiUl{R8|2v7LW|km_--i+BIVyY+W)8m~<_9yuC@&(lU5b$qSMm)zyL+K-L8ud6jS5?1Y4b z*7~ZG#}9yVmM@i{_P=EDh8P+=G6G)_%2f5}cCfWx4OxR}T2Zw;kWDRJFGFQO<+M50 z@!O0I0O!WZ$zTw%daGAfq<$7$W(9?mauBox8_xDdm+daXI)!M&z`!EBX{P7d zY1G2X-&9$kbAegIIQ14bHXggt-ntN6pu&uRM!e|HLGwcOIK!o+;lv@auoy4jOVxdU zF=%%l2PX{?y8}8PfP;Bjl8$LN>1>{7YgFHxayx8pZ^wjM1oo7qwW`uM*!!_!7}Jy| zWd1DWfPKx9jo9Jl<$gQAXeJtxB&ifi-4nvNt%70{s1oL2E!!KPgevXthOLsuzD*E8Rbw+rGRZmBbU! zw$Mr$EwMVBLFIAd_M_()XuzH6Hjc{2g+fw=6TjrUZ41Tz@w;3%?7M45v?L;;qJDq4 zMeu;jnv^Y<@Y|0fp;D`ZA$3fj$Bc}d6k7C?)$hU-G6rQe=e+DM&m4(42l?+t_eD^N zWvdqHXYLcyjqd+t45HY4a~>UbfSqyu;K`gc)(A18E%{wg@TbOd;rqh{n*PNDD`JlB zh5lhzpP`Let5sA!abnc2i-fZksXX@*E8er}z-G!^1HeZBId)#uq5XldfI_8|4(2Gn zmXM&Np}7v51}JwTyR@s1eFWh-U!H{WaC6^*5mxZ?fu?~!IlvEd#Y6s`O|S^%OZsU1 z`}bSxJl5+uYV22Jz|zd(WD*M{RaI5hH8gxozWVjl^wPx)tFGekBp+_C>dVVnQ%Q#8X8#)!i&A}&*un< zkjQvIOssEg>>nLX2o@J4sN5kUe6?cD|3tBj(Q2YBLn%ig*>hhL-9u6j$S9DH=tbTapsry+@3h4@g5B?JVl=}DEy)B8g-Y-}$OCOOUCoa&X zaur(r&@lSnkLyM$@9!GvP>At}PoyguevF6iVUHsbBMmmAv+pnZGt%me)nLe-DD*T( zaF`~#YU(Bv0gP=&I7&mx3R#ek2IM zeQP@Nw90Yqg9zmu=qRk9#Fm!TAD=~6vSlI9q0rMUr2IjA>B4E%3GdHrW20#;%`=#Y zfqXzZSxlB3qkXb9^=r-Git(#pLZXvXZW-XI z#+U0$n$JBF7p~4tLDz06-&YRQ)A{E*!2R)zD=WVc_x<~qHSUMR-f@bwNFm^m*ll)m z@lVxIhs)veZID~Psq|PczUva(e`5vLl;N(oXKuqz@>_?uDJnhN2kTtnKYqNm8y)@e zc3TR-t7fofvegz$jAfI9#X2})R)E!@9o>4ms%r-=0HUscU8oH>9O;1207Y0UM18a^ z&3EQz1UVBt_yZ|rqqSC4KxTai@OlA$M1{PEahKK$$Ww;pK}k=qZjoGSQTga|+pgu9 zKWr=zVjSewbaqm*A!4aX3Uww8CF`e#l=8R&w1%;^z)0#(3tJPY-aKrXyOJw7onP1} zKs~$Hoz#5MEXnh>m%@U?0$<EZ=PeHHsk^HXeBL4R6P5HiY-@d|E|Fy?zK$SXVK5TIIy>fJ=^R=ofHww-X59%ls`>hv8ww*Q< zw>L(VhtQTg&9d&@_45n(oSRi3qx1Glj4k@o8D#q^=AVC=5wKe>LH6RXBq0+Na**@P z5dz#I8nN_vczEW8aOx=u=K)Zi;b+2zLd<3U=Ov;Ewsq5kh`xlBhYE^{%7r@hx6&g9c@X~B949@LL*RN<${=3oRB4ddZa?o`s zY*2u@d@U`Vk)y(}&b62BNg!*^VcH0~0?JLqo%g=AwdO8x!eOT;H~GL3gq?mpcVv0l z7-XFk$??JnZ@Wue^6S~dTJ?dn92=FzRx8$xO@g}C)}c8$Ik~lP*p)q1?jPwz<%W~x zUIDfPq=S77EMkG{FVhnlpf;P8aYK4-#wLD+SsSK5(A7(~#K%}OPBV)C+H#%^Kk9k> z_UaB^V4AP*unR&54lCrohd_mA@6@P>(y4Z&4hTWYXm$ua6t9DwAiqi;I02wW0}}_8 z+xG8W7^;MvZ~QnZ(c^-o!813I4hcJAh0mTmw46BG@1j(*{oBa{{d1T}F%9y#(batc ztM3^H2Q~cuKp5_89P2z|XNQ@&Pi1xa4o54itKP_mif1f#B;4pR4&i~p%aj@-!{fS3 z?A4Kgl$7%4z>cGUxCJFSF^q*{=*WbIBHK&vKOek4YJhX%B0htoh!pjET!^^@tv^~r54`2)1 z&G&lx;}CmG8GjT?)g8?)y34E`wGeQ$Kk|v%a-vAm#0zB0bfv)gL~eKQrJ(yi9M7MI zCUIxP*SqbYLi$3>*UKBq1K_=!E%-66zI?;2ox=er(kX4OM9qLxFsxyXP7EVl5AY7HQ zmop2;5R2=60^(!MC@91|$a@3~lvt0f&-2UYpTug}o^d zs*jP)pgVE@R5N$#ELh`$f1#Zh9edB*$8GO-4{>y7`cEB%K36?d*kePwKLJU8nBdEf zj9gHh5^tQu|Nc?<&%3x$*Ycxq7a{ha3IVz1F#mIa`aHaN@!RG(3I9h6#>h#9(M zCUs4|&F;IA0;XEL+(e(x`q4e{14l}CTgsDjf0%OoIBF@tF$rw_w^u>y7A@5$Z_D@v ze>I2S_~3ql;_aKmG(;xRTlGRKv1{|-0QmuKs@;!EY)=e$q=~Cis4OKOO^|q}@i~?5r$4(-G4F1A+WJ%D9*0xPVx&~$Gl*Vi zwGI%_JMJjn^zwKwKwV=xx9%^Z>5Hr;5NWH6N}|)Q9eJZ7Nm~U0tt{(r?kINf7*G>D zY8#;x+Oi2QEiLV?f4AfNl2X2Q*rauTfU%-pk?;hygkd^-rrkA1JL`{PoR`fFzOTu{ z!+xJ+tGg#_Jj6^Ayn^7Ln$@<7I>law&^~NVdCBRp%?TDrhG|dM*~{t_BN*$O#MTB7 zxE@_~F0jl>cFn}}zip>1i0zji??kpfB7UEolZuM*U5bT`T`u8@l{11mtm#f2IoLX4 z3W`ey6zpF_rJuve-9M5T%d(?(_O^VSoj{n1c%@J#MAPU=QgO<7O$P3}eb#0E#$z+E zI4r@iHE;z>uVTnvvD*O3idstVBJsGotw0-0g`^QG$JtQ zc$@-GGMKU7*7HotpWbDh&9y)EqOYD19uaUp-{yxD)+{|XX4o-7kfDk@xaK&;zq@-x z&BLQ+Fg-|AY@4^z?I&3!hRceaP&j`&<+A(ig~9YpLV0dz>6vu=K;^m3KO1l?bI&m6 z4QO-xA=IWWQzxpxBW9>RS^-ewKY~v0A4ZhLBAY5VJCTZIeK>h~ zvX|E4?h#X{-ZY63bjZ;$c$poRcw3%g-721$_KKWvbnpkZCpqD6=S~VI!cv^^IPE7w z+s{iB-D?I9AspZW3<|g+z(?Jmdbg>f(%thYG2=ge{3;58#NT&OK0S-8wSx`IBNWv&d_8@&S^f}fMN zQY|P@q`t5wHDX}>@EUmo`~N3DNtInFsY0C0EUoO{Lag&Yu83xZgP-Qpr|TiqYRu=V z$H&GJ(R47e*<8|Hs9lO_5=JpUyKQv<7B7Zm4BTo!E8nXAF?2QS!v#5oWA`&`l8s8Y zMkAUAy9c0pwY76dhfxsq^Y*3r#*l>a)7?ixRxpiQ&cenvp!qQ9yj(Kt2uW;1*S&6$ zJCvh)IPMsy)cynWv~u@pdoIlKBgW5_cS6dQQ&qgYpV$oCg`t0zc4!j(%Qe`Zbqr81NI zxGjLRll(P9*HCsi$xiI&?+$H9Ym~D&EUKbFMx+pY&1TyJ_X>x%#!4LdP#CV%JD}(o z8ZOd5Bt|L;!Pk7J|F-^+cRnv`1>gaApd3~baOzq0{yO{%W^~o4v)ON7TePb*KZU18 z!Y{;JHqD^QvxFO8>5+l1`DO33>GnVjkS+)7eBlsN@gmNT81AbfUF*JkWYtralwAB> zF|(%y@kbsi^jEbla#7P#$fDH8kQK0XsbK3H^0brkq{FRi5%$0K??54N|Jmi1_TD{gLVd!t9XAD z1!FN$ChHL>R8&N;_Ce2iqV|ouE55>BPI5U%HKMWPul0LmbU%k@L3e8yh4RExAp5Cc zs#$p7SQ-{)A*e;j>8Equsj=u_$V7uJEYo@`9yzK5Yj&K&Lf16=JDY@#`ZMc(CHHa1 z7Qjo8UPG0XrLkFUtZFD@D$k?%I;`DFn) z0FGnMn+#UCQjvf?N>VFd1JKfI{b~2N$*1Av6G1j-g*W*k&cnL5#BZFp%yJ^{A248YnIC5>=e-_cvThf;)9MNG9OF@s?s4 z4yOMX-XerDX#U@H%hNOfLRsuKom)alC2}IEkvs*L^%DzJz|?-V_eTPD16ABLxG`XFEC>$*i#LpOlYT*o{9bt8#VgD|TsCkrWyyQqPfgIl;4aLZ zj;M;oC%GN@h%`cD4j=}@<@pi#Zr8n87HH>IVR)QOLzUfzlZ4Y6VxjVKjbaN_rHd&2 zcq-iZLP$7fSCVsWPgZS*E0kE*`X)kYRkDbfRn;;&}`TQtpxD0+Hc8mD} z$FG{H4cx*lJh89zw)D zq=y#mY%?JNFgSr*nd#<;FQ;y-VO1@F9Vkp z#iaGggRlID={6$-pJqFnB4EHl{IOig+eaZTMAWpj_H+G4fV9ZhX{x~m;-;=UoFUA2 zalDoOu4ntA^vXsnuh*h+(d=M#7^9wlqNuzlXMa))!oMJ_*bvdpv^n|{Igo;;Yh?Bs zGP-{J{5iO)Ga}GImuE$Yi9u3snU+*2n`s5#vgz<>5|~R$1Arh^crM)Iu(W)YNS0H4H#bLk5f4=m@dL!51wPR|k9yU2kyx{eN zG$EIzA=Bt~Lb%z$y%cN(&)heDFal@`QI*Iw;Bic@Os{h7q0V$o#c~H0G7TBTsin*F zJJ8GJsMe7Ngp#Apc2Aun2fh?Jy5?}~+1Mb?Ea1J;Cm0LQkc^SbL$&`=-db#i6gFYv z?KOZ+$~s5L0ee6{kWC(-7fol5ZJ+sKTX_3n!aC?2naMX#eV2}n8^va(ZUJ<5hn?|T zuy1Q7CS|W3v6*!1$T4Y@IXpq~hA{R5jpS$1+i=}QtxKH4d@Q@I%}sAaM4E%?Mi9i= zLkL94u4M~Y8_FK9)A2$7{#Y6vEsAoF6kkYR|C@QW@nD5K8gxU=bz})+yCax(qpEBU zgR1!|7gcwkD1H3+hYoJG1Z~+Yl7^kBy-#JZMST;hmh^~#`{dJg&t3mUdDeQpifq=S z^#H78VToZ1RZ~aX5OV}GLT(#bzxU3>=H}_YC z(%fYXr|9*l(W!Cwb9dR7tBxVT5Bxyshb2FeFPgd0_9;fYD@U$?5%N%K+>W(ZDidI| zN+yY4tRbxxPM@6VVqahWh1THTeJ^X@Y|Yp$Ph!9pnXT2qG{9-+=L4`WWo(Bs7Xum0 zGsRmA8hp!GW>7PM&#ixHoSBJD zKA4%5N6zkS}**&xs@tH9IQwseupX$c>vab$)`gBidZO!cf?H!D7a|kMP?H?KEA@B zqt^h4wsVg>_w-_b;yJ$kvC`b3VkoMUBQfo;@~zJP zIPCr9Xf@&?*xc}beCldl6*3*3)Ro;%DC^C1WXR7TU3>i)KfZ!%(HrroJ?2R>YJC$Z z*&-Gb-?w#MC?kmP-_MFM>&E1ZLuF};SF0H*y2V(rAAiX#RKkeC6XGx;iK!EM^Qul> zyNh3~T#tZ+%f<-69N3Frhs#YBJr=uGsPHS54#U!p<+NWt6awKH$Mkoe5ijORozHJi z;<|Nj_0lTY)1~gKdDVJt$mk1ELc?bSD@RpBEuy0t1LR;CxPJn~8$qUR1DFN!3pDz0 z#4Jg)KbnCgQ&vE8Vmg!a&lIe;2i^?5E6t6LE7rw(w>`B2_lIEo!X|8R3HmKk`%y3^&rLcfl=qK>aKnfYEPoyz4fkz|?%Lz8Gxa%L+GSV|) zM_^$aB70_v^!;O)67yWAOd52dHXkbb@N+p2YGp_~Pl6TS*H}zs66Qsoc}A?M(N#(kJJOv=v}HHTFB0Mh4`cIg+?$(Q-{DI;Sb-q4{BHXZ zAR4PHZ)kt*LIwgrR7rzPPiUd*@h%7mH*s*}L$Fr|e%IoWRXJtB2mWK|4+G|3dgZ#r zvJ4#&*M2wCvw51esyRg=K^*6J zK#sOa=`eHc{|&&wSpL5OIJ~YOK-q@EO{TIxBaYoM(|q-Zg5O`{EKzxKE*11W5S;Sl z33yNTp}EQa?o^bN5Cy2>A+J^CuDtb>11%@YIAlt#%AFAMLe%T;0>}C>C@jk%ClZzt z&>_obT@%Dxtco4^*P%36fT3)lM&RyYDf=>QpxoxMO2N;fYi9NmnXhVJiOe#($EYR> z*EB#hAW7ZA>GOP*kZHEyuUX+7w~!?>klwdqH8om`jAc@sdVU9nYoJzU-4G4=oQB34 z_oz_YqVSv-VwiyX4DVP1zy)q=M2Ym8+OKuSmxqH4WTwTLR)TE~uK}te?bD~ByvhC< zPB11Ps^x}K0d)_+Akc7u;;wAH63fh1%f?;}WQ=s(bC&}S#eJG*AK^;IBO*L}g60-> zj(ZIPIx9pcm?omf{*>iwjUQP%7B^wZF?Xe6&(lk9cZ{NDrywi)Puw)0N=N#M4sreGPbg2KN;yD%bomlgJ+K0Yp-53EnD(yEF z+Y!$e^d5JS?YCm9v?8=(Dc`rcBbt^erciYz_Za$P#ifig;H_AN>THpgOYZ`EWK`q`3wAk@=-7 zyQ9g}Ou~cFa;|Yzb@jg*VLA`E9nhW1$`-qqe8dR45`L6H&~>hfwMTlTsdmTubv@%SSvccnwoDsa)kYU_%&7%9Ep2_rPfr-I-O zRrN25`4UD~r2M3E#uZ>EN(btnc~RccJ-T?62=~|kn?F}2rxUUXm7BU`zw)bKpuM-5 zLC$I^$8hLOKMyA3Nu*S(c=;k3rplox)L-m<2@?xQ!eQ^PGWvCli1vm@Qo>r-3n2)!I$-j5^F8~nS{q}`+Z`Lj^xWmn?#EXhZ}jyoKfJ@j zReqbS9jMmrhAkDMRe~^XV=7aTc?S2ltq?+8|>OndNY}G5E$P zpRu!x=y?h=q$I-7=3Tf4Z#=vCm!%-;UXSgs+%EC*2DA@3!u8Gc+OqY0k@sQ%fCdHv zvV=8sJf`y6wt1ZXo0N)|4A9A?kYE{LMZsPm<5?VpV} z56^U){7S?=d*SS}Y)?By%1CFHuiZm`;r19fX0`L*ZL`z0Zc+4yhZ_&CItp~^Wt#le zB=@s;>=thlG8lXM(98W;SyHywxWXa89YqL=-!E*~~*t z5=_D`1pkW#w;Rbcnh2FjX5XeT*+~1W_`q``X*&5G+S}4+_XKy2q63*OZx{XiIsYf3 zGkGKL4op~f4%7nJE723+XcFD70?F^=3iW<2*QR$BFdASpeY{XAz!V?oa zFJ4KStxwCLdyXm9niJws@OK|+x$RnIT*&+9x(39kG>8dS11@r|^#M41cnu`DOpvhmE Date: Thu, 6 Aug 2020 11:53:52 -0300 Subject: [PATCH 06/99] [arch] fix -> some conflict resolution scenarios when upgrading several packages --- CHANGELOG.md | 4 + bauh/gems/arch/dependencies.py | 4 +- bauh/gems/arch/pacman.py | 68 ++++++++--- bauh/gems/arch/updates.py | 204 ++++++++++++++++++++++++++------- bauh/view/qt/thread.py | 6 +- 5 files changed, 228 insertions(+), 58 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc1effb..dd035d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) - Downloading some AUR packages sources twice when multi-threaded download is enabled + - upgrade summary: + - not displaying all packages that must be uninstalled + - displaying "required size" for packages that must be uninstalled + - some conflict resolution scenarios when upgrading several packages - UI - crashing when nothing can be upgraded diff --git a/bauh/gems/arch/dependencies.py b/bauh/gems/arch/dependencies.py index 24404504..99f21c8a 100644 --- a/bauh/gems/arch/dependencies.py +++ b/bauh/gems/arch/dependencies.py @@ -456,7 +456,9 @@ def fill_providers_deps(self, missing_deps: List[Tuple[str, str]], return missing_deps def map_all_required_by(self, pkgnames: Iterable[str], to_ignore: Set[str]) -> Set[str]: - to_ignore.update(pkgnames) + if pkgnames: + to_ignore.update(pkgnames) + all_requirements = {req for reqs in pacman.map_required_by(pkgnames).values() for req in reqs if req not in to_ignore} if all_requirements: diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 903d605e..cc33fdd3 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -4,8 +4,7 @@ from typing import List, Set, Tuple, Dict, Iterable from bauh.commons import system -from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, SimpleProcess, \ - ProcessHandler +from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, SimpleProcess from bauh.commons.util import size_to_byte from bauh.gems.arch.exceptions import PackageNotFoundException @@ -539,16 +538,6 @@ def upgrade_system(root_password: str) -> SimpleProcess: return SimpleProcess(cmd=['pacman', '-Syyu', '--noconfirm'], root_password=root_password) -def get_dependencies_to_remove(pkgs: Iterable[str], root_password: str) -> Dict[str, str]: - proc = SimpleProcess(cmd=['pacman', '-R', *pkgs, '--confirm'], root_password=root_password) - success, output = ProcessHandler().handle_simple(proc) - - if not output: - return {} - - return {t[1]: t[0] for t in RE_REMOVE_TRANSITIVE_DEPS.findall(output)} - - def fill_provided_map(key: str, val: str, output: dict): current_val = output.get(key) @@ -744,8 +733,12 @@ def upgrade_several(pkgnames: Iterable[str], root_password: str, overwrite_confl error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}) -def remove_several(pkgnames: Iterable[str], root_password: str) -> SystemProcess: +def remove_several(pkgnames: Iterable[str], root_password: str, skip_checks: bool = False) -> SystemProcess: cmd = ['pacman', '-R', *pkgnames, '--noconfirm'] + + if skip_checks: + cmd.append('-dd') + if root_password: return SystemProcess(new_root_subprocess(cmd, root_password), wrong_error_phrase='warning:') else: @@ -865,6 +858,51 @@ def map_all_deps(names: Iterable[str], only_installed: bool = False) -> Dict[str return res +def map_required_dependencies(*names: str) -> Dict[str, Set[str]]: + output = run_cmd('pacman -Qi {}'.format(' '.join(names) if names else '')) + + if output: + res = {} + latest_name, deps, latest_field = None, None, None + + for l in output.split('\n'): + if l: + if l[0] != ' ': + line = l.strip() + field_sep_idx = line.index(':') + field = line[0:field_sep_idx].strip() + + if field == 'Name': + val = line[field_sep_idx + 1:].strip() + latest_name = val + deps = None + elif field == 'Depends On': + val = line[field_sep_idx + 1:].strip() + + if deps is None: + deps = set() + + if val != 'None': + if ':' in val: + dep_info = val.split(':') + deps.add(dep_info[0].strip()) + else: + deps.update({dep.strip() for dep in val.split(' ') if dep}) + + elif latest_name and deps is not None: + res[latest_name] = deps + latest_name, deps, latest_field = None, None, None + + elif latest_name and deps is not None: + if ':' in l: + dep_info = l.split(':') + deps.add(dep_info[0].strip()) + else: + deps.update({dep.strip() for dep in l.split(' ') if dep}) + + return res + + def get_cache_dir() -> str: dir_pattern = re.compile(r'.*CacheDir\s*=\s*.+') @@ -887,8 +925,8 @@ def get_cache_dir() -> str: return '/var/cache/pacman/pkg' -def map_required_by(names: Iterable[str]) -> Dict[str, Set[str]]: - output = run_cmd('pacman -Qi {}'.format(' '.join(names))) +def map_required_by(names: Iterable[str] = None) -> Dict[str, Set[str]]: + output = run_cmd('pacman -Qi {}'.format(' '.join(names) if names else '')) if output: res = {} diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index ee010f8d..11bacc94 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -10,6 +10,7 @@ from bauh.gems.arch.dependencies import DependenciesAnalyser from bauh.gems.arch.exceptions import PackageNotFoundException from bauh.gems.arch.model import ArchPackage +from bauh.gems.arch.pacman import RE_DEP_OPERATORS from bauh.view.util.translation import I18n @@ -181,45 +182,13 @@ def _fill_conflicts(self, context: UpdateRequirementsContext, blacklist: Iterabl root_conflict = self._filter_and_map_conflicts(context) - sub_conflict = pacman.get_dependencies_to_remove(root_conflict.keys(), context.root_password) if root_conflict else None - - to_remove_map = {} - if sub_conflict: - for dep, source in sub_conflict.items(): - if dep not in to_remove_map and (not blacklist or dep not in blacklist): - req = ArchPackage(name=dep, installed=True, i18n=self.i18n) - to_remove_map[dep] = req - reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), source) - context.to_remove[dep] = UpgradeRequirement(req, reason) - if root_conflict: for dep, source in root_conflict.items(): - if dep not in to_remove_map and (not blacklist or dep not in blacklist): + if dep not in context.to_remove and (not blacklist or dep not in blacklist): req = ArchPackage(name=dep, installed=True, i18n=self.i18n) - to_remove_map[dep] = req reason = "{} '{}'".format(self.i18n['arch.info.conflicts with'].capitalize(), source) context.to_remove[dep] = UpgradeRequirement(req, reason) - if to_remove_map: - for name in to_remove_map.keys(): # upgrading lists - if name in context.pkgs_data: - del context.pkgs_data[name] - - if name in context.aur_to_update: - del context.aur_to_update[name] - - if name in context.repo_to_update: - del context.repo_to_update[name] - - removed_size = pacman.get_installed_size([*to_remove_map.keys()]) - - if removed_size: - for name, size in removed_size.items(): - if size is not None: - req = context.to_remove.get(name) - if req: - req.extra_size = size - def _map_and_add_package(self, pkg_data: Tuple[str, str], idx: int, output: dict): version = None @@ -334,7 +303,7 @@ def __fill_aur_index(self, context: UpdateRequirementsContext): context.aur_index.update(names) self.logger.info("AUR index loaded on the context") - def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, installed_sizes: Dict[str, int] = None, to_install: bool = False, to_upgrade: Set[str] = None) -> UpgradeRequirement: + def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, installed_sizes: Dict[str, int] = None, to_install: bool = False, to_sync: Set[str] = None) -> UpgradeRequirement: requirement = UpgradeRequirement(pkg) if pkg.repository != 'aur': @@ -347,10 +316,32 @@ def _map_requirement(self, pkg: ArchPackage, context: UpdateRequirementsContext, if current_size is not None and data['s']: requirement.extra_size = data['s'] - current_size - if to_install and context.pkgs_data and context.to_update: + required_by = set() + + if to_install and to_sync and context.pkgs_data: names = context.pkgs_data[pkg.name].get('p', {pkg.name}) - required_by = ','.join([p for p in to_upgrade if p != pkg.name and context.pkgs_data[p]['d'] and any([n for n in names if n in context.pkgs_data[p]['d']])]) - requirement.reason = '{}: {}'.format(self.i18n['arch.info.required by'].capitalize(), required_by if required_by else '?') + to_sync_deps_cache = {} + for p in to_sync: + if p != pkg.name and p in context.pkgs_data: + deps = to_sync_deps_cache.get(p) + + if deps is None: + deps = context.pkgs_data[p]['d'] + + if deps is None: + deps = set() + else: + deps = {RE_DEP_OPERATORS.split(d)[0] for d in deps} + + to_sync_deps_cache[p] = deps + + if deps: + for n in names: + if n in deps: + required_by.add(p) + break + + requirement.reason = '{}: {}'.format(self.i18n['arch.info.required by'].capitalize(), ','.join(required_by) if required_by else '?') return requirement @@ -403,6 +394,8 @@ def summarize(self, pkgs: List[ArchPackage], root_password: str, arch_config: di self.logger.error("Package '{}' not found".format(e.name)) return + self.__update_context_based_on_to_remove(context) + if context.to_update: installed_sizes = pacman.get_installed_size(list(context.to_update.keys())) @@ -427,7 +420,140 @@ def summarize(self, pkgs: List[ArchPackage], root_password: str, arch_config: di res.cannot_upgrade = [d for d in context.cannot_upgrade.values()] if context.to_install: - to_upgrade = {r.pkg.name for r in res.to_upgrade} if res.to_upgrade else None - res.to_install = [self._map_requirement(p, context, to_install=True, to_upgrade=to_upgrade) for p in context.to_install.values()] + to_sync = {r.pkg.name for r in res.to_upgrade} if res.to_upgrade else {} + to_sync.update(context.to_install.keys()) + res.to_install = [self._map_requirement(p, context, to_install=True, to_sync=to_sync) for p in context.to_install.values()] return res + + def __update_context_based_on_to_remove(self, context: UpdateRequirementsContext): + if context.to_remove: + to_remove_provided = {} + # filtering all package to synchronization from the transaction context + to_sync = {*(context.to_update.keys() if context.to_update else set()), *(context.to_install.keys() if context.to_install else set())} + if to_sync: # checking if any packages to sync on the context rely on the 'to remove' ones + to_remove_provided.update(pacman.map_provided(remote=False, pkgs=context.to_remove.keys())) + to_remove_from_sync = {} # will store all packages that should be removed + + for pname in to_sync: + if pname in context.pkgs_data: + deps = context.pkgs_data[pname].get('d') + + if deps: + required = set() + + for pkg in context.to_remove: + for provided in to_remove_provided[pkg]: + if provided in deps: + required.add(pkg) + break + + if required: + to_remove_from_sync[pname] = required + else: + self.logger.warning("Conflict resolution: package '{}' marked to synchronization has no data loaded") + + if to_remove_from_sync: # removing all these packages and their dependents from the context + self._add_to_remove(to_sync, to_remove_from_sync, context) + + # checking if the installed packages that are not in the transaction context rely on the current packages to be removed: + current_to_remove = {*context.to_remove.keys()} + required_by_installed = self.deps_analyser.map_all_required_by(current_to_remove, {*to_sync}) + + if required_by_installed: + # updating provided context: + provided_not_mapped = set() + for pkg in current_to_remove.difference({*to_remove_provided.keys()}): + if pkg not in context.pkgs_data: + provided_not_mapped.add(pkg) + else: + provided = context.pkgs_data[pkg].get('p') + if provided: + to_remove_provided[pkg] = provided + else: + provided_not_mapped.add(pkg) + + if provided_not_mapped: + to_remove_provided.update(pacman.map_provided(remote=False, pkgs=provided_not_mapped)) + + deps_no_data = {dep for dep in required_by_installed if dep in context.pkgs_data} + deps_nodata_deps = pacman.map_required_dependencies(*deps_no_data) if deps_no_data else {} + + reverse_to_remove_provided = {p: name for name, provided in to_remove_provided for p in provided} + + for pkg in required_by_installed: + if pkg not in context.to_remove: + if pkg in context.pkgs_data: + dep_deps = context.pkgs_data[pkg].get('d') + else: + dep_deps = deps_nodata_deps.get(pkg) + + if dep_deps: + source = ', '.join((reverse_to_remove_provided[d] for d in dep_deps if d in reverse_to_remove_provided)) + reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), source if source else '?') + context.to_remove[pkg] = UpgradeRequirement(pkg=ArchPackage(name=pkg, + installed=True, + i18n=self.i18n), + reason=reason) + + for name in context.to_remove: # upgrading lists + if name in context.pkgs_data: + del context.pkgs_data[name] + + if name in context.aur_to_update: + del context.aur_to_update[name] + + if name in context.repo_to_update: + del context.repo_to_update[name] + + removed_size = pacman.get_installed_size([*context.to_remove.keys()]) + + if removed_size: + for name, size in removed_size.items(): + if size is not None: + req = context.to_remove.get(name) + if req: + req.extra_size = size + + def _add_to_remove(self, pkgs_to_sync: Set[str], names: Dict[str, Set[str]], context: UpdateRequirementsContext, to_ignore: Set[str] = None): + blacklist = to_ignore if to_ignore else set() + blacklist.update(names) + + dependents = {} + for pname in pkgs_to_sync: + if pname not in blacklist: + data = context.pkgs_data.get(pname) + + if data: + deps = data.get('d') + + if deps: + for n in names: + if n in deps: + all_deps = dependents.get(n, set()) + all_deps.update(pname) + dependents[n] = all_deps + + else: + self.logger.warning("Package '{}' to sync could not be removed from the transaction context because its data was not loaded") + + for n in names: + if n in context.pkgs_data: + if n not in context.to_remove: + depends_on = names.get(n) + if depends_on: + reason = "{} '{}'".format(self.i18n['arch.info.depends on'].capitalize(), ', '.join(depends_on)) + else: + reason = '?' + + context.to_remove[n] = UpgradeRequirement(pkg=ArchPackage(name=n, + installed=True, + i18n=self.i18n), + reason=reason) + + all_deps = dependents.get(n) + + if all_deps: + self._add_to_remove(pkgs_to_sync, {dep: {n} for dep in all_deps}, context, blacklist) + else: + self.logger.warning("Package '{}' could not be removed from the transaction context because its data was not loaded") diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 7a561002..10a66c95 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -192,7 +192,7 @@ def __init__(self, manager: SoftwareManager, i18n: I18n, pkgs: List[PackageView] self.manager = manager self.i18n = i18n - def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None) -> InputOption: + def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None, required_size: bool = True) -> InputOption: if req.pkg.installed: icon_path = req.pkg.get_disk_icon_path() @@ -204,7 +204,7 @@ def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_t size_str = '{}: {}'.format(self.i18n['size'].capitalize(), '?' if req.extra_size is None else get_human_size_str(req.extra_size)) - if req.extra_size != req.required_size: + if required_size and req.extra_size != req.required_size: size_str += ' ( {}: {} )'.format(self.i18n['action.update.pkg.required_size'].capitalize(), '?' if req.required_size is None else get_human_size_str(req.required_size)) @@ -250,7 +250,7 @@ def _gen_to_install_form(self, reqs: List[UpgradeRequirement]) -> Tuple[FormComp return FormComponent(label=lb, components=comps), (required_size, extra_size) def _gen_to_remove_form(self, reqs: List[UpgradeRequirement]) -> FormComponent: - opts = [self._req_as_option(r, False, r.reason) for r in reqs] + opts = [self._req_as_option(r, False, r.reason, required_size=False) for r in reqs] comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))] required_size, extra_size = self._sum_pkgs_size(reqs) From b68a21894e82b4fbbda152349f013b01dbb35bec Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 6 Aug 2020 14:52:46 -0300 Subject: [PATCH 07/99] [arch] improvement -> upgrade: only removing packages after downloading the required ones (when multi-threaded download is enabled) --- CHANGELOG.md | 1 + bauh/gems/arch/controller.py | 93 ++++++++++++++++++++---------- bauh/gems/arch/resources/locale/ca | 1 + bauh/gems/arch/resources/locale/de | 1 + bauh/gems/arch/resources/locale/en | 1 + bauh/gems/arch/resources/locale/es | 1 + bauh/gems/arch/resources/locale/it | 1 + bauh/gems/arch/resources/locale/pt | 1 + bauh/gems/arch/resources/locale/ru | 1 + bauh/gems/arch/resources/locale/tr | 1 + 10 files changed, 72 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd035d76..7aaf7b59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

+ - upgrade: only removing packages after downloading the required ones (when multi-threaded download is enabled) ### Fixes - Arch diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 752ae501..c33bee57 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -769,16 +769,15 @@ def _map_conflicting_file(self, output: str) -> List[TextComponent]: return files def _upgrade_repo_pkgs(self, pkgs: List[str], handler: ProcessHandler, root_password: str, arch_config: dict, overwrite_files: bool = False, - status_handler: TransactionStatusHandler = None, already_downloaded: bool = False, sizes: Dict[str, int] = None) -> bool: + status_handler: TransactionStatusHandler = None, skip_download: bool = False, sizes: Dict[str, int] = None, already_downloaded: int = 0) -> bool: - downloaded = 0 - - if not already_downloaded: - if self._should_download_packages(arch_config): - try: - downloaded = self._download_packages(pkgs, handler, root_password, sizes) - except ArchDownloadException: - return False + if not skip_download and self._should_download_packages(arch_config): + try: + downloaded = self._download_packages(pkgs, handler, root_password, sizes) + except ArchDownloadException: + return False + else: + downloaded = already_downloaded try: if status_handler: @@ -823,7 +822,7 @@ def _upgrade_repo_pkgs(self, pkgs: List[str], handler: ProcessHandler, root_pass root_password=root_password, overwrite_files=True, status_handler=output_handler, - already_downloaded=True, + skip_download=True, arch_config=arch_config, sizes=sizes) else: @@ -843,6 +842,37 @@ def _upgrade_repo_pkgs(self, pkgs: List[str], handler: ProcessHandler, root_pass traceback.print_exc() return False + def _remove_transaction_packages(self, requirements: UpgradeRequirements, handler: ProcessHandler, root_password: str) -> bool: + to_remove_names = {r.pkg.name for r in requirements.to_remove} + output_handler = TransactionStatusHandler(watcher=handler.watcher, + i18n=self.i18n, + pkgs_to_sync=0, + logger=self.logger, + pkgs_to_remove=len(to_remove_names)) + output_handler.start() + try: + success = handler.handle(pacman.remove_several(pkgnames=to_remove_names, root_password=root_password, skip_checks=True), + output_handler=output_handler.handle) + + if not success: + self.logger.error("Could not remove packages: {}".format(', '.join(to_remove_names))) + output_handler.stop_working() + output_handler.join() + return False + + return True + except: + self.logger.error("An error occured while removing packages: {}".format(', '.join(to_remove_names))) + traceback.print_exc() + output_handler.stop_working() + output_handler.join() + return False + + def _show_upgrade_download_failed(self, watcher: ProcessWatcher): + watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['arch.upgrade.mthreaddownload.fail'], + type_=MessageType.ERROR) + def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool: self.aur_client.clean_caches() watcher.change_status("{}...".format(self.i18n['manage_window.status.upgrading'])) @@ -869,31 +899,32 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher arch_config = read_config() self._sync_databases(arch_config=arch_config, root_password=root_password, handler=handler) - if requirements.to_remove: - to_remove_names = {r.pkg.name for r in requirements.to_remove} - output_handler = TransactionStatusHandler(watcher=handler.watcher, - i18n=self.i18n, - pkgs_to_sync=0, - logger=self.logger, - pkgs_to_remove=len(to_remove_names)) - output_handler.start() - try: - success = handler.handle(pacman.remove_several(to_remove_names, root_password), output_handler=output_handler.handle) + # for now if the multithreaded-download is disabled packages will be removed before the download: + multithreaded_download = self._should_download_packages(arch_config) - if not success: - self.logger.error("Could not remove packages: {}".format(', '.join(to_remove_names))) - output_handler.stop_working() - output_handler.join() - return False - except: - self.logger.error("An error occured while removing packages: {}".format(', '.join(to_remove_names))) - traceback.print_exc() - output_handler.stop_working() - output_handler.join() + if requirements.to_remove and (not repo_pkgs or not multithreaded_download): + if not self._remove_transaction_packages(requirements, handler, root_password): return False if repo_pkgs: repo_pkgs_names = [p.name for p in repo_pkgs] + + downloaded = -1 + if requirements.to_remove and multithreaded_download: # pre-downloading all packages before removing any + try: + downloaded = self._download_packages(repo_pkgs_names, handler, root_password, pkg_sizes) + + if downloaded < len(repo_pkgs_names): + self._show_upgrade_download_failed(handler.watcher) + return False + + except ArchDownloadException: + self._show_upgrade_download_failed(handler.watcher) + return False + + if not self._remove_transaction_packages(requirements, handler, root_password): + return False + watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_repo_pkgs'])) self.logger.info("Upgrading {} repository packages: {}".format(len(repo_pkgs_names), ', '.join(repo_pkgs_names))) @@ -902,6 +933,8 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher handler=handler, root_password=root_password, arch_config=arch_config, + skip_download=downloaded >= 0, + already_downloaded=0 if downloaded < 0 else downloaded, sizes=pkg_sizes): return False diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 7f114f37..12a4dd5d 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -199,6 +199,7 @@ arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed +arch.upgrade.mthreaddownload.fail=It was not possible to download all packages for upgrading arch.upgrade.success=Package {} successfully upgraded arch.upgrade.upgrade_aur_pkgs=Upgrading AUR packages arch.upgrade.upgrade_repo_pkgs=Upgrading packages from repositories diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 3f556069..053c80d3 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -199,6 +199,7 @@ arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed +arch.upgrade.mthreaddownload.fail=It was not possible to download all packages for upgrading arch.upgrade.success=Package {} successfully upgraded arch.upgrade.upgrade_aur_pkgs=Upgrading AUR packages arch.upgrade.upgrade_repo_pkgs=Upgrading packages from repositories diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index e50f16c5..197975e8 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -199,6 +199,7 @@ arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed +arch.upgrade.mthreaddownload.fail=It was not possible to download all packages for upgrading arch.upgrade.success=Package {} successfully upgraded arch.upgrade.upgrade_aur_pkgs=Upgrading AUR packages arch.upgrade.upgrade_repo_pkgs=Upgrading packages from repositories diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 62ddba54..b69b0b21 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -199,6 +199,7 @@ arch.upgrade.error.conflicting_files=Algunos de los paquetes que se están actua arch.upgrade.conflicting_files.proceed=Permitir y continuar arch.upgrade.conflicting_files.stop=Cancelar la actualización arch.upgrade.fail=Falló la actualización del paquete {} +arch.upgrade.mthreaddownload.fail=No fue posible descargar todos los paquetes para actualizar arch.upgrade.success=Paquete {} actualizado con éxito arch.upgrade.upgrade_aur_pkgs=Actualizando paquetes de AUR arch.upgrade.upgrade_repo_pkgs=Actualizando paquetes de repositorios diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index ec76bfa3..4294be60 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -199,6 +199,7 @@ arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed +arch.upgrade.mthreaddownload.fail=It was not possible to download all packages for upgrading arch.upgrade.success=Package {} successfully upgraded arch.upgrade.upgrade_aur_pkgs=Upgrading AUR packages arch.upgrade.upgrade_repo_pkgs=Upgrading packages from repositories diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 0c80cfcb..e5ba5d0e 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -198,6 +198,7 @@ arch.upgrade.error.conflicting_files=Alguns dos pacotes que estão sendo atualiz arch.upgrade.conflicting_files.proceed=Permitir e continuar arch.upgrade.conflicting_files.stop=Cancelar atualização arch.upgrade.fail=Atualização do pacote {} falhou +arch.upgrade.mthreaddownload.fail=Não foi possível baixar todos os pacotes para atualizar arch.upgrade.success=Pacote {} atualizado com sucesso arch.upgrade.upgrade_aur_pkgs=Atualizando pacotes do AUR arch.upgrade.upgrade_repo_pkgs=Atualizando pacotes dos repositórios diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 67d9b0f8..ed3444f3 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -199,6 +199,7 @@ arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed +arch.upgrade.mthreaddownload.fail=It was not possible to download all packages for upgrading arch.upgrade.success=Package {} successfully upgraded arch.upgrade.upgrade_aur_pkgs=Upgrading AUR packages arch.upgrade.upgrade_repo_pkgs=Upgrading packages from repositories diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 471b1461..ebe03ea9 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -199,6 +199,7 @@ arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail={} paketi yükseltilemedi +arch.upgrade.mthreaddownload.fail=It was not possible to download all packages for upgrading arch.upgrade.success={} paketi başarıyla yükseltildi arch.upgrade.upgrade_aur_pkgs=AUR paketleri yükseltiliyor arch.upgrade.upgrade_repo_pkgs=Resmi depo paketleri yükseltiliyor From 57c88f9589d513c950332c4dc9af388cb187aef0 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 11:08:09 -0300 Subject: [PATCH 08/99] [flatpak] improvement -> Creating the exports path '~/.local/share/flatpak/exports/share' (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages --- CHANGELOG.md | 2 ++ bauh/commons/system.py | 10 ++++++---- bauh/gems/flatpak/__init__.py | 2 ++ bauh/gems/flatpak/controller.py | 29 ++++++++++++++++++++++++++++- bauh/gems/flatpak/flatpak.py | 13 ++++++++----- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aaf7b59..2c77f449 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- upgrade: only removing packages after downloading the required ones (when multi-threaded download is enabled) +- Flatpak + - Creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. ### Fixes - Arch diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 5655ec7c..4624578e 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -216,13 +216,14 @@ def run_cmd(cmd: str, expected_code: int = 0, ignore_return_code: bool = False, def new_subprocess(cmd: List[str], cwd: str = '.', shell: bool = False, stdin = None, - global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG) -> subprocess.Popen: + global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG, + extra_paths: Set[str] = None) -> subprocess.Popen: args = { "stdout": PIPE, "stderr": PIPE, "cwd": cwd, "shell": shell, - "env": gen_env(global_interpreter, lang) + "env": gen_env(global_interpreter, lang, extra_paths) } if input: @@ -232,7 +233,8 @@ def new_subprocess(cmd: List[str], cwd: str = '.', shell: bool = False, stdin = def new_root_subprocess(cmd: List[str], root_password: str, cwd: str = '.', - global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG) -> subprocess.Popen: + global_interpreter: bool = USE_GLOBAL_INTERPRETER, lang: str = DEFAULT_LANG, + extra_paths: Set[str] = None) -> subprocess.Popen: pwdin, final_cmd = None, [] if root_password is not None: @@ -241,7 +243,7 @@ def new_root_subprocess(cmd: List[str], root_password: str, cwd: str = '.', final_cmd.extend(cmd) - return subprocess.Popen(final_cmd, stdin=pwdin, stdout=PIPE, stderr=PIPE, cwd=cwd, env=gen_env(global_interpreter, lang)) + return subprocess.Popen(final_cmd, stdin=pwdin, stdout=PIPE, stderr=PIPE, cwd=cwd, env=gen_env(global_interpreter, lang, extra_paths)) def notify_user(msg: str, app_name: str, icon_path: str): diff --git a/bauh/gems/flatpak/__init__.py b/bauh/gems/flatpak/__init__.py index cc5933d2..69e08aae 100644 --- a/bauh/gems/flatpak/__init__.py +++ b/bauh/gems/flatpak/__init__.py @@ -1,4 +1,5 @@ import os +from pathlib import Path from bauh.api.constants import CONFIG_PATH @@ -7,3 +8,4 @@ CONFIG_FILE = '{}/flatpak.yml'.format(CONFIG_PATH) CONFIG_DIR = '{}/flatpak'.format(CONFIG_PATH) UPDATES_IGNORED_FILE = '{}/updates_ignored.txt'.format(CONFIG_DIR) +EXPORTS_PATH = '{}/.local/share/flatpak/exports/share'.format(str(Path.home())) diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 76e2febe..0f79781e 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -18,7 +18,7 @@ from bauh.commons.config import save_config from bauh.commons.html import strip_html, bold from bauh.commons.system import SystemProcess, ProcessHandler -from bauh.gems.flatpak import flatpak, SUGGESTIONS_FILE, CONFIG_FILE, UPDATES_IGNORED_FILE, CONFIG_DIR +from bauh.gems.flatpak import flatpak, SUGGESTIONS_FILE, CONFIG_FILE, UPDATES_IGNORED_FILE, CONFIG_DIR, EXPORTS_PATH from bauh.gems.flatpak.config import read_config from bauh.gems.flatpak.constants import FLATHUB_API_URL from bauh.gems.flatpak.model import FlatpakApplication @@ -166,6 +166,10 @@ def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_app return SearchResult(models, None, len(models)) def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool: + + if not self._make_exports_dir(watcher): + return False + handler = ProcessHandler(watcher) pkg.commit = flatpak.get_commit(pkg.id, pkg.branch, pkg.installation) @@ -198,6 +202,10 @@ def clean_cache_for(self, pkg: FlatpakApplication): def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher: ProcessWatcher) -> bool: flatpak_version = flatpak.get_version() + + if not self._make_exports_dir(watcher): + return False + for req in requirements.to_upgrade: watcher.change_status("{} {} ({})...".format(self.i18n['manage_window.status.upgrading'], req.pkg.name, req.pkg.version)) related, deps = False, False @@ -227,6 +235,10 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher return True def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher, disk_loader: DiskCacheLoader) -> TransactionResult: + + if not self._make_exports_dir(watcher): + return TransactionResult.fail() + uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref, pkg.installation))) if uninstalled: @@ -298,6 +310,18 @@ def get_history(self, pkg: FlatpakApplication) -> PackageHistory: return PackageHistory(pkg=pkg, history=commits, pkg_status_idx=status_idx) + def _make_exports_dir(self, watcher: ProcessWatcher) -> bool: + if not os.path.exists(EXPORTS_PATH): + self.logger.info("Creating dir '{}'".format(EXPORTS_PATH)) + watcher.print('Creating dir {}'.format(EXPORTS_PATH)) + try: + os.mkdir(EXPORTS_PATH) + except: + watcher.print('Error while creating the directory {}'.format(EXPORTS_PATH)) + return False + + return True + def install(self, pkg: FlatpakApplication, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher) -> TransactionResult: config = read_config() @@ -348,6 +372,9 @@ def install(self, pkg: FlatpakApplication, root_password: str, disk_loader: Disk installed = flatpak.list_installed(flatpak_version) installed_by_level = {'{}:{}:{}'.format(p['id'], p['name'], p['branch']) for p in installed if p['installation'] == pkg.installation} if installed else None + if not self._make_exports_dir(handler.watcher): + return TransactionResult(success=False, installed=[], removed=[]) + res = handler.handle(SystemProcess(subproc=flatpak.install(str(pkg.id), pkg.origin, pkg.installation), wrong_error_phrase='Warning')) if res: diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index 56eda91d..e16fa310 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -7,6 +7,7 @@ from bauh.api.exception import NoInternetException from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess, SimpleProcess, ProcessHandler from bauh.commons.util import size_to_byte +from bauh.gems.flatpak import EXPORTS_PATH RE_SEVERAL_SPACES = re.compile(r'\s+') @@ -164,7 +165,7 @@ def update(app_ref: str, installation: str, related: bool = False, deps: bool = if not deps: cmd.append('--no-deps') - return new_subprocess(cmd) + return new_subprocess(cmd=cmd, extra_paths={EXPORTS_PATH}) def uninstall(app_ref: str, installation: str): @@ -173,7 +174,8 @@ def uninstall(app_ref: str, installation: str): :param app_ref: :return: """ - return new_subprocess(['flatpak', 'uninstall', app_ref, '-y', '--{}'.format(installation)]) + return new_subprocess(cmd=['flatpak', 'uninstall', app_ref, '-y', '--{}'.format(installation)], + extra_paths={EXPORTS_PATH}) def list_updates_as_str(version: str) -> Dict[str, set]: @@ -231,9 +233,9 @@ def downgrade(app_ref: str, commit: str, installation: str, root_password: str) cmd = ['flatpak', 'update', '--no-related', '--no-deps', '--commit={}'.format(commit), app_ref, '-y', '--{}'.format(installation)] if installation == 'system': - return new_root_subprocess(cmd, root_password) + return new_root_subprocess(cmd=cmd, root_password=root_password, extra_paths={EXPORTS_PATH}) else: - return new_subprocess(cmd) + return new_subprocess(cmd=cmd, extra_paths={EXPORTS_PATH}) def get_app_commits(app_ref: str, origin: str, installation: str, handler: ProcessHandler) -> List[str]: @@ -354,7 +356,8 @@ def search(version: str, word: str, installation: str, app_id: bool = False) -> def install(app_id: str, origin: str, installation: str): - return new_subprocess(['flatpak', 'install', origin, app_id, '-y', '--{}'.format(installation)]) + return new_subprocess(cmd=['flatpak', 'install', origin, app_id, '-y', '--{}'.format(installation)], + extra_paths={EXPORTS_PATH}) def set_default_remotes(installation: str, root_password: str = None) -> SimpleProcess: From 444dca56418fdccdd1fab935d7bf1536534c16e5 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 11:21:05 -0300 Subject: [PATCH 09/99] [flatpak] fix -> downgrading crashing with version 1.8.X | history: the top commit is returned as '(null)' --- CHANGELOG.md | 3 +++ bauh/gems/flatpak/controller.py | 14 +++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c77f449..87bfb355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - not displaying all packages that must be uninstalled - displaying "required size" for packages that must be uninstalled - some conflict resolution scenarios when upgrading several packages +- Flatpak + - downgrading crashing with version 1.8.X + - history: the top commit is returned as "(null)" - UI - crashing when nothing can be upgraded diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 0f79781e..cd0e41a5 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -180,7 +180,13 @@ def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: Proces if commits is None: return False - commit_idx = commits.index(pkg.commit) + try: + commit_idx = commits.index(pkg.commit) + except ValueError: + if commits[0] == '(null)': + commit_idx = 0 + else: + return False # downgrade is not possible if the app current commit in the first one: if commit_idx == len(commits) - 1: @@ -301,13 +307,19 @@ def get_info(self, app: FlatpakApplication) -> dict: def get_history(self, pkg: FlatpakApplication) -> PackageHistory: pkg.commit = flatpak.get_commit(pkg.id, pkg.branch, pkg.installation) commits = flatpak.get_app_commits_data(pkg.ref, pkg.origin, pkg.installation) + status_idx = 0 + commit_found = False for idx, data in enumerate(commits): if data['commit'] == pkg.commit: status_idx = idx + commit_found = True break + if not commit_found and pkg.commit and commits[0]['commit'] == '(null)': + commits[0]['commit'] = pkg.commit + return PackageHistory(pkg=pkg, history=commits, pkg_status_idx=status_idx) def _make_exports_dir(self, watcher: ProcessWatcher) -> bool: From 31b67a32b734c38dfec86c0be2c0fe47f14f5fd0 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 11:24:41 -0300 Subject: [PATCH 10/99] [arch] improvements -> upgrading firstly the keyring packages declared in 'SyncFirst' (/etc/pacman.conf) to avoid pacman downloading issues | only removing packages after downloading the required ones --- CHANGELOG.md | 4 +- bauh/api/abstract/controller.py | 1 + bauh/gems/arch/controller.py | 195 +++++++++++++++++++++----------- bauh/gems/arch/pacman.py | 17 +++ bauh/gems/arch/updates.py | 1 + 5 files changed, 149 insertions(+), 69 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aaf7b59..00ba46fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- - upgrade: only removing packages after downloading the required ones (when multi-threaded download is enabled) + - upgrade: + - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues + - only removing packages after downloading the required ones ### Fixes - Arch diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 91fd2989..3b7d5774 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -58,6 +58,7 @@ def __init__(self, to_install: List[UpgradeRequirement], to_remove: List[Upgrade self.to_remove = to_remove # when an upgrading package conflicts with a not upgrading package ( check all the non-upgrading packages deps an add here [including those selected to upgrade as well] self.to_upgrade = to_upgrade self.cannot_upgrade = cannot_upgrade + self.context = {} # caches relevant data to actually perform the upgrade class TransactionResult: diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index c33bee57..93453c41 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -11,7 +11,7 @@ from math import floor from pathlib import Path from threading import Thread -from typing import List, Set, Type, Tuple, Dict, Iterable +from typing import List, Set, Type, Tuple, Dict, Iterable, Optional import requests @@ -768,25 +768,93 @@ def _map_conflicting_file(self, output: str) -> List[TextComponent]: return files - def _upgrade_repo_pkgs(self, pkgs: List[str], handler: ProcessHandler, root_password: str, arch_config: dict, overwrite_files: bool = False, - status_handler: TransactionStatusHandler = None, skip_download: bool = False, sizes: Dict[str, int] = None, already_downloaded: int = 0) -> bool: + def list_related(self, pkgs: Iterable[str], all_pkgs: Iterable[str], data: Dict[str, dict], related: Set[str], provided_map: Dict[str, Set[str]]) -> Set[str]: + related.update(pkgs) + + deps = set() + + for pkg in pkgs: + pkg_deps = data[pkg]['d'] + + if pkg_deps: + deps.update({d for d in pkg_deps if d not in related}) + + if deps: + if not provided_map: + for p in all_pkgs: + for provided in data[p].get('p', {p}): + sources = provided_map.get(provided, set()) + provided_map[provided] = sources + sources.add(p) + + added_sources = set() + for dep in deps: + sources = provided_map.get(dep) + + if sources: + for source in sources: + if source not in related: + related.add(source) + added_sources.add(source) + + if added_sources: + self.list_related(added_sources, all_pkgs, data, related, provided_map) + + return related + + def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str]], handler: ProcessHandler, root_password: str, + multithread_download: bool, pkgs_data: Dict[str, dict], overwrite_files: bool = False, + status_handler: TransactionStatusHandler = None, sizes: Dict[str, int] = None, download: bool = True, + check_syncfirst: bool = True) -> bool: + + to_sync_first = None + if check_syncfirst: + to_sync_first = [p for p in pacman.get_packages_to_sync_first() if p.endswith('-keyring') and p in to_upgrade] + + if to_sync_first: + self.logger.info("Upgrading keyrings marked as 'SyncFirst'") + if not self._upgrade_repo_pkgs(to_upgrade=to_sync_first, + to_remove=None, + handler=handler, + root_password=root_password, + sizes=sizes, + download=True, + multithread_download=multithread_download, + pkgs_data=pkgs_data, + check_syncfirst=False): + return False + + to_upgrade_remaining = [p for p in to_upgrade if p not in to_sync_first] if to_sync_first else to_upgrade - if not skip_download and self._should_download_packages(arch_config): + # pre-downloading all packages before removing any + if download: try: - downloaded = self._download_packages(pkgs, handler, root_password, sizes) + downloaded = self._download_packages(pkgnames=to_upgrade_remaining, + handler=handler, + root_password=root_password, + sizes=sizes, + multithreaded=multithread_download) + + if downloaded < len(to_upgrade_remaining): + self._show_upgrade_download_failed(handler.watcher) + return False + except ArchDownloadException: + self._show_upgrade_download_failed(handler.watcher) return False - else: - downloaded = already_downloaded + + if to_remove and not self._remove_transaction_packages(to_remove, handler, root_password): + return False try: if status_handler: output_handler = status_handler else: - output_handler = TransactionStatusHandler(handler.watcher, self.i18n, len(pkgs), self.logger, downloading=downloaded) + output_handler = TransactionStatusHandler(handler.watcher, self.i18n, len(to_upgrade), self.logger, downloading=len(to_upgrade_remaining)) output_handler.start() - success, upgrade_output = handler.handle_simple(pacman.upgrade_several(pkgnames=pkgs, + self.logger.info("Upgrading {} repository packages: {}".format(len(to_upgrade), ', '.join(to_upgrade))) + success, upgrade_output = handler.handle_simple(pacman.upgrade_several(pkgnames=to_upgrade_remaining, root_password=root_password, overwrite_conflicting_files=overwrite_files), output_handler=output_handler.handle,) @@ -797,10 +865,10 @@ def _upgrade_repo_pkgs(self, pkgs: List[str], handler: ProcessHandler, root_pass output_handler.join() handler.watcher.print("Repository packages successfully upgraded") handler.watcher.change_substatus(self.i18n['arch.upgrade.caching_pkgs_data']) - repo_map = pacman.map_repositories(pkgs) + repo_map = pacman.map_repositories(to_upgrade_remaining) pkg_map = {} - for name in pkgs: + for name in to_upgrade_remaining: repo = repo_map.get(name) pkg_map[name] = ArchPackage(name=name, repository=repo, @@ -817,13 +885,16 @@ def _upgrade_repo_pkgs(self, pkgs: List[str], handler: ProcessHandler, root_pass confirmation_label=self.i18n['arch.upgrade.conflicting_files.stop'], components=files): - return self._upgrade_repo_pkgs(pkgs=pkgs, + return self._upgrade_repo_pkgs(to_upgrade=to_upgrade_remaining, handler=handler, root_password=root_password, overwrite_files=True, status_handler=output_handler, - skip_download=True, - arch_config=arch_config, + multithread_download=multithread_download, + download=False, + check_syncfirst=False, + pkgs_data=pkgs_data, + to_remove=None, sizes=sizes) else: output_handler.stop_working() @@ -842,27 +913,26 @@ def _upgrade_repo_pkgs(self, pkgs: List[str], handler: ProcessHandler, root_pass traceback.print_exc() return False - def _remove_transaction_packages(self, requirements: UpgradeRequirements, handler: ProcessHandler, root_password: str) -> bool: - to_remove_names = {r.pkg.name for r in requirements.to_remove} + def _remove_transaction_packages(self, to_remove: Set[str], handler: ProcessHandler, root_password: str) -> bool: output_handler = TransactionStatusHandler(watcher=handler.watcher, i18n=self.i18n, pkgs_to_sync=0, logger=self.logger, - pkgs_to_remove=len(to_remove_names)) + pkgs_to_remove=len(to_remove)) output_handler.start() try: - success = handler.handle(pacman.remove_several(pkgnames=to_remove_names, root_password=root_password, skip_checks=True), + success = handler.handle(pacman.remove_several(pkgnames=to_remove, root_password=root_password, skip_checks=True), output_handler=output_handler.handle) if not success: - self.logger.error("Could not remove packages: {}".format(', '.join(to_remove_names))) + self.logger.error("Could not remove packages: {}".format(', '.join(to_remove))) output_handler.stop_working() output_handler.join() return False return True except: - self.logger.error("An error occured while removing packages: {}".format(', '.join(to_remove_names))) + self.logger.error("An error occured while removing packages: {}".format(', '.join(to_remove))) traceback.print_exc() output_handler.stop_working() output_handler.join() @@ -899,45 +969,19 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher arch_config = read_config() self._sync_databases(arch_config=arch_config, root_password=root_password, handler=handler) - # for now if the multithreaded-download is disabled packages will be removed before the download: - multithreaded_download = self._should_download_packages(arch_config) - - if requirements.to_remove and (not repo_pkgs or not multithreaded_download): - if not self._remove_transaction_packages(requirements, handler, root_password): - return False - if repo_pkgs: - repo_pkgs_names = [p.name for p in repo_pkgs] - - downloaded = -1 - if requirements.to_remove and multithreaded_download: # pre-downloading all packages before removing any - try: - downloaded = self._download_packages(repo_pkgs_names, handler, root_password, pkg_sizes) - - if downloaded < len(repo_pkgs_names): - self._show_upgrade_download_failed(handler.watcher) - return False - - except ArchDownloadException: - self._show_upgrade_download_failed(handler.watcher) - return False - - if not self._remove_transaction_packages(requirements, handler, root_password): - return False - - watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_repo_pkgs'])) - self.logger.info("Upgrading {} repository packages: {}".format(len(repo_pkgs_names), - ', '.join(repo_pkgs_names))) - - if not self._upgrade_repo_pkgs(pkgs=repo_pkgs_names, + 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, root_password=root_password, - arch_config=arch_config, - skip_download=downloaded >= 0, - already_downloaded=0 if downloaded < 0 else downloaded, + multithread_download=self._multithreaded_download_enabled(arch_config), + pkgs_data=requirements.context['data'], sizes=pkg_sizes): return False + 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: watcher.change_status('{}...'.format(self.i18n['arch.upgrade.upgrade_aur_pkgs'])) for pkg in aur_pkgs: @@ -1450,10 +1494,10 @@ def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]] return {dep} downloaded = 0 - if self._should_download_packages(context.config): + if self._multithreaded_download_enabled(context.config): try: pkg_sizes = pacman.map_download_sizes(repo_dep_names) - downloaded = self._download_packages(repo_dep_names, context.handler, context.root_password, pkg_sizes) + downloaded = self._download_packages(repo_dep_names, context.handler, context.root_password, pkg_sizes, multithreaded=True) except ArchDownloadException: return False @@ -1756,21 +1800,36 @@ def _install_optdeps(self, context: TransactionContext) -> bool: return True - def _should_download_packages(self, arch_config: dict) -> bool: + def _multithreaded_download_enabled(self, arch_config: dict) -> bool: return bool(arch_config['repositories_mthread_download']) \ and self.context.file_downloader.is_multithreaded() \ and pacman.is_mirrors_available() - def _download_packages(self, pkgnames: List[str], handler: ProcessHandler, root_password: str, sizes: Dict[str, int] = None) -> int: - download_service = MultithreadedDownloadService(file_downloader=self.context.file_downloader, - http_client=self.http_client, - logger=self.context.logger, - i18n=self.i18n) - self.logger.info("Initializing multi-threaded download for {} package(s)".format(len(pkgnames))) - return download_service.download_packages(pkgs=pkgnames, - handler=handler, - sizes=sizes, - root_password=root_password) + def _download_packages(self, pkgnames: List[str], handler: ProcessHandler, root_password: str, sizes: Dict[str, int] = None, multithreaded: bool = True) -> int: + if multithreaded: + download_service = MultithreadedDownloadService(file_downloader=self.context.file_downloader, + http_client=self.http_client, + logger=self.context.logger, + i18n=self.i18n) + self.logger.info("Initializing multi-threaded download for {} repository package(s)".format(len(pkgnames))) + return download_service.download_packages(pkgs=pkgnames, + handler=handler, + sizes=sizes, + root_password=root_password) + else: + self.logger.info("Downloading {} repository package(s)".format(len(pkgnames))) + output_handler = TransactionStatusHandler(handler.watcher, self.i18n, len(pkgnames), self.logger) + output_handler.start() + try: + success, _ = handler.handle_simple(pacman.download(root_password, *pkgnames), output_handler=output_handler.handle) + + if success: + return len(pkgnames) + else: + raise ArchDownloadException() + except: + traceback.print_exc() + raise ArchDownloadException() def _install(self, context: TransactionContext) -> bool: check_install_output = [] @@ -1830,13 +1889,13 @@ def _install(self, context: TransactionContext) -> bool: to_install.append(pkgpath) downloaded = 0 - if self._should_download_packages(context.config): + if self._multithreaded_download_enabled(context.config): to_download = [p for p in to_install if not p.startswith('/')] if to_download: try: pkg_sizes = pacman.map_download_sizes(to_download) - downloaded = self._download_packages(to_download, context.handler, context.root_password, pkg_sizes) + downloaded = self._download_packages(to_download, context.handler, context.root_password, pkg_sizes, multithreaded=True) except ArchDownloadException: return False diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index cc33fdd3..1c7b7aa0 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -18,6 +18,7 @@ RE_UPDATE_REQUIRED_FIELDS = re.compile(r'(\bProvides\b|\bInstalled Size\b|\bConflicts With\b)\s*:\s(.+)\n') RE_REMOVE_TRANSITIVE_DEPS = re.compile(r'removing\s([\w\-_]+)\s.+required\sby\s([\w\-_]+)\n?') RE_AVAILABLE_MIRRORS = re.compile(r'.+\s+OK\s+.+\s+(\d+:\d+)\s+.+(http.+)') +RE_PACMAN_SYNC_FIRST = re.compile(r'SyncFirst\s*=\s*(.+)') def is_available() -> bool: @@ -733,6 +734,12 @@ def upgrade_several(pkgnames: Iterable[str], root_password: str, overwrite_confl error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}) +def download(root_password: str, *pkgnames: str) -> SimpleProcess: + return SimpleProcess(cmd=['pacman', '-Swdd', *pkgnames, '--noconfirm'], + root_password=root_password, + error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}) + + def remove_several(pkgnames: Iterable[str], root_password: str, skip_checks: bool = False) -> SystemProcess: cmd = ['pacman', '-R', *pkgnames, '--noconfirm'] @@ -1119,3 +1126,13 @@ def get_mirrors_branch() -> str: _, output = system.run(['pacman-mirrors', '-G']) return output.strip() + +def get_packages_to_sync_first() -> Set[str]: + if os.path.exists('/etc/pacman.conf'): + with open('/etc/pacman.conf') as f: + to_sync_first = RE_PACMAN_SYNC_FIRST.findall(f.read()) + + if to_sync_first: + return {s.strip() for s in to_sync_first[0].split(' ') if s and s.strip()} + + return set() diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index 11bacc94..66cebfce 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -424,6 +424,7 @@ def summarize(self, pkgs: List[ArchPackage], root_password: str, arch_config: di to_sync.update(context.to_install.keys()) res.to_install = [self._map_requirement(p, context, to_install=True, to_sync=to_sync) for p in context.to_install.values()] + res.context['data'] = context.pkgs_data return res def __update_context_based_on_to_remove(self, context: UpdateRequirementsContext): From a0fbf52ad4331d8f82504ed62d81795dadb0428c Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 11:34:23 -0300 Subject: [PATCH 11/99] [arch] improvement -> pacman as the default downloader for repository packages --- CHANGELOG.md | 1 + bauh/gems/arch/config.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00ba46fe..a1ae098a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - upgrade: - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues - only removing packages after downloading the required ones + - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. ### Fixes - Arch diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index 2c511bc3..abdb0d27 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -11,6 +11,6 @@ def read_config(update_file: bool = False) -> dict: "refresh_mirrors_startup": False, "sync_databases_startup": True, 'mirrors_sort_limit': 5, - 'repositories_mthread_download': True, + 'repositories_mthread_download': False, 'automatch_providers': True} return read(CONFIG_FILE, template, update_file=update_file) From 4de2eb317ed5d0f9bb6cc010d5caad02b43f59ed Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 13:38:35 -0300 Subject: [PATCH 12/99] [appimage] improvement -> manual file installation default search path set to '~/Downloads' --- CHANGELOG.md | 2 ++ bauh/api/abstract/view.py | 3 ++- bauh/gems/appimage/controller.py | 7 ++++++- bauh/view/qt/components.py | 2 ++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f78ac834..84c6a2fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.9.7] 2020 ### Improvements +- AppImage + - Manual file installation default search path set to '~/Downloads' - Arch - upgrade summary: displaying the reason a given package must be installed

diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index 3e3a7369..7995add7 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -182,13 +182,14 @@ def get_component(self, id_: str) -> ViewComponent: class FileChooserComponent(ViewComponent): def __init__(self, allowed_extensions: Set[str] = None, label: str = None, tooltip: str = None, - file_path: str = None, max_width: int = -1, id_: str = None): + file_path: str = None, max_width: int = -1, id_: str = None, search_path: str = None): super(FileChooserComponent, self).__init__(id_=id_) self.label = label self.allowed_extensions = allowed_extensions self.file_path = file_path self.tooltip = tooltip self.max_width = max_width + self.search_path = search_path class TabComponent(ViewComponent): diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 18bac25d..b787814d 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -69,7 +69,12 @@ def __init__(self, context: ApplicationContext): icon_path=resource.get_path('img/upgrade.svg', ROOT_DIR))] def install_file(self, root_password: str, watcher: ProcessWatcher) -> bool: - file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), allowed_extensions={'AppImage'}) + default_path = '{}/Downloads'.format(str(Path.home())) + + if not os.path.isdir(default_path): + default_path = None + + file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), allowed_extensions={'AppImage'}, search_path=default_path) input_name = TextInputComponent(label=self.i18n['name'].capitalize()) input_version = TextInputComponent(label=self.i18n['version'].capitalize()) input_description = TextInputComponent(label=self.i18n['description'].capitalize()) diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 16f1e159..4501953b 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -840,6 +840,8 @@ def open_chooser(e): if c.file_path and os.path.isfile(c.file_path): cur_path = c.file_path + elif c.search_path and os.path.exists(c.search_path): + cur_path = c.search_path else: cur_path = str(Path.home()) From 244d48f18adf2d07a73850ea8bd3e96ff56c5652 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 14:01:04 -0300 Subject: [PATCH 13/99] [appimage] fix -> manual file installation: crashing the application icon is not on the extracted folder root path --- CHANGELOG.md | 3 +++ bauh/gems/appimage/controller.py | 11 +++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84c6a2fa..42496ebf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. ### Fixes +- AppImage + - manual file installation: + - crashing the application icon is not on the extracted folder root path [#132](https://github.com/vinifmor/bauh/issues/132) - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) - Downloading some AUR packages sources twice when multi-threaded download is enabled diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index b787814d..8daa2e73 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -419,10 +419,9 @@ def _find_desktop_file(self, folder: str) -> str: return f def _find_icon_file(self, folder: str) -> str: - for r, d, files in os.walk(folder): - for f in files: - if RE_ICON_ENDS_WITH.match(f): - return f + for f in glob.glob(folder + ('/**' if not folder.endswith('/') else '**'), recursive=True): + if RE_ICON_ENDS_WITH.match(f): + return f def install(self, pkg: AppImage, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher) -> TransactionResult: handler = ProcessHandler(watcher) @@ -508,8 +507,8 @@ def install(self, pkg: AppImage, root_password: str, disk_loader: DiskCacheLoade extracted_icon = self._find_icon_file(extracted_folder) if extracted_icon: - icon_path = out_dir + '/logo.' + extracted_icon.split('.')[-1] - shutil.copy('{}/{}'.format(extracted_folder, extracted_icon), icon_path) + icon_path = out_dir + '/logo.' + extracted_icon.split('/')[-1].split('.')[-1] + shutil.copy(extracted_icon, icon_path) de_content = RE_DESKTOP_ICON.sub('Icon={}\n'.format(icon_path), de_content) pkg.icon_path = icon_path From ae721f6eea4af86c1b0a175222fd834fec8be215 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 15:38:30 -0300 Subject: [PATCH 14/99] [appimage] improvement -> manual file installation: trying to auto-fill the 'Name' and 'Version' fields --- CHANGELOG.md | 4 +++- bauh/api/abstract/view.py | 29 +++++++++++++++++++++++++++-- bauh/gems/appimage/controller.py | 30 ++++++++++++++++++++++++++++-- bauh/view/qt/components.py | 30 ++++++++++++++++++++---------- 4 files changed, 78 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42496ebf..3f1eb744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.9.7] 2020 ### Improvements - AppImage - - Manual file installation default search path set to '~/Downloads' + - Manual file installation: + - default search path set to '~/Downloads' + - trying to auto-fill the 'Name' and 'Version' fields - Arch - upgrade summary: displaying the reason a given package must be installed

diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index 7995add7..bd0adf52 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -1,6 +1,6 @@ from abc import ABC from enum import Enum -from typing import List, Set +from typing import List, Set, Optional class MessageType: @@ -9,12 +9,22 @@ class MessageType: ERROR = 2 +class ViewObserver: + + def on_change(self, change): + pass + + class ViewComponent(ABC): """ Represents a GUI component """ - def __init__(self, id_: str): + def __init__(self, id_: str, observers: List[ViewObserver] = None): self.id = id_ + self.observers = observers if observers else [] + + def add_observer(self, obs): + self.observers.append(obs) class SpacerComponent(ViewComponent): @@ -157,6 +167,14 @@ def get_value(self) -> str: else: return '' + def set_value(self, val: Optional[str], caller: object = None): + self.value = val + + if self.observers: + for o in self.observers: + if caller != o: + o.on_change(val) + def get_int_value(self) -> int: if self.value is not None: val = self.value.strip() if isinstance(self.value, str) else self.value @@ -191,6 +209,13 @@ def __init__(self, allowed_extensions: Set[str] = None, label: str = None, toolt self.max_width = max_width self.search_path = search_path + def set_file_path(self, fpath: str): + self.file_path = fpath + + if self.observers: + for o in self.observers: + o.on_change(self.file_path) + class TabComponent(ViewComponent): diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 8daa2e73..db995849 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -23,7 +23,7 @@ from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ SuggestionPriority, CustomSoftwareAction from bauh.api.abstract.view import MessageType, ViewComponent, FormComponent, InputOption, SingleSelectComponent, \ - SelectViewType, TextInputComponent, PanelComponent, FileChooserComponent + SelectViewType, TextInputComponent, PanelComponent, FileChooserComponent, ViewObserver from bauh.commons import resource from bauh.commons.config import save_config from bauh.commons.html import bold @@ -42,6 +42,28 @@ RE_DESKTOP_EXEC = re.compile(r'Exec\s*=\s*.+\n') RE_DESKTOP_ICON = re.compile(r'Icon\s*=\s*.+\n') RE_ICON_ENDS_WITH = re.compile(r'.+\.(png|svg)$') +RE_APPIMAGE_NAME = re.compile(r'(.+)\.appimage', flags=re.IGNORECASE) + + +class ManualInstallationFileObserver(ViewObserver): + + def __init__(self, name: TextInputComponent, version: TextInputComponent): + self.name = name + self.version = version + + def on_change(self, file_path: str): + if file_path: + name_found = RE_APPIMAGE_NAME.findall(file_path.split('/')[-1]) + + if name_found: + name_split = name_found[0].split('-') + self.name.set_value(name_split[0].strip()) + + if len(name_split) > 1: + self.version.set_value(name_split[1].strip()) + else: + self.name.set_value(None) + self.version.set_value(None) class AppImageManager(SoftwareManager): @@ -74,9 +96,13 @@ def install_file(self, root_password: str, watcher: ProcessWatcher) -> bool: if not os.path.isdir(default_path): default_path = None - file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), allowed_extensions={'AppImage'}, search_path=default_path) + file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), + allowed_extensions={'AppImage'}, + search_path=default_path) input_name = TextInputComponent(label=self.i18n['name'].capitalize()) input_version = TextInputComponent(label=self.i18n['version'].capitalize()) + file_chooser.observers.append(ManualInstallationFileObserver(input_name, input_version)) + input_description = TextInputComponent(label=self.i18n['description'].capitalize()) cat_ops = [InputOption(label=self.i18n['category.none'].capitalize(), value=0)] diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 4501953b..273d27b5 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -11,7 +11,7 @@ from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType, \ TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent, PanelComponent, \ - TwoStateButtonComponent, TextComponent, SpacerComponent, RangeInputComponent + TwoStateButtonComponent, TextComponent, SpacerComponent, RangeInputComponent, ViewObserver from bauh.view.qt import css from bauh.view.qt.colors import RED from bauh.view.util import resource @@ -428,6 +428,15 @@ def __init__(self, model: SingleSelectComponent): self.layout().addWidget(FormComboBoxQt(model), 0, 1) +class QLineEditObserver(QLineEdit, ViewObserver): + + def __init__(self, **kwargs): + super(QLineEditObserver, self).__init__(**kwargs) + + def on_change(self, change: str): + self.setText(change if change is not None else '') + + class TextInputQt(QGroupBox): def __init__(self, model: TextInputComponent): @@ -440,7 +449,7 @@ def __init__(self, model: TextInputComponent): if self.model.max_width > 0: self.setMaximumWidth(self.model.max_width) - self.text_input = QLineEdit() + self.text_input = QLineEditObserver() if model.only_int: self.text_input.setValidator(QIntValidator()) @@ -457,10 +466,11 @@ def __init__(self, model: TextInputComponent): self.text_input.textChanged.connect(self._update_model) + self.model.observers.append(self.text_input) self.layout().addWidget(self.text_input, 0, 1) def _update_model(self, text: str): - self.model.value = text + self.model.set_value(val=text, caller=self) class MultipleSelectQt(QGroupBox): @@ -754,7 +764,7 @@ def gen_tip_icon(self, tip: str) -> QLabel: return tip_icon def _new_text_input(self, c: TextInputComponent) -> Tuple[QLabel, QLineEdit]: - line_edit = QLineEdit() + line_edit = QLineEditObserver() if c.only_int: line_edit.setValidator(QIntValidator()) @@ -773,9 +783,10 @@ def _new_text_input(self, c: TextInputComponent) -> Tuple[QLabel, QLineEdit]: line_edit.setEnabled(False) def update_model(text: str): - c.value = text + c.set_value(val=text, caller=line_edit) line_edit.textChanged.connect(update_model) + c.observers.append(line_edit) label = QWidget() label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) @@ -819,7 +830,7 @@ def _wrap(self, comp: QWidget, model: ViewComponent) -> QWidget: return field_container def _new_file_chooser(self, c: FileChooserComponent) -> Tuple[QLabel, QLineEdit]: - chooser = QLineEdit() + chooser = QLineEditObserver() chooser.setReadOnly(True) if c.max_width > 0: @@ -828,6 +839,7 @@ def _new_file_chooser(self, c: FileChooserComponent) -> Tuple[QLabel, QLineEdit] if c.file_path: chooser.setText(c.file_path) + c.observers.append(chooser) chooser.setPlaceholderText(self.i18n['view.components.file_chooser.placeholder']) def open_chooser(e): @@ -848,14 +860,12 @@ def open_chooser(e): file_path, _ = QFileDialog.getOpenFileName(self, self.i18n['file_chooser.title'], cur_path, exts, options=options) if file_path: - c.file_path = file_path - chooser.setText(file_path) + c.set_file_path(file_path) chooser.setCursorPosition(0) def clean_path(): - c.file_path = None - chooser.setText('') + c.set_file_path(None) chooser.mousePressEvent = open_chooser From a7f8e1cb08d10de9792add7d83e4ed8ce659a9a0 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 15:45:09 -0300 Subject: [PATCH 15/99] [appimage] improvement -> changing 'imported' parentheses style --- CHANGELOG.md | 4 ++++ bauh/gems/appimage/model.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f1eb744..05b2c187 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Manual file installation: - default search path set to '~/Downloads' - trying to auto-fill the 'Name' and 'Version' fields + - Arch - upgrade summary: displaying the reason a given package must be installed

@@ -19,9 +20,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues - only removing packages after downloading the required ones - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. + - Flatpak - Creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. +- minor UI improvements + ### Fixes - AppImage - manual file installation: diff --git a/bauh/gems/appimage/model.py b/bauh/gems/appimage/model.py index c22bc3a5..ec9bc559 100644 --- a/bauh/gems/appimage/model.py +++ b/bauh/gems/appimage/model.py @@ -100,7 +100,7 @@ def has_screenshots(self): def get_display_name(self) -> str: if self.name and self.imported: - return '{} ( {} )'.format(self.name, self.i18n['imported']) + return '{} ({})'.format(self.name, self.i18n['imported']) return self.name From 69f83ac6da72f2906fdb1087a0cbbd39598d74cd Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 16:00:11 -0300 Subject: [PATCH 16/99] Updating CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05b2c187..33e04472 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. - Flatpak - - Creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. + - Creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) - minor UI improvements From adb0459ae26c68e78beed5e61bc9e5b22594cab0 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 18:16:50 -0300 Subject: [PATCH 17/99] [appimage] -> improvement: manual file upgrade -> auto-filling version and settings default search path to '~/Downloads' --- CHANGELOG.md | 2 +- bauh/gems/appimage/__init__.py | 6 ++++++ bauh/gems/appimage/controller.py | 26 ++++++++++++++------------ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33e04472..0b6aba06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.9.7] 2020 ### Improvements - AppImage - - Manual file installation: + - Manual file installation/upgrade: - default search path set to '~/Downloads' - trying to auto-fill the 'Name' and 'Version' fields diff --git a/bauh/gems/appimage/__init__.py b/bauh/gems/appimage/__init__.py index 6a577ed1..b2242bf3 100644 --- a/bauh/gems/appimage/__init__.py +++ b/bauh/gems/appimage/__init__.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from typing import Optional from bauh.api.constants import CONFIG_PATH from bauh.commons import resource @@ -16,3 +17,8 @@ def get_icon_path() -> str: return resource.get_path('img/appimage.svg', ROOT_DIR) + + +def get_default_manual_installation_file_dir() -> Optional[str]: + default_path = '{}/Downloads'.format(str(Path.home())) + return default_path if os.path.isdir(default_path) else None diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index db995849..087af469 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -11,7 +11,7 @@ from math import floor from pathlib import Path from threading import Lock -from typing import Set, Type, List, Tuple +from typing import Set, Type, List, Tuple, Optional from colorama import Fore @@ -29,7 +29,7 @@ from bauh.commons.html import bold from bauh.commons.system import SystemProcess, new_subprocess, ProcessHandler, run_cmd, SimpleProcess from bauh.gems.appimage import query, INSTALLATION_PATH, LOCAL_PATH, SUGGESTIONS_FILE, CONFIG_FILE, ROOT_DIR, \ - CONFIG_DIR, UPDATES_IGNORED_FILE, util + CONFIG_DIR, UPDATES_IGNORED_FILE, util, get_default_manual_installation_file_dir from bauh.gems.appimage.config import read_config from bauh.gems.appimage.model import AppImage from bauh.gems.appimage.worker import DatabaseUpdater, SymlinksVerifier @@ -47,7 +47,7 @@ class ManualInstallationFileObserver(ViewObserver): - def __init__(self, name: TextInputComponent, version: TextInputComponent): + def __init__(self, name: Optional[TextInputComponent], version: TextInputComponent): self.name = name self.version = version @@ -57,12 +57,16 @@ def on_change(self, file_path: str): if name_found: name_split = name_found[0].split('-') - self.name.set_value(name_split[0].strip()) + + if self.name: + self.name.set_value(name_split[0].strip()) if len(name_split) > 1: self.version.set_value(name_split[1].strip()) else: - self.name.set_value(None) + if self.name: + self.name.set_value(None) + self.version.set_value(None) @@ -91,14 +95,9 @@ def __init__(self, context: ApplicationContext): icon_path=resource.get_path('img/upgrade.svg', ROOT_DIR))] def install_file(self, root_password: str, watcher: ProcessWatcher) -> bool: - default_path = '{}/Downloads'.format(str(Path.home())) - - if not os.path.isdir(default_path): - default_path = None - file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), allowed_extensions={'AppImage'}, - search_path=default_path) + search_path=get_default_manual_installation_file_dir()) input_name = TextInputComponent(label=self.i18n['name'].capitalize()) input_version = TextInputComponent(label=self.i18n['version'].capitalize()) file_chooser.observers.append(ManualInstallationFileObserver(input_name, input_version)) @@ -150,8 +149,11 @@ def install_file(self, root_password: str, watcher: ProcessWatcher) -> bool: return res def update_file(self, pkg: AppImage, root_password: str, watcher: ProcessWatcher): - file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), allowed_extensions={'AppImage'}) + file_chooser = FileChooserComponent(label=self.i18n['file'].capitalize(), + allowed_extensions={'AppImage'}, + search_path=get_default_manual_installation_file_dir()) input_version = TextInputComponent(label=self.i18n['version'].capitalize()) + file_chooser.observers.append(ManualInstallationFileObserver(None, input_version)) while True: if watcher.request_confirmation(title=self.i18n['appimage.custom_action.manual_update.details'], body=None, From d426397584e1001f4d2a242bc84da0a42bac54ab Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 11 Aug 2020 18:35:31 -0300 Subject: [PATCH 18/99] [ui] fix -> random C++ wrapper errors with some forms due to missing references --- CHANGELOG.md | 1 + bauh/view/qt/components.py | 33 ++++++++++++++++++++++----------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b6aba06..068953cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - history: the top commit is returned as "(null)" - UI - crashing when nothing can be upgraded + - random C++ wrapper errors with some forms due to missing references ## [0.9.6] 2020-06-26 diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 273d27b5..90603fdf 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -696,35 +696,46 @@ def __init__(self, model: FormComponent, i18n: I18n): if model.spaces: self.layout().addRow(QLabel(), QLabel()) - for c in model.components: + for idx, c in enumerate(model.components): if isinstance(c, TextInputComponent): label, field = self._new_text_input(c) self.layout().addRow(label, field) elif isinstance(c, SingleSelectComponent): label = self._new_label(c) - field = FormComboBoxQt(c) if c.type == SelectViewType.COMBO else FormRadioSelectQt(c) - self.layout().addRow(label, self._wrap(field, c)) + form = FormComboBoxQt(c) if c.type == SelectViewType.COMBO else FormRadioSelectQt(c) + field = self._wrap(form, c) + self.layout().addRow(label, field) elif isinstance(c, RangeInputComponent): label = self._new_label(c) - self.layout().addRow(label, self._wrap(self._new_range_input(c), c)) + field = self._wrap(self._new_range_input(c), c) + self.layout().addRow(label, field) elif isinstance(c, FileChooserComponent): label, field = self._new_file_chooser(c) self.layout().addRow(label, field) elif isinstance(c, FormComponent): - self.layout().addRow(FormQt(c, self.i18n)) + label, field = None, FormQt(c, self.i18n) + self.layout().addRow(field) elif isinstance(c, TwoStateButtonComponent): - label = self._new_label(c) - self.layout().addRow(label, TwoStateButtonQt(c)) + label, field = self._new_label(c), TwoStateButtonQt(c) + self.layout().addRow(label, field) elif isinstance(c, MultipleSelectComponent): - label = self._new_label(c) - self.layout().addRow(label, FormMultipleSelectQt(c)) + label, field = self._new_label(c), FormMultipleSelectQt(c) + self.layout().addRow(label, field) elif isinstance(c, TextComponent): - self.layout().addRow(self._new_label(c), QWidget()) + label, field = self._new_label(c), QWidget() + self.layout().addRow(label, field) elif isinstance(c, RangeInputComponent): - self.layout() + label, field = self._new_label(c), self._new_range_input(c) + self.layout().addRow(label, field) else: raise Exception('Unsupported component type {}'.format(c.__class__.__name__)) + if label: # to prevent C++ wrap errors + setattr(self, 'label_{}'.format(idx), label) + + if field: # to prevent C++ wrap errors + setattr(self, 'field_{}'.format(idx), field) + if model.spaces: self.layout().addRow(QLabel(), QLabel()) From 12b7acc5591a6f16d826b141bfc1dc85e56d9cd0 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 12 Aug 2020 10:28:00 -0300 Subject: [PATCH 19/99] [flatpak] fix -> installation fails when there are multiple references for a given package --- CHANGELOG.md | 4 +++ bauh/commons/system.py | 12 +++++++-- bauh/gems/flatpak/controller.py | 31 ++++++++++++++++++++--- bauh/gems/flatpak/flatpak.py | 7 ++--- bauh/gems/flatpak/resources/locale/ca | 2 ++ bauh/gems/flatpak/resources/locale/de | 2 ++ bauh/gems/flatpak/resources/locale/en | 2 ++ bauh/gems/flatpak/resources/locale/es | 2 ++ bauh/gems/flatpak/resources/locale/it | 2 ++ bauh/gems/flatpak/resources/locale/pt | 2 ++ bauh/gems/flatpak/resources/locale/ru | 2 ++ bauh/gems/flatpak/resources/locale/tr | 2 ++ pictures/releases/0.9.7/flatpak_refs.png | Bin 0 -> 26246 bytes 13 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 pictures/releases/0.9.7/flatpak_refs.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 068953cf..004d4679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" + - installation fails when there are multiple references for a given package (e.g: openh264) +

+ +

- UI - crashing when nothing can be upgraded - random C++ wrapper errors with some forms due to missing references diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 4624578e..1684682b 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -65,7 +65,7 @@ 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): + extra_paths: Set[str] = None, error_phrases: Set[str] = None, wrong_error_phrases: Set[str] = None): pwdin, final_cmd = None, [] if root_password is not None: @@ -77,6 +77,7 @@ def __init__(self, cmd: List[str], cwd: str = '.', expected_code: int = 0, self.instance = self._new(final_cmd, cwd, global_interpreter, lang, stdin=pwdin, extra_paths=extra_paths) self.expected_code = expected_code self.error_phrases = error_phrases + self.wrong_error_phrases = wrong_error_phrases def _new(self, cmd: List[str], cwd: str, global_interpreter: bool, lang: str, stdin = None, extra_paths: Set[str] = None) -> subprocess.Popen: @@ -182,11 +183,18 @@ def handle_simple(self, proc: SimpleProcess, output_handler=None) -> Tuple[bool, success = proc.instance.returncode == proc.expected_code string_output = output.read() - if proc.error_phrases: + if not success and proc.wrong_error_phrases: + for phrase in proc.wrong_error_phrases: + if phrase in string_output: + success = True + break + + if success and proc.error_phrases: for phrase in proc.error_phrases: if phrase in string_output: success = False break + return success, string_output diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index cd0e41a5..d91b49d1 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -1,4 +1,5 @@ import os +import re import traceback from datetime import datetime from math import floor @@ -25,6 +26,7 @@ from bauh.gems.flatpak.worker import FlatpakAsyncDataLoader, FlatpakUpdateLoader DATE_FORMAT = '%Y-%m-%dT%H:%M:%S.000Z' +RE_INSTALL_REFS = re.compile(r'\d+\)\s+(.+)') class FlatpakManager(SoftwareManager): @@ -387,9 +389,30 @@ def install(self, pkg: FlatpakApplication, root_password: str, disk_loader: Disk if not self._make_exports_dir(handler.watcher): return TransactionResult(success=False, installed=[], removed=[]) - res = handler.handle(SystemProcess(subproc=flatpak.install(str(pkg.id), pkg.origin, pkg.installation), wrong_error_phrase='Warning')) + installed, output = handler.handle_simple(flatpak.install(str(pkg.id), pkg.origin, pkg.installation)) + + if not installed and 'error: No ref chosen to resolve matches' in output: + ref_opts = RE_INSTALL_REFS.findall(output) + + if ref_opts and len(ref_opts) > 1: + view_opts = [InputOption(label=o, value=o.strip()) for o in ref_opts if o] + ref_select = SingleSelectComponent(type_=SelectViewType.RADIO, options=view_opts, default_option=view_opts[0], label='') + if watcher.request_confirmation(title=self.i18n['flatpak.install.ref_choose.title'], + body=self.i18n['flatpak.install.ref_choose.body'].format(bold(pkg.name)), + components=[ref_select], + confirmation_label=self.i18n['proceed'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + ref = ref_select.get_selected() + installed, output = handler.handle_simple(flatpak.install(ref, pkg.origin, pkg.installation)) + pkg.ref = ref + pkg.runtime = 'runtime' in ref + else: + watcher.print('Aborted by the user') + return TransactionResult.fail() + else: + return TransactionResult.fail() - if res: + if installed: try: fields = flatpak.get_fields(str(pkg.id), pkg.branch, ['Ref', 'Branch']) @@ -399,7 +422,7 @@ def install(self, pkg: FlatpakApplication, root_password: str, disk_loader: Disk except: traceback.print_exc() - if res: + if installed: new_installed = [pkg] current_installed = flatpak.list_installed(flatpak_version) current_installed_by_level = [p for p in current_installed if p['installation'] == pkg.installation] if current_installed else None @@ -413,7 +436,7 @@ def install(self, pkg: FlatpakApplication, root_password: str, disk_loader: Disk new_installed.append(self._map_to_model(app_json=p, installed=True, disk_loader=disk_loader, internet=net_available)) - return TransactionResult(success=res, installed=new_installed, removed=[]) + return TransactionResult(success=installed, installed=new_installed, removed=[]) else: return TransactionResult.fail() diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index e16fa310..640a2129 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -355,9 +355,10 @@ def search(version: str, word: str, installation: str, app_id: bool = False) -> return found -def install(app_id: str, origin: str, installation: str): - return new_subprocess(cmd=['flatpak', 'install', origin, app_id, '-y', '--{}'.format(installation)], - extra_paths={EXPORTS_PATH}) +def install(app_id: str, origin: str, installation: str) -> SimpleProcess: + return SimpleProcess(cmd=['flatpak', 'install', origin, app_id, '-y', '--{}'.format(installation)], + extra_paths={EXPORTS_PATH}, + wrong_error_phrases={'Warning'}) def set_default_remotes(installation: str, root_password: str = None) -> SimpleProcess: diff --git a/bauh/gems/flatpak/resources/locale/ca b/bauh/gems/flatpak/resources/locale/ca index 89eec7dd..a20ee7bd 100644 --- a/bauh/gems/flatpak/resources/locale/ca +++ b/bauh/gems/flatpak/resources/locale/ca @@ -48,6 +48,8 @@ flatpak.info.version=versió flatpak.install.bad_install_level.body=Valor invàlid per a {field} al fitxer de configuració {file} flatpak.install.install_level.body=S'ha d'instal·lar {} per a tots els usuaris del dispositiu ( sistema ) ? flatpak.install.install_level.title=Tipus d'instal·lació +flatpak.install.ref_choose.title=Multiple references +flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=Si no voleu utilitzar aplicacions Flatpak, desmarqueu {} a {} flatpak.notification.no_remotes=No hi ha dipòsits («remotes») del Flatpak configurats. No podreu cercar aplicacions Flatpak. flatpak.remotes.system_flathub.error=No s'ha pogut afegir Flathub com a dipòsit del sistema ( remote ) diff --git a/bauh/gems/flatpak/resources/locale/de b/bauh/gems/flatpak/resources/locale/de index 6ead2323..dcc8bbc9 100644 --- a/bauh/gems/flatpak/resources/locale/de +++ b/bauh/gems/flatpak/resources/locale/de @@ -48,6 +48,8 @@ flatpak.info.version=Version flatpak.install.bad_install_level.body=Ungültiger Wert für {field} in der Konfigurationsdatei {file} flatpak.install.install_level.body=Sollte {} für alle Gerätebenutzer installiert werden ( system ) ? flatpak.install.install_level.title=Installationstyp +flatpak.install.ref_choose.title=Multiple references +flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=Um keine Flatpak Anwendungen zu verwenden deaktiviere {} unter {} flatpak.notification.no_remotes=Keine Flatpak Remotes sind konfiguriert. Die Suche nach Flatpak Apps ist nicht möglich. flatpak.remotes.system_flathub.error=Flathub konnte nicht als System-Repository ( remote ) hinzugefügt werden diff --git a/bauh/gems/flatpak/resources/locale/en b/bauh/gems/flatpak/resources/locale/en index 10ae4ff9..253b3229 100644 --- a/bauh/gems/flatpak/resources/locale/en +++ b/bauh/gems/flatpak/resources/locale/en @@ -48,6 +48,8 @@ flatpak.info.version=version flatpak.install.bad_install_level.body=Invalid value for {field} in the configuration file {file} flatpak.install.install_level.body=Should {} be installed for all the device users ( system ) ? flatpak.install.install_level.title=Installation type +flatpak.install.ref_choose.title=Multiple references +flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=If you do not want to use Flatpak applications, uncheck {} in {} flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps. flatpak.remotes.system_flathub.error=It was not possible to add Flathub as a system repository ( remote ) diff --git a/bauh/gems/flatpak/resources/locale/es b/bauh/gems/flatpak/resources/locale/es index 2c0fc09e..af99e2cc 100644 --- a/bauh/gems/flatpak/resources/locale/es +++ b/bauh/gems/flatpak/resources/locale/es @@ -48,6 +48,8 @@ flatpak.info.version=versión flatpak.install.bad_install_level.body=Valor inválido para {field} en el archivo de configuración {file} flatpak.install.install_level.body=¿Debería {} estar instalado para todos los usuarios del dispositivo ( sistema )? flatpak.install.install_level.title=Tipo de instalación +flatpak.install.ref_choose.title=Varias referencias +flatpak.install.ref_choose.body=Hay varias referencias para {}. Seleccione una para proceder: flatpak.notification.disable=Si no desea usar aplicativos Flatpak, desmarque {} en {} flatpak.notification.no_remotes=No hay repositorios (remotes) Flatpak configurados. No será posible buscar aplicativos Flatpak. flatpak.remotes.system_flathub.error=No fue posible agregar Flathub como repositorio del sistema ( remote ) diff --git a/bauh/gems/flatpak/resources/locale/it b/bauh/gems/flatpak/resources/locale/it index ff3a5ec1..b7015d8a 100644 --- a/bauh/gems/flatpak/resources/locale/it +++ b/bauh/gems/flatpak/resources/locale/it @@ -48,6 +48,8 @@ flatpak.info.version=version flatpak.install.bad_install_level.body=Valore non valido per {field} nel file di configurazione {file} flatpak.install.install_level.body={} deve essere installato per tutti gli utenti del dispositivo ( sistema ) ? flatpak.install.install_level.title=Tipo di installazione +flatpak.install.ref_choose.title=Multiple references +flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=Ise non si desidera utilizzare le applicazioni Flatpak, deselezionare {} in {} flatpak.notification.no_remotes=Nessun set remoti Flatpak. Non sarà possibile cercare app Flatpak. flatpak.remotes.system_flathub.error=Non è stato possibile aggiungere Flathub come repository di sistema ( remote ) diff --git a/bauh/gems/flatpak/resources/locale/pt b/bauh/gems/flatpak/resources/locale/pt index 36b9dd68..8b0632b8 100644 --- a/bauh/gems/flatpak/resources/locale/pt +++ b/bauh/gems/flatpak/resources/locale/pt @@ -48,6 +48,8 @@ flatpak.info.version=versão flatpak.install.bad_install_level.body=Valor inválido para {field} no arquivo de configuração {file} flatpak.install.install_level.body={} deve ser instalado para todos os usuários desse dispositivo ( sistema ) ? flatpak.install.install_level.title=Tipo de instalação +flatpak.install.ref_choose.title=Múltiplas referências +flatpak.install.ref_choose.body=Existem múltiplas referências para {}. Selecione uma para continuar: flatpak.notification.disable=Se não deseja usar aplicativos Flatpak, desmarque {} em {} flatpak.notification.no_remotes=Não há repositórios (remotes) Flatpak configurados. Não será possível buscar aplicativos Flatpak. flatpak.remotes.system_flathub.error=Não foi possível adicionar o Flathub como um repositório do sistema ( remote ) diff --git a/bauh/gems/flatpak/resources/locale/ru b/bauh/gems/flatpak/resources/locale/ru index 4005a082..850b0e45 100644 --- a/bauh/gems/flatpak/resources/locale/ru +++ b/bauh/gems/flatpak/resources/locale/ru @@ -48,6 +48,8 @@ flatpak.info.version=Версия flatpak.install.bad_install_level.body=Недопустимое значение для {field} в файле конфигурации {file} flatpak.install.install_level.body=Должен ли {} быть установлен для всех пользователей устройства (системы)? flatpak.install.install_level.title=Тип установки +flatpak.install.ref_choose.title=Multiple references +flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=Если вы не хотите использовать приложения Flatpak, снимите флажок {} в {} flatpak.notification.no_remotes=Поддержка Flatpak не установлена. Поиск приложений Flatpak будет невозможен. flatpak.remotes.system_flathub.error=Не удалось добавить Flathub в качестве системного репозитория (удаленного) diff --git a/bauh/gems/flatpak/resources/locale/tr b/bauh/gems/flatpak/resources/locale/tr index 8d1b187c..cc5aabaf 100644 --- a/bauh/gems/flatpak/resources/locale/tr +++ b/bauh/gems/flatpak/resources/locale/tr @@ -48,6 +48,8 @@ flatpak.info.version=sürüm flatpak.install.bad_install_level.body={file} yapılandırma dosyasında {field} için geçersiz değer flatpak.install.install_level.body=Tüm cihaz kullanıcıları (sistem) için {} kurulmalı mı ? flatpak.install.install_level.title=Kurulum türü +flatpak.install.ref_choose.title=Multiple references +flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=If you do not want to use Flatpak applications, uncheck {} in {} flatpak.notification.no_remotes=Flatpak uygulamalarını kullanmak istemiyorsanız, {} içindeki {} işaretini kaldırın flatpak.remotes.system_flathub.error=Flathub sistem havuzu (uzak) olarak eklenemedi diff --git a/pictures/releases/0.9.7/flatpak_refs.png b/pictures/releases/0.9.7/flatpak_refs.png new file mode 100644 index 0000000000000000000000000000000000000000..299a50e47b97a4fa540c31d5e59d213cf88a08e4 GIT binary patch literal 26246 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

wWNii*N+OJ7@FUyp#pxj^rj(3rX;UhkS{tMI~5 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 Date: Wed, 12 Aug 2020 10:29:19 -0300 Subject: [PATCH 20/99] Updating CHANGELOG --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 004d4679..dee33a92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. - Flatpak - - Creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) + - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) - minor UI improvements @@ -32,14 +32,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - crashing the application icon is not on the extracted folder root path [#132](https://github.com/vinifmor/bauh/issues/132) - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) - - Downloading some AUR packages sources twice when multi-threaded download is enabled + - downloading some AUR packages sources twice when multi-threaded download is enabled - upgrade summary: - not displaying all packages that must be uninstalled - displaying "required size" for packages that must be uninstalled - some conflict resolution scenarios when upgrading several packages - Flatpak - downgrading crashing with version 1.8.X - - history: the top commit is returned as "(null)" + - history: the top commit is returned as "(null)" in version 1.8.X - installation fails when there are multiple references for a given package (e.g: openh264)

From dd8683063bb6fb91ad4915419f51e2bf5efed309 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 12 Aug 2020 11:46:55 -0300 Subject: [PATCH 21/99] [flatpak] improvement -> downgrade function refactored --- CHANGELOG.md | 1 + bauh/gems/flatpak/controller.py | 48 ++++++++++++--------------- bauh/gems/flatpak/flatpak.py | 14 ++++---- bauh/gems/flatpak/resources/locale/ca | 2 +- bauh/gems/flatpak/resources/locale/de | 2 +- bauh/gems/flatpak/resources/locale/en | 2 +- bauh/gems/flatpak/resources/locale/es | 2 +- bauh/gems/flatpak/resources/locale/it | 2 +- bauh/gems/flatpak/resources/locale/pt | 2 +- bauh/gems/flatpak/resources/locale/ru | 2 +- bauh/gems/flatpak/resources/locale/tr | 2 +- 11 files changed, 38 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dee33a92..bf13ddee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Flatpak - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) + - downgrade function refactored - minor UI improvements diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index d91b49d1..3f72e818 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -168,39 +168,27 @@ def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_app return SearchResult(models, None, len(models)) def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: ProcessWatcher) -> bool: - if not self._make_exports_dir(watcher): return False - handler = ProcessHandler(watcher) - pkg.commit = flatpak.get_commit(pkg.id, pkg.branch, pkg.installation) - watcher.change_progress(10) watcher.change_substatus(self.i18n['flatpak.downgrade.commits']) - commits = flatpak.get_app_commits(pkg.ref, pkg.origin, pkg.installation, handler) - if commits is None: - return False - - try: - commit_idx = commits.index(pkg.commit) - except ValueError: - if commits[0] == '(null)': - commit_idx = 0 - else: - return False + history = self.get_history(pkg) # downgrade is not possible if the app current commit in the first one: - if commit_idx == len(commits) - 1: - watcher.show_message(self.i18n['flatpak.downgrade.impossible.title'], self.i18n['flatpak.downgrade.impossible.body'], MessageType.WARNING) + if history.pkg_status_idx == len(history.history) - 1: + watcher.show_message(self.i18n['flatpak.downgrade.impossible.title'], + self.i18n['flatpak.downgrade.impossible.body'].format(bold(pkg.name)), + MessageType.ERROR) return False - commit = commits[commit_idx + 1] + commit = history.history[history.pkg_status_idx + 1]['commit'] watcher.change_substatus(self.i18n['flatpak.downgrade.reverting']) watcher.change_progress(50) - success = handler.handle(SystemProcess(subproc=flatpak.downgrade(pkg.ref, commit, pkg.installation, root_password), - success_phrases=['Changes complete.', 'Updates complete.'], - wrong_error_phrase='Warning')) + success = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.downgrade(pkg.ref, commit, pkg.installation, root_password), + success_phrases=['Changes complete.', 'Updates complete.'], + wrong_error_phrase='Warning')) watcher.change_progress(100) return success @@ -311,13 +299,19 @@ def get_history(self, pkg: FlatpakApplication) -> PackageHistory: commits = flatpak.get_app_commits_data(pkg.ref, pkg.origin, pkg.installation) status_idx = 0 - commit_found = False - for idx, data in enumerate(commits): - if data['commit'] == pkg.commit: - status_idx = idx - commit_found = True - break + + if pkg.commit is None and len(commits) > 1 and commits[0]['commit'] == '(null)': + del commits[0] + pkg.commit = commits[0] + commit_found = True + + if not commit_found: + for idx, data in enumerate(commits): + if data['commit'] == pkg.commit: + status_idx = idx + commit_found = True + break if not commit_found and pkg.commit and commits[0]['commit'] == '(null)': commits[0]['commit'] = pkg.commit diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index 640a2129..5779179e 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -2,7 +2,7 @@ import subprocess import traceback from datetime import datetime -from typing import List, Dict, Set, Iterable +from typing import List, Dict, Set, Iterable, Optional from bauh.api.exception import NoInternetException from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess, SimpleProcess, ProcessHandler @@ -10,6 +10,7 @@ from bauh.gems.flatpak import EXPORTS_PATH RE_SEVERAL_SPACES = re.compile(r'\s+') +RE_COMMIT = re.compile(r'(Latest commit|Commit)\s*:\s*(.+)') def get_app_info_fields(app_id: str, branch: str, installation: str, fields: List[str] = [], check_runtime: bool = False): @@ -76,12 +77,13 @@ def get_app_info(app_id: str, branch: str, installation: str): return '' -def get_commit(app_id: str, branch: str, installation: str) -> str: - info = new_subprocess(['flatpak', 'info', app_id, branch, '--{}'.format(installation)]) +def get_commit(app_id: str, branch: str, installation: str) -> Optional[str]: + info = run_cmd('flatpak info {} {} --{}'.format(app_id, branch, installation)) - for o in new_subprocess(['grep', 'Commit:', '--color=never'], stdin=info.stdout).stdout: - if o: - return o.decode().split(':')[1].strip() + if info: + commits = RE_COMMIT.findall(info) + if commits: + return commits[0][1].strip() def list_installed(version: str) -> List[dict]: diff --git a/bauh/gems/flatpak/resources/locale/ca b/bauh/gems/flatpak/resources/locale/ca index a20ee7bd..e7cc6745 100644 --- a/bauh/gems/flatpak/resources/locale/ca +++ b/bauh/gems/flatpak/resources/locale/ca @@ -5,7 +5,7 @@ flatpak.config.install_level.system.tip=Applications will be installed for all t flatpak.config.install_level.user=user flatpak.config.install_level.user.tip=Application will be installed only for the current user flatpak.downgrade.commits=S’estan llegint les revisions del paquet -flatpak.downgrade.impossible.body=No s’ha pogut revertir la versió: l’aplicació és a la seva primera versió +flatpak.downgrade.impossible.body=No s’ha pogut revertir la versió: {} és a la seva primera versió flatpak.downgrade.impossible.title=Error flatpak.downgrade.reverting=S’està revertint la versió flatpak.history.commit=revisió diff --git a/bauh/gems/flatpak/resources/locale/de b/bauh/gems/flatpak/resources/locale/de index dcc8bbc9..59bdf7f2 100644 --- a/bauh/gems/flatpak/resources/locale/de +++ b/bauh/gems/flatpak/resources/locale/de @@ -5,7 +5,7 @@ flatpak.config.install_level.system.tip=Applications will be installed for all t flatpak.config.install_level.user=user flatpak.config.install_level.user.tip=Application will be installed only for the current user flatpak.downgrade.commits=Paket-Commits lesen -flatpak.downgrade.impossible.body=Downgrade nicht möglich. Dies ist die erste Version der App +flatpak.downgrade.impossible.body=Downgrade nicht möglich. Dies ist die erste Version der {} flatpak.downgrade.impossible.title=Fehler flatpak.downgrade.reverting=Version zurücksetzen flatpak.history.commit=Commit diff --git a/bauh/gems/flatpak/resources/locale/en b/bauh/gems/flatpak/resources/locale/en index 253b3229..3e1c7a16 100644 --- a/bauh/gems/flatpak/resources/locale/en +++ b/bauh/gems/flatpak/resources/locale/en @@ -5,7 +5,7 @@ flatpak.config.install_level.system.tip=Applications will be installed for all t flatpak.config.install_level.user=user flatpak.config.install_level.user.tip=Application will be installed only for the current user flatpak.downgrade.commits=Reading package commits -flatpak.downgrade.impossible.body=Impossible to downgrade: the app is in its first version +flatpak.downgrade.impossible.body=Impossible to downgrade: {} is in its first version flatpak.downgrade.impossible.title=Error flatpak.downgrade.reverting=Reverting version flatpak.history.commit=commit diff --git a/bauh/gems/flatpak/resources/locale/es b/bauh/gems/flatpak/resources/locale/es index af99e2cc..e73cb12a 100644 --- a/bauh/gems/flatpak/resources/locale/es +++ b/bauh/gems/flatpak/resources/locale/es @@ -5,7 +5,7 @@ flatpak.config.install_level.system.tip=Se instalarán aplicaciones para todos l flatpak.config.install_level.user=usuario flatpak.config.install_level.user.tip=La aplicación se instalará solo para el usuario actual flatpak.downgrade.commits=Leyendo commits del paquete -flatpak.downgrade.impossible.body=Imposible revertir la versión: el aplicativo está en su primera versión +flatpak.downgrade.impossible.body=Imposible revertir la versión: {} está en su primera versión flatpak.downgrade.impossible.title=Error flatpak.downgrade.reverting=Revirtiendo la versión flatpak.history.commit=commit diff --git a/bauh/gems/flatpak/resources/locale/it b/bauh/gems/flatpak/resources/locale/it index b7015d8a..a735febc 100644 --- a/bauh/gems/flatpak/resources/locale/it +++ b/bauh/gems/flatpak/resources/locale/it @@ -5,7 +5,7 @@ flatpak.config.install_level.system.tip=Applications will be installed for all t flatpak.config.install_level.user=user flatpak.config.install_level.user.tip=Application will be installed only for the current user flatpak.downgrade.commits=lettura commits del pacchetto -flatpak.downgrade.impossible.body=Iimpossibile effettuare il downgrade: l'app è nella sua prima versione +flatpak.downgrade.impossible.body=Iimpossibile effettuare il downgrade: {} è nella sua prima versione flatpak.downgrade.impossible.title=Errore flatpak.downgrade.reverting=Ripristino versione flatpak.history.commit=commit diff --git a/bauh/gems/flatpak/resources/locale/pt b/bauh/gems/flatpak/resources/locale/pt index 8b0632b8..694afd97 100644 --- a/bauh/gems/flatpak/resources/locale/pt +++ b/bauh/gems/flatpak/resources/locale/pt @@ -5,7 +5,7 @@ flatpak.config.install_level.system.tip=Os aplicativos serão instalados para to flatpak.config.install_level.user=usuário flatpak.config.install_level.user.tip=Os aplicativos serão instalados somente para o usuário atual flatpak.downgrade.commits=Lendo os commits do pacote -flatpak.downgrade.impossible.body=Impossível reverter a versão: o aplicativo está na sua primeira versão +flatpak.downgrade.impossible.body=Impossível reverter a versão: {} está na sua primeira versão flatpak.downgrade.impossible.title=Erro flatpak.downgrade.reverting=Revertendo a versão flatpak.history.commit=commit diff --git a/bauh/gems/flatpak/resources/locale/ru b/bauh/gems/flatpak/resources/locale/ru index 850b0e45..80016c54 100644 --- a/bauh/gems/flatpak/resources/locale/ru +++ b/bauh/gems/flatpak/resources/locale/ru @@ -5,7 +5,7 @@ flatpak.config.install_level.system.tip=Приложения будут уста flatpak.config.install_level.user=Пользователь flatpak.config.install_level.user.tip=Приложение будет установлено только для текущего пользователя flatpak.downgrade.commits=Чтение коммитов пакета -flatpak.downgrade.impossible.body=Невозможно понизить версию: приложение находится в первой версии +flatpak.downgrade.impossible.body=Невозможно понизить версию: {} находится в первой версии flatpak.downgrade.impossible.title=Ошибка flatpak.downgrade.reverting=Возврат версии flatpak.history.commit=Коммит diff --git a/bauh/gems/flatpak/resources/locale/tr b/bauh/gems/flatpak/resources/locale/tr index cc5aabaf..1901e447 100644 --- a/bauh/gems/flatpak/resources/locale/tr +++ b/bauh/gems/flatpak/resources/locale/tr @@ -5,7 +5,7 @@ flatpak.config.install_level.system.tip=Uygulamalar tüm cihaz kullanıcıları flatpak.config.install_level.user=kullanıcı flatpak.config.install_level.user.tip=Uygulama yalnızca geçerli kullanıcı için yüklenecek flatpak.downgrade.commits=Paket notları okunuyor -flatpak.downgrade.impossible.body=Sürüm düşürme imkansız: uygulama zaten ilk sürümünde +flatpak.downgrade.impossible.body=Sürüm düşürme imkansız: {} zaten ilk sürümünde flatpak.downgrade.impossible.title=Hata flatpak.downgrade.reverting=Sürüm geri döndürülüyor flatpak.history.commit=işlem From a02741284266176ef6de2c50252859eec454f450 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 12 Aug 2020 11:56:42 -0300 Subject: [PATCH 22/99] [flatpak] fix -> crashing when an update size cannot be read --- CHANGELOG.md | 1 + bauh/gems/flatpak/flatpak.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf13ddee..3dafa14f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X + - crashing when an update size cannot be read [#130](https://github.com/vinifmor/bauh/issues/130) - installation fails when there are multiple references for a given package (e.g: openh264)

diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index 5779179e..de3acf2a 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -415,5 +415,10 @@ def map_update_download_size(app_ids: Iterable[str], installation: str, version: if related_id: size = p2.findall(line_split[6])[0].split('?') - res[related_id[0].strip()] = size_to_byte(float(size[0]), size[1]) + + if size and len(size) > 1: + try: + res[related_id[0].strip()] = size_to_byte(float(size[0]), size[1]) + except: + traceback.print_exc() return res From ef6909be3821415e90fbc702570495a0039b29a2 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 12 Aug 2020 12:18:32 -0300 Subject: [PATCH 23/99] Updating README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b644548..99da07ed 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,7 @@ refresh_mirrors_startup: false # if the package mirrors should be refreshed duri mirrors_sort_limit: 5 # defines the maximum number of mirrors that will be used for speed sorting. Use 0 for no limit or leave it blank to disable sorting. aur: true # allows to manage AUR packages repositories: true # allows to manage packages from the configured repositories -repositories_mthread_download: true # enable multi-threaded download for repository packages if aria2 is installed +repositories_mthread_download: false # enable multi-threaded download for repository packages if aria2/axel is installed automatch_providers: true # if a possible provider for a given package dependency exactly matches its name, it will be chosen instead of asking for the user to decide (false). ``` - Required dependencies: From 9c1ffbd3564b5a7e3fb63a5249c30debddc58ea9 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 16 Aug 2020 12:39:20 -0300 Subject: [PATCH 24/99] [arch] feature -> AUR: allowing to edit the PKGBUILD file of a package to be installed/upgraded/downgraded --- CHANGELOG.md | 18 ++ README.md | 6 +- bauh/api/abstract/model.py | 19 +- bauh/api/abstract/view.py | 21 +- bauh/commons/view_utils.py | 18 ++ bauh/gems/appimage/controller.py | 4 +- bauh/gems/arch/__init__.py | 1 + bauh/gems/arch/config.py | 3 +- bauh/gems/arch/controller.py | 203 ++++++++++++++++-- bauh/gems/arch/makepkg.py | 11 + bauh/gems/arch/mapper.py | 14 +- bauh/gems/arch/model.py | 37 +++- .../gems/arch/resources/img/mark_pkgbuild.svg | 120 +++++++++++ .../arch/resources/img/unmark_pkgbuild.svg | 129 +++++++++++ bauh/gems/arch/resources/locale/ca | 8 + bauh/gems/arch/resources/locale/de | 8 + bauh/gems/arch/resources/locale/en | 9 + bauh/gems/arch/resources/locale/es | 8 + bauh/gems/arch/resources/locale/it | 8 + bauh/gems/arch/resources/locale/pt | 9 + bauh/gems/arch/resources/locale/ru | 8 + bauh/gems/arch/resources/locale/tr | 8 + bauh/gems/snap/controller.py | 5 +- bauh/gems/snap/resources/locale/ca | 1 + bauh/gems/snap/resources/locale/de | 1 + bauh/gems/snap/resources/locale/en | 1 + bauh/gems/snap/resources/locale/es | 1 + bauh/gems/snap/resources/locale/it | 1 + bauh/gems/snap/resources/locale/pt | 1 + bauh/gems/snap/resources/locale/ru | 1 + bauh/gems/snap/resources/locale/tr | 1 + bauh/gems/web/controller.py | 2 +- bauh/view/core/controller.py | 4 +- bauh/view/core/settings.py | 104 ++++----- bauh/view/qt/apps_table.py | 13 +- bauh/view/qt/components.py | 52 +++-- bauh/view/qt/window.py | 4 +- bauh/view/resources/img/ignore_update.svg | 158 +++++++------- pictures/releases/0.9.7/aur_pkgbuild.png | Bin 0 -> 38812 bytes pictures/releases/0.9.7/mark_pkgbuild.png | Bin 0 -> 10951 bytes pictures/releases/0.9.7/unmark_pkgbuild.png | Bin 0 -> 7561 bytes 41 files changed, 813 insertions(+), 207 deletions(-) create mode 100644 bauh/commons/view_utils.py create mode 100644 bauh/gems/arch/resources/img/mark_pkgbuild.svg create mode 100644 bauh/gems/arch/resources/img/unmark_pkgbuild.svg create mode 100644 pictures/releases/0.9.7/aur_pkgbuild.png create mode 100644 pictures/releases/0.9.7/mark_pkgbuild.png create mode 100644 pictures/releases/0.9.7/unmark_pkgbuild.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dafa14f..e02a7045 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ 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.7] 2020 +### Features +- Arch + - AUR: + - allowing to edit the PKGBUILD file of a package to be installed/upgraded/downgraded. If enabled, a popup will be displayed during this acctions allowing the PKGBUILD to be edited. +

+ +

+ - mark a given PKGBUILD of a package as editable (if the property above is enabled, the same behavior will be applied) +

+ +

+ - unmark a given PKGBUILD of a package as editable (it prevents the behavior described above to happen) +

+ +

+ ### Improvements - AppImage - Manual file installation/upgrade: @@ -20,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues - only removing packages after downloading the required ones - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. + - AUR: caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) - Flatpak - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) @@ -38,6 +55,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - not displaying all packages that must be uninstalled - displaying "required size" for packages that must be uninstalled - some conflict resolution scenarios when upgrading several packages + - AUR: info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X diff --git a/README.md b/README.md index 99da07ed..7f9aa11b 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,9 @@ db_updater: - **refresh mirrors**: allows the user to define multiple mirrors locations and sort by the fastest (`sudo pacman-mirrors -c country1,country2 && sudo pacman-mirrors --fasttrack 5 && sudo pacman -Syy`) - **quick system upgrade**: it executes a default pacman upgrade (`pacman -Syyu --noconfirm`) - **clean cache**: it cleans the pacman cache diretory (default: `/var/cache/pacman/pkg`) + - **mark PKGBUILD as editable**: it marks a given PKGBUILD of a package as editable (a popup with the PKGBUILD will be displayed before upgrading/downgrading this package). Action only available when the configuration property **edit_aur_pkgbuild** is not **false**. + - **unmark PKGBUILD as editable**: reverts the action described above. Action only available when the configuration property **edit_aur_pkgbuild** is not **false**. +- Installed AUR packages have their **PKGBUILD** files cached at **~/.cache/bauh/arch/installed/$pkgname** - Packages with ignored updates are defined at **~/.config/bauh/arch/updates_ignored.txt** - The configuration file is located at **~/.config/bauh/arch.yml** and it allows the following customizations: ``` @@ -190,7 +193,8 @@ aur: true # allows to manage AUR packages repositories: true # allows to manage packages from the configured repositories repositories_mthread_download: false # enable multi-threaded download for repository packages if aria2/axel is installed automatch_providers: true # if a possible provider for a given package dependency exactly matches its name, it will be chosen instead of asking for the user to decide (false). -``` +edit_aur_pkgbuild: false # if the AUR PKGBUILD file should be displayed for edition before the make process. true (PKGBUILD will always be displayed for edition), false (PKGBUILD never will be displayed), null (a popup will ask if the user want to edit the PKGBUILD) +``` - Required dependencies: - **pacman** - **wget** diff --git a/bauh/api/abstract/model.py b/bauh/api/abstract/model.py index 66f56d9b..3cb78981 100644 --- a/bauh/api/abstract/model.py +++ b/bauh/api/abstract/model.py @@ -7,9 +7,12 @@ class CustomSoftwareAction: - def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str, requires_root: bool, manager: "SoftwareManager" = None, backup: bool = False, refresh: bool = True): + def __init__(self, i18n_label_key: str, i18n_status_key: str, icon_path: str, manager_method: str, + requires_root: bool, manager: "SoftwareManager" = None, + backup: bool = False, refresh: bool = True, + i18n_confirm_key: str = None): """ - :param i18_label_key: the i18n key that will be used to display the action name + :param i18n_label_key: the i18n key that will be used to display the action name :param i18n_status_key: the i18n key that will be used to display the action name being executed :param icon_path: the action icon path. Use None for no icon :param manager_method: the SoftwareManager method name that should be called. The method must has the following parameters: (pkg: SoftwarePackage, root_password: str, watcher: ProcessWatcher) @@ -17,8 +20,9 @@ def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, man :param backup: if a system backup should be performed before executing the action :param requires_root: :param refresh: if the a full app refresh should be done if the action succeeds + :param i18n_confirm_key: action confirmation message """ - self.i18_label_key = i18_label_key + self.i18n_label_key = i18n_label_key self.i18n_status_key = i18n_status_key self.icon_path = icon_path self.manager_method = manager_method @@ -26,12 +30,13 @@ def __init__(self, i18_label_key: str, i18n_status_key: str, icon_path: str, man self.manager = manager self.backup = backup self.refresh = refresh + self.i18n_confirm_key = i18n_confirm_key def __hash__(self): - return self.i18_label_key.__hash__() + self.i18n_status_key.__hash__() + self.manager_method.__hash__() + return self.i18n_label_key.__hash__() + self.i18n_status_key.__hash__() + self.manager_method.__hash__() def __repr__(self): - return "CustomAction (label={}, method={})".format(self.i18_label_key, self.manager_method) + return "CustomAction (label={}, method={})".format(self.i18n_label_key, self.manager_method) class PackageStatus(Enum): @@ -252,6 +257,10 @@ def __init__(self, pkg: SoftwarePackage, history: List[dict], pkg_status_idx: in self.history = history self.pkg_status_idx = pkg_status_idx + @classmethod + def empyt(cls, pkg: SoftwarePackage): + return cls(pkg=pkg, history=[], pkg_status_idx=-1) + class SuggestionPriority(Enum): LOW = 0 diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index bd0adf52..fe39b081 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -148,10 +148,15 @@ def __init__(self, label: str, tooltip: str = None, state: bool = False, id_: s self.state = state +class TextInputType(Enum): + SINGLE_LINE = 0 + MULTIPLE_LINES = 1 + + class TextInputComponent(ViewComponent): def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip: str = None, read_only: bool =False, - id_: str = None, only_int: bool = False, max_width: int = -1): + id_: str = None, only_int: bool = False, max_width: int = -1, type_: TextInputType = TextInputType.SINGLE_LINE): super(TextInputComponent, self).__init__(id_=id_) self.label = label self.value = value @@ -160,20 +165,22 @@ def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip self.read_only = read_only self.only_int = only_int self.max_width = max_width + self.type = type_ def get_value(self) -> str: if self.value is not None: - return self.value.strip() + return self.value else: return '' def set_value(self, val: Optional[str], caller: object = None): - self.value = val + if val != self.value: + self.value = val - if self.observers: - for o in self.observers: - if caller != o: - o.on_change(val) + if self.observers: + for o in self.observers: + if caller != o: + o.on_change(val) def get_int_value(self) -> int: if self.value is not None: diff --git a/bauh/commons/view_utils.py b/bauh/commons/view_utils.py new file mode 100644 index 00000000..11611bd0 --- /dev/null +++ b/bauh/commons/view_utils.py @@ -0,0 +1,18 @@ +from typing import List, Tuple, Optional + +from bauh.api.abstract.view import SelectViewType, InputOption, SingleSelectComponent + + +def new_select(label: str, tip: str, id_: str, opts: List[Tuple[Optional[str], object, Optional[str]]], value: object, max_width: int, + type_: SelectViewType = SelectViewType.RADIO, capitalize_label: bool = True): + inp_opts = [InputOption(label=o[0].capitalize(), value=o[1], tooltip=o[2]) for o in opts] + def_opt = [o for o in inp_opts if o.value == value] + return SingleSelectComponent(label=label, + tooltip=tip, + options=inp_opts, + default_option=def_opt[0] if def_opt else inp_opts[0], + max_per_line=len(inp_opts), + max_width=max_width, + type_=type_, + id_=id_, + capitalize_label=capitalize_label) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 087af469..36738aea 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -82,13 +82,13 @@ def __init__(self, context: ApplicationContext): self.logger = context.logger self.file_downloader = context.file_downloader self.db_locks = {DB_APPS_PATH: Lock(), DB_RELEASES_PATH: Lock()} - self.custom_actions = [CustomSoftwareAction(i18_label_key='appimage.custom_action.install_file', + self.custom_actions = [CustomSoftwareAction(i18n_label_key='appimage.custom_action.install_file', i18n_status_key='appimage.custom_action.install_file.status', manager=self, manager_method='install_file', icon_path=resource.get_path('img/appimage.svg', ROOT_DIR), requires_root=False)] - self.custom_app_actions = [CustomSoftwareAction(i18_label_key='appimage.custom_action.manual_update', + self.custom_app_actions = [CustomSoftwareAction(i18n_label_key='appimage.custom_action.manual_update', i18n_status_key='appimage.custom_action.manual_update.status', manager_method='update_file', requires_root=False, diff --git a/bauh/gems/arch/__init__.py b/bauh/gems/arch/__init__.py index 274fa271..6a1cb5c2 100644 --- a/bauh/gems/arch/__init__.py +++ b/bauh/gems/arch/__init__.py @@ -16,6 +16,7 @@ CONFIG_FILE = '{}/arch.yml'.format(CONFIG_PATH) SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/arch/aur_suggestions.txt' UPDATES_IGNORED_FILE = '{}/updates_ignored.txt'.format(CONFIG_DIR) +EDITABLE_PKGBUILDS_FILE = '{}/aur/editable_pkgbuilds.txt'.format(CONFIG_DIR) def get_icon_path() -> str: diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index abdb0d27..f2d6b5a1 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -12,5 +12,6 @@ def read_config(update_file: bool = False) -> dict: "sync_databases_startup": True, 'mirrors_sort_limit': 5, 'repositories_mthread_download': False, - 'automatch_providers': True} + 'automatch_providers': True, + 'edit_aur_pkgbuild': False} return read(CONFIG_FILE, template, update_file=update_file) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 93453c41..06a3fe98 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -22,17 +22,18 @@ from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus, \ SuggestionPriority, CustomSoftwareAction from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ - ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextComponent + ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextComponent, TextInputType from bauh.api.constants import TEMP_DIR from bauh.commons import user, internet 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.view_utils import new_select from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, message, confirmation, disk, git, \ gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ CONFIG_FILE, get_icon_path, database, mirrors, sorting, cpu_manager, ARCH_CACHE_PATH, UPDATES_IGNORED_FILE, \ - CONFIG_DIR + CONFIG_DIR, EDITABLE_PKGBUILDS_FILE from bauh.gems.arch.aur import AURClient from bauh.gems.arch.config import read_config from bauh.gems.arch.dependencies import DependenciesAnalyser @@ -65,7 +66,8 @@ def __init__(self, name: str = None, base: str = None, maintainer: str = None, w remote_repo_map: Dict[str, str] = None, provided_map: Dict[str, Set[str]] = None, remote_provided_map: Dict[str, Set[str]] = None, aur_idx: Set[str] = None, 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): + disk_loader: DiskCacheLoader = None, disk_cache_updater: Thread = None, + new_pkg: bool = False): self.name = name self.base = base self.maintainer = maintainer @@ -90,13 +92,15 @@ def __init__(self, name: str = None, base: str = None, maintainer: str = None, w self.removed = removed self.disk_loader = disk_loader self.disk_cache_updater = disk_cache_updater + self.pkgbuild_edited = False + self.new_pkg = new_pkg @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={}) + installed=set(), removed={}, new_pkg=not pkg.installed) def get_base_name(self): return self.base if self.base else self.name @@ -173,26 +177,26 @@ def __init__(self, context: ApplicationContext, disk_cache_updater: ArchDiskCach self.deps_analyser = DependenciesAnalyser(self.aur_client, self.i18n) self.http_client = context.http_client self.custom_actions = { - 'sys_up': CustomSoftwareAction(i18_label_key='arch.custom_action.upgrade_system', + 'sys_up': CustomSoftwareAction(i18n_label_key='arch.custom_action.upgrade_system', i18n_status_key='arch.custom_action.upgrade_system.status', manager_method='upgrade_system', icon_path=get_icon_path(), requires_root=True, backup=True, manager=self), - 'ref_dbs': CustomSoftwareAction(i18_label_key='arch.custom_action.refresh_dbs', + 'ref_dbs': CustomSoftwareAction(i18n_label_key='arch.custom_action.refresh_dbs', i18n_status_key='arch.sync_databases.substatus', manager_method='sync_databases', icon_path=get_icon_path(), requires_root=True, manager=self), - 'ref_mirrors': CustomSoftwareAction(i18_label_key='arch.custom_action.refresh_mirrors', + 'ref_mirrors': CustomSoftwareAction(i18n_label_key='arch.custom_action.refresh_mirrors', i18n_status_key='arch.task.mirrors', manager_method='refresh_mirrors', icon_path=get_icon_path(), requires_root=True, manager=self), - 'clean_cache': CustomSoftwareAction(i18_label_key='arch.custom_action.clean_cache', + 'clean_cache': CustomSoftwareAction(i18n_label_key='arch.custom_action.clean_cache', i18n_status_key='arch.custom_action.clean_cache.status', manager_method='clean_cache', icon_path=get_icon_path(), @@ -416,7 +420,8 @@ def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_u res.total = len(res.installed) + len(res.new) return res - def _fill_aur_pkgs(self, aur_pkgs: dict, output: list, disk_loader: DiskCacheLoader, internet_available: bool): + 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() if internet_available: @@ -424,9 +429,11 @@ def _fill_aur_pkgs(self, aur_pkgs: dict, output: list, disk_loader: DiskCacheLoa pkgsinfo = self.aur_client.get_info(aur_pkgs.keys()) 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.downgrade_enabled = downgrade_enabled + pkg.pkgbuild_editable = pkg.name in editable_pkgbuilds if editable_pkgbuilds is not None else None if disk_loader: disk_loader.fill(pkg) @@ -434,11 +441,13 @@ def _fill_aur_pkgs(self, aur_pkgs: dict, output: list, disk_loader: DiskCacheLoa output.append(pkg) - return + return + except requests.exceptions.ConnectionError: self.logger.warning('Could not retrieve installed AUR packages API data. It seems the internet connection is off.') self.logger.info("Reading only local AUR packages data") + editable_pkgbuilds = self._read_editable_pkgbuilds() if arch_config['edit_aur_pkgbuild'] is not False else None for name, data in aur_pkgs.items(): pkg = ArchPackage(name=name, version=data.get('version'), latest_version=data.get('version'), description=data.get('description'), @@ -446,6 +455,7 @@ def _fill_aur_pkgs(self, aur_pkgs: dict, output: list, disk_loader: DiskCacheLoa pkg.categories = self.categories.get(pkg.name) pkg.downgrade_enabled = downgrade_enabled + pkg.pkgbuild_editable = pkg.name in editable_pkgbuilds if editable_pkgbuilds is not None else None if disk_loader: disk_loader.fill(pkg) @@ -538,7 +548,7 @@ def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_app map_threads = [] if aur_pkgs: - t = Thread(target=self._fill_aur_pkgs, args=(aur_pkgs, pkgs, disk_loader, internet_available), daemon=True) + t = Thread(target=self._fill_aur_pkgs, args=(aur_pkgs, pkgs, disk_loader, internet_available, arch_config), daemon=True) t.start() map_threads.append(t) @@ -1207,6 +1217,8 @@ def _uninstall(self, context: TransactionContext, remove_unneeded: bool = False, self._revert_ignored_updates(to_uninstall) + self._remove_from_editable_pkgbuilds(context.name) + self._update_progress(context, 100) return uninstalled @@ -1314,8 +1326,12 @@ def _get_history_aur_pkg(self, pkg: ArchPackage) -> PackageHistory: run_cmd('git clone ' + URL_GIT.format(base_name), print_error=False, cwd=temp_dir) clone_path = '{}/{}'.format(temp_dir, base_name) + srcinfo_path = '{}/.SRCINFO'.format(clone_path) + if not os.path.exists(srcinfo_path): + return PackageHistory.empyt(pkg) + commits = git.list_commits(clone_path) if commits: @@ -1572,7 +1588,62 @@ def _pre_download_source(self, project_dir: str, watcher: ProcessWatcher) -> boo return True + def _display_pkgbuild_for_editing(self, pkgname: str, watcher: ProcessWatcher, pkgbuild_path: str) -> bool: + with open(pkgbuild_path) as f: + pkgbuild = f.read() + + pkgbuild_input = TextInputComponent(label='', value=pkgbuild, type_=TextInputType.MULTIPLE_LINES) + + watcher.request_confirmation(title='PKGBUILD ({})'.format(pkgname), + body='', + components=[pkgbuild_input], + confirmation_label=self.i18n['proceed'].capitalize(), + deny_button=False) + + if pkgbuild_input.get_value() != pkgbuild: + with open(pkgbuild_path, 'w+') as f: + f.write(pkgbuild_input.get_value()) + + return makepkg.update_srcinfo('/'.join(pkgbuild_path.split('/')[0:-1])) + + return False + + def _ask_for_pkgbuild_edition(self, pkgname: str, arch_config: dict, watcher: ProcessWatcher, pkgbuild_path: str) -> bool: + if pkgbuild_path: + if arch_config['edit_aur_pkgbuild'] is None: + if watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(), + body=self.i18n['arch.aur.action.edit_pkgbuild.body'].format( + bold(pkgname))): + return self._display_pkgbuild_for_editing(pkgname, watcher, pkgbuild_path) + elif arch_config['edit_aur_pkgbuild']: + return self._display_pkgbuild_for_editing(pkgname, watcher, pkgbuild_path) + + return False + + def _edit_pkgbuild_and_update_context(self, context: TransactionContext): + if context.new_pkg or context.name in self._read_editable_pkgbuilds(): + if self._ask_for_pkgbuild_edition(pkgname=context.name, + arch_config=context.config, + watcher=context.watcher, + pkgbuild_path='{}/PKGBUILD'.format(context.project_dir)): + context.pkgbuild_edited = True + srcinfo = aur.map_srcinfo(makepkg.gen_srcinfo(context.project_dir)) + + if srcinfo: + context.name = srcinfo['pkgname'] + context.base = srcinfo['pkgbase'] + + if context.pkg: + for pkgattr, srcattr in {'name': 'pkgname', + 'package_base': 'pkgbase', + 'version': 'pkgversion', + 'latest_version': 'pkgversion', + 'license': 'license', + 'description': 'pkgdesc'}.items(): + setattr(context.pkg, pkgattr, srcinfo.get(srcattr, getattr(context.pkg, pkgattr))) + def _build(self, context: TransactionContext) -> bool: + self._edit_pkgbuild_and_update_context(context) self._pre_download_source(context.project_dir, context.watcher) self._update_progress(context, 50) @@ -1608,6 +1679,7 @@ def _build(self, context: TransactionContext) -> bool: context.install_file = '{}/{}'.format(context.project_dir, gen_file[0]) if self._install(context=context): + self._save_pkgbuild(context) if context.dependency or context.skip_opt_deps: return True @@ -1620,6 +1692,24 @@ def _build(self, context: TransactionContext) -> bool: return False + def _save_pkgbuild(self, context: TransactionContext): + cache_path = ArchPackage.disk_cache_path(context.name) + if not os.path.exists(cache_path): + try: + os.mkdir(cache_path) + except: + print("Could not create cache directory '{}'".format(cache_path)) + traceback.print_exc() + return + + src_pkgbuild = '{}/PKGBUILD'.format(context.project_dir) + dest_pkgbuild = '{}/PKGBUILD'.format(cache_path) + try: + shutil.copy(src_pkgbuild, dest_pkgbuild) + except: + context.watcher.print("Could not copy '{}' to '{}'".format(src_pkgbuild, dest_pkgbuild)) + traceback.print_exc() + def _ask_and_install_missing_deps(self, context: TransactionContext, missing_deps: List[Tuple[str, str]]) -> bool: context.watcher.change_substatus(self.i18n['arch.missing_deps_found'].format(bold(context.name))) @@ -2086,12 +2176,21 @@ def install(self, pkg: ArchPackage, root_password: str, disk_loader: DiskCacheLo else: res = self._install_from_repository(install_context) - if res and os.path.exists(pkg.get_disk_data_path()): - with open(pkg.get_disk_data_path()) as f: - data = f.read() - if data: - data = json.loads(data) - pkg.fill_cached_data(data) + if res: + pkg.name = install_context.name # changes the package name in case the PKGBUILD was edited + + if os.path.exists(pkg.get_disk_data_path()): + with open(pkg.get_disk_data_path()) as f: + data = f.read() + if data: + data = json.loads(data) + pkg.fill_cached_data(data) + + if install_context.new_pkg and install_context.config['edit_aur_pkgbuild'] is not False and pkg.repository == 'aur': + if install_context.pkgbuild_edited: + pkg.pkgbuild_editable = self._add_as_editable_pkgbuild(pkg.name) + else: + pkg.pkgbuild_editable = not self._remove_from_editable_pkgbuilds(pkg.name) installed = [] @@ -2311,7 +2410,7 @@ def _gen_bool_selector(self, id_: str, label_key: str, tooltip_key: str, value: def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: local_config = read_config() - max_width = floor(screen_width * 0.15) + max_width = floor(screen_width * 0.22) db_sync_start = self._gen_bool_selector(id_='sync_dbs_start', label_key='arch.config.sync_dbs', @@ -2370,7 +2469,19 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: tooltip=self.i18n['arch.config.mirrors_sort_limit.tip'], only_int=True, max_width=max_width, - value=local_config['mirrors_sort_limit'] if isinstance(local_config['mirrors_sort_limit'], int) else '') + value=local_config['mirrors_sort_limit'] if isinstance(local_config['mirrors_sort_limit'], int) else ''), + new_select(label=self.i18n['arch.config.edit_aur_pkgbuild'], + tip=self.i18n['arch.config.edit_aur_pkgbuild.tip'], + id_='edit_aur_pkgbuild', + opts=[(self.i18n['yes'].capitalize(), True, None), + (self.i18n['no'].capitalize(), False, None), + (self.i18n['ask'].capitalize(), None, None), + ], + value=local_config['edit_aur_pkgbuild'], + max_width=max_width, + type_=SelectViewType.RADIO, + capitalize_label=False) + ] return PanelComponent([FormComponent(fields, spaces=False)]) @@ -2389,6 +2500,7 @@ def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: config['mirrors_sort_limit'] = form_install.get_component('mirrors_sort_limit').get_int_value() config['repositories_mthread_download'] = form_install.get_component('mthread_download').get_selected() config['automatch_providers'] = form_install.get_component('autoprovs').get_selected() + config['edit_aur_pkgbuild'] = form_install.get_component('edit_aur_pkgbuild').get_selected() try: save_config(config, CONFIG_FILE) @@ -2614,3 +2726,56 @@ def _revert_ignored_updates(self, pkgs: Iterable[str]): def revert_ignored_update(self, pkg: ArchPackage): self._revert_ignored_updates({pkg.name}) pkg.update_ignored = False + + def _add_as_editable_pkgbuild(self, pkgname: str): + try: + Path('/'.join(EDITABLE_PKGBUILDS_FILE.split('/')[0:-1])).mkdir(parents=True, exist_ok=True) + + editable = self._read_editable_pkgbuilds() + + if pkgname not in editable: + editable.add(pkgname) + + self._write_editable_pkgbuilds(editable) + return True + except: + traceback.print_exc() + return False + + def _write_editable_pkgbuilds(self, editable: Set[str]): + if editable: + with open(EDITABLE_PKGBUILDS_FILE, 'w+') as f: + for name in sorted([*editable]): + f.write('{}\n'.format(name)) + else: + os.remove(EDITABLE_PKGBUILDS_FILE) + + def _remove_from_editable_pkgbuilds(self, pkgname: str): + if os.path.exists(EDITABLE_PKGBUILDS_FILE): + try: + editable = self._read_editable_pkgbuilds() + + if pkgname in editable: + editable.remove(pkgname) + + self._write_editable_pkgbuilds(editable) + except: + traceback.print_exc() + return False + + return True + + def _read_editable_pkgbuilds(self) -> Set[str]: + if os.path.exists(EDITABLE_PKGBUILDS_FILE): + with open(EDITABLE_PKGBUILDS_FILE) as f: + return {l.strip() for l in f.readlines() if l and l.strip()} + + return set() + + def enable_pkgbuild_edition(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher): + if self._add_as_editable_pkgbuild(pkg.name): + pkg.pkgbuild_editable = True + + def disable_pkgbuild_edition(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher): + if self._remove_from_editable_pkgbuilds(pkg.name): + pkg.pkgbuild_editable = False diff --git a/bauh/gems/arch/makepkg.py b/bauh/gems/arch/makepkg.py index 1b9ee6d6..f76d96cb 100644 --- a/bauh/gems/arch/makepkg.py +++ b/bauh/gems/arch/makepkg.py @@ -55,3 +55,14 @@ def make(pkgdir: str, optimize: bool, handler: ProcessHandler) -> Tuple[bool, st handler.watcher.print('Custom optimized makepkg.conf ( {} ) not found'.format(CUSTOM_MAKEPKG_FILE)) return handler.handle_simple(SimpleProcess(cmd, cwd=pkgdir)) + + +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 diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index f489789a..e4311a2b 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -1,3 +1,4 @@ +import os import re from datetime import datetime @@ -110,10 +111,15 @@ def check_update(version: str, latest_version: str, check_suffix: bool = False) return False def fill_package_build(self, pkg: ArchPackage): - res = self.http_client.get(pkg.get_pkg_build_url()) - - if res and res.status_code == 200 and res.text: - pkg.pkgbuild = res.text + cached_pkgbuild = pkg.get_cached_pkgbuild_path() + if os.path.exists(cached_pkgbuild): + with open(cached_pkgbuild) as f: + pkg.pkgbuild = f.read() + else: + res = self.http_client.get(pkg.get_pkg_build_url()) + + 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 diff --git a/bauh/gems/arch/model.py b/bauh/gems/arch/model.py index fc3d452e..d72c256d 100644 --- a/bauh/gems/arch/model.py +++ b/bauh/gems/arch/model.py @@ -1,13 +1,27 @@ import datetime from typing import List, Set -from bauh.api.abstract.model import SoftwarePackage +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'} +ACTIONS_AUR_ENABLE_PKGBUILD_EDITION = [CustomSoftwareAction(i18n_label_key='arch.action.enable_pkgbuild_edition', + i18n_status_key='arch.action.enable_pkgbuild_edition.status', + i18n_confirm_key='arch.action.enable_pkgbuild_edition.confirm', + requires_root=False, + manager_method='enable_pkgbuild_edition', + icon_path=resource.get_path('img/mark_pkgbuild.svg', ROOT_DIR))] + +ACTIONS_AUR_DISABLE_PKGBUILD_EDITION = [CustomSoftwareAction(i18n_label_key='arch.action.disable_pkgbuild_edition', + i18n_status_key='arch.action.disable_pkgbuild_edition', + i18n_confirm_key='arch.action.disable_pkgbuild_edition.confirm', + requires_root=False, + manager_method='disable_pkgbuild_edition', + icon_path=resource.get_path('img/unmark_pkgbuild.svg', ROOT_DIR))] + class ArchPackage(SoftwarePackage): @@ -16,7 +30,8 @@ def __init__(self, name: str = None, version: str = None, latest_version: str = first_submitted: datetime.datetime = None, last_modified: datetime.datetime = 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): + categories: List[str] = None, i18n: I18n = None, update_ignored: bool = False, arch: str = None, + pkgbuild_editable: bool = None): super(ArchPackage, self).__init__(name=name, version=version, latest_version=latest_version, description=description, installed=installed, categories=categories) @@ -38,6 +53,8 @@ def __init__(self, name: str = None, version: str = None, latest_version: str = self.arch = arch self.i18n = i18n 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) @staticmethod def disk_cache_path(pkgname: str): @@ -48,7 +65,7 @@ def get_pkg_build_url(self): return 'https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=' + self.package_base def has_history(self): - return self.installed and self.repository == 'aur' + return self.can_be_downgraded() def has_info(self): return True @@ -148,4 +165,18 @@ def __repr__(self): def __eq__(self, other): if isinstance(other, ArchPackage): + if self.view_name is not None and other.view_name is not None: + return self.view_name == other.view_name and self.repository == other.repository + return self.name == other.name and self.repository == other.repository + + def get_cached_pkgbuild_path(self) -> str: + return '{}/PKGBUILD'.format(self.get_disk_cache_path()) + + def get_custom_supported_actions(self) -> List[CustomSoftwareAction]: + if self.installed and self.pkgbuild_editable is not None and self.repository == 'aur': + if self.pkgbuild_editable: + return ACTIONS_AUR_DISABLE_PKGBUILD_EDITION + else: + return ACTIONS_AUR_ENABLE_PKGBUILD_EDITION + diff --git a/bauh/gems/arch/resources/img/mark_pkgbuild.svg b/bauh/gems/arch/resources/img/mark_pkgbuild.svg new file mode 100644 index 00000000..d9ab0b45 --- /dev/null +++ b/bauh/gems/arch/resources/img/mark_pkgbuild.svg @@ -0,0 +1,120 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bauh/gems/arch/resources/img/unmark_pkgbuild.svg b/bauh/gems/arch/resources/img/unmark_pkgbuild.svg new file mode 100644 index 00000000..43ced941 --- /dev/null +++ b/bauh/gems/arch/resources/img/unmark_pkgbuild.svg @@ -0,0 +1,129 @@ + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 12a4dd5d..a8b0ae4d 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -3,6 +3,12 @@ arch.action.db_locked.body.l2=It is necessary to unlock it to continue. arch.action.db_locked.confirmation=Unlock and continue arch.action.db_locked.error=It was not possible to unlock the database. arch.action.db_locked.title=Database locked +arch.action.disable_pkgbuild_edition=Unmark PKGBUILD as editable +arch.action.disable_pkgbuild_edition.confirm=Unmark PKGBUILD of {} as editable ? +arch.action.disable_pkgbuild_edition.status=Unmarking PKGBUILD as editable +arch.action.enable_pkgbuild_edition=Mark PKGBUILD as editable +arch.action.enable_pkgbuild_edition.confirm=Mark PKGBUILD of {} as editable ? +arch.action.enable_pkgbuild_edition.status=Marking PKGBUILD as editable arch.aur.install.pgp.body=Per a instal·lar {} cal rebre les claus PGP següents arch.aur.install.pgp.receive_fail=Could not receive PGP key {} arch.aur.install.pgp.sign_fail=No s’ha pogut rebre la clau PGP {} @@ -26,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.clean_cache=Elimina les versions antigues arch.config.clean_cache.tip=Si cal eliminar les versions antigues d'un paquet emmagatzemat al disc durant la desinstal·lació +arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) +arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Mirrors sort limit arch.config.mirrors_sort_limit.tip=Defines the maximum number of mirrors that will be used for speed sorting. Use 0 for no limit or leave it blank to disable sorting. arch.config.optimize=optimize diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 053c80d3..add22c05 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -3,6 +3,12 @@ arch.action.db_locked.body.l2=It is necessary to unlock it to continue. arch.action.db_locked.confirmation=Unlock and continue arch.action.db_locked.error=It was not possible to unlock the database. arch.action.db_locked.title=Database locked +arch.action.disable_pkgbuild_edition=Unmark PKGBUILD as editable +arch.action.disable_pkgbuild_edition.confirm=Unmark PKGBUILD of {} as editable ? +arch.action.disable_pkgbuild_edition.status=Unmarking PKGBUILD as editable +arch.action.enable_pkgbuild_edition=Mark PKGBUILD as editable +arch.action.enable_pkgbuild_edition.confirm=Mark PKGBUILD of {} as editable ? +arch.action.enable_pkgbuild_edition.status=Marking PKGBUILD as editable arch.aur.install.pgp.body=Um {} zu installieren sind folgende PGP Schlüssel nötig arch.aur.install.pgp.receive_fail=PGP Schlüssel {} konnte nicht empfangen werden arch.aur.install.pgp.sign_fail=PGP Schlüssel {} konnte nicht signiert werden @@ -26,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall +arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) +arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Mirrors sort limit arch.config.mirrors_sort_limit.tip=Defines the maximum number of mirrors that will be used for speed sorting. Use 0 for no limit or leave it blank to disable sorting. arch.config.optimize=optimize diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 197975e8..581655f1 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -3,6 +3,13 @@ arch.action.db_locked.body.l2=It is necessary to unlock it to continue. arch.action.db_locked.confirmation=Unlock and continue arch.action.db_locked.error=It was not possible to unlock the database. arch.action.db_locked.title=Database locked +arch.action.disable_pkgbuild_edition=Unmark PKGBUILD as editable +arch.action.disable_pkgbuild_edition.confirm=Unmark PKGBUILD of {} as editable ? +arch.action.disable_pkgbuild_edition.status=Unmarking PKGBUILD as editable +arch.action.enable_pkgbuild_edition=Mark PKGBUILD as editable +arch.action.enable_pkgbuild_edition.confirm=Mark PKGBUILD of {} as editable ? +arch.action.enable_pkgbuild_edition.status=Marking PKGBUILD as editable +arch.aur.action.edit_pkgbuild.body=Edit the PKGBUILD file of {} before continuing ? arch.aur.install.pgp.body=To install {} is necessary to receive the following PGP keys arch.aur.install.pgp.receive_fail=Could not receive PGP key {} arch.aur.install.pgp.sign_fail=Could not sign PGP key {} @@ -26,6 +33,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall +arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) +arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Mirrors sort limit arch.config.mirrors_sort_limit.tip=Defines the maximum number of mirrors that will be used for speed sorting. Use 0 for no limit or leave it blank to disable sorting. arch.config.optimize=optimize diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index b69b0b21..01ffa1fe 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -3,6 +3,12 @@ arch.action.db_locked.body.l2=Es necesario desbloquearla para continuar. arch.action.db_locked.confirmation=Desbloquear y continuar arch.action.db_locked.error=No fue posible desbloquear la base de datos. arch.action.db_locked.title=Base de dados bloqueada +arch.action.disable_pkgbuild_edition=Desmarcar PKGBUILD como editable +arch.action.disable_pkgbuild_edition.confirm=Desmarcar PKGBUILD de {} como editable ? +arch.action.disable_pkgbuild_edition.status=Desmarcando PKGBUILD como editable +arch.action.enable_pkgbuild_edition=Marcar PKGBUILD como editable +arch.action.enable_pkgbuild_edition.confirm=Marcar PKGBUILD de {} como editable ? +arch.action.enable_pkgbuild_edition.status=Marcando PKGBUILD como editable arch.aur.install.pgp.body=Para instalar {} es necesario recibir las siguientes claves PGP arch.aur.install.pgp.receive_fail=Could not receive PGP key {} arch.aur.install.pgp.sign_fail=No fue posible recibir la clave PGP {} @@ -26,6 +32,8 @@ arch.config.automatch_providers=Autodefinir proveedores de dependencia arch.config.automatch_providers.tip=Elige automáticamente qué proveedor se usará para una dependencia de paquete cuando ambos nombres son iguales. arch.config.clean_cache=Eliminar versiones antiguas arch.config.clean_cache.tip=Si las versiones antiguas de un paquete almacenado en el disco deben ser eliminadas durante la desinstalación +arch.config.edit_aur_pkgbuild=Editar PKGBUILD (AUR) +arch.config.edit_aur_pkgbuild.tip=Si el archivo PKGBUILD de un paquete AUR debe ser exhibido para edición antes de su instalación/actualización/degradación arch.config.mirrors_sort_limit=Límite de ordenación de espejos arch.config.mirrors_sort_limit.tip=Define el número máximo de espejos que se utilizarán para la ordenación por velocidad. Use 0 para no limitar o déjelo en blanco para deshabilitar la clasificación. arch.config.optimize=optimizar diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 4294be60..50301ffb 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -3,6 +3,12 @@ arch.action.db_locked.body.l2=It is necessary to unlock it to continue. arch.action.db_locked.confirmation=Unlock and continue arch.action.db_locked.error=It was not possible to unlock the database. arch.action.db_locked.title=Database locked +arch.action.disable_pkgbuild_edition=Unmark PKGBUILD as editable +arch.action.disable_pkgbuild_edition.confirm=Unmark PKGBUILD of {} as editable ? +arch.action.disable_pkgbuild_edition.status=Unmarking PKGBUILD as editable +arch.action.enable_pkgbuild_edition=Mark PKGBUILD as editable +arch.action.enable_pkgbuild_edition.confirm=Mark PKGBUILD of {} as editable ? +arch.action.enable_pkgbuild_edition.status=Marking PKGBUILD as editable arch.aur.install.pgp.body=Per installare {} è necessario ricevere le seguenti chiavi PGP arch.aur.install.pgp.receive_fail=Impossibile ricevere la chiave PGP {} arch.aur.install.pgp.sign_fail=Impossibile firmare la chiave PGP {} @@ -26,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.clean_cache=Rimuovi le vecchie versioni arch.config.clean_cache.tip=Se le vecchie versioni di un pacchetto memorizzate sul disco devono essere rimosse durante la disinstallazione +arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) +arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Mirrors sort limit arch.config.mirrors_sort_limit.tip=Defines the maximum number of mirrors that will be used for speed sorting. Use 0 for no limit or leave it blank to disable sorting. arch.config.optimize=optimize diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index e5ba5d0e..109e02ad 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -3,6 +3,13 @@ arch.action.db_locked.body.l2=É necessário desbloquea-lo para continuar. arch.action.db_locked.confirmation=Desbloquear e continuar arch.action.db_locked.error=Não foi possível desbloquear o banco de dados. arch.action.db_locked.title=Banco de dados bloqueado +arch.action.disable_pkgbuild_edition=Desmarcar PKGBUILD como editável +arch.action.disable_pkgbuild_edition.confirm=Desmarcar o PKGBUILD de {} como editável ? +arch.action.disable_pkgbuild_edition.status=Desmarcando PKGBUILD como editável +arch.action.enable_pkgbuild_edition=Marcando PKGBUILD como editável +arch.action.enable_pkgbuild_edition.confirm=Marcar o PKGBUILD de {} como editável ? +arch.action.enable_pkgbuild_edition.status=Marcando PKGBUILD como editável +arch.aur.action.edit_pkgbuild.body=Editar o arquivo PKGBUILD de {} antes de continuar ? arch.aur.install.pgp.body=Para instalar {} é necessário receber as seguintes chaves PGP arch.aur.install.pgp.receive_fail=Não foi possível receber a chave PGP {} arch.aur.install.pgp.sign_fail=Não foi possível assinar a chave PGP {} @@ -26,6 +33,8 @@ arch.config.automatch_providers=Auto-definir provedores de dependências arch.config.automatch_providers.tip=Escolhe automaticamente qual provedor será utilizado para determinada dependência de um pacote caso os nomes de ambos sejam iguais. arch.config.clean_cache=Remover versões antigas arch.config.clean_cache.tip=Se versões antigas de um pacote armazenadas em disco devem ser removidas durante a desinstalação +arch.config.edit_aur_pkgbuild=Editar PKGBUILD (AUR) +arch.config.edit_aur_pkgbuild.tip=Se o arquivo PKGBUILD de um pacote do AUR deve ser exibido para edição antes da instalação/atualização/reversão arch.config.mirrors_sort_limit=Limite de ordenação de espelhos arch.config.mirrors_sort_limit.tip=Define o número máximo de espelhos que serão utilizados para a ordenação por velocidade. Use 0 para não limitar ou deixe em branco para desabilitar a ordenação. arch.config.optimize=Otimizar diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index ed3444f3..cd80b584 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -3,6 +3,12 @@ arch.action.db_locked.body.l2=It is necessary to unlock it to continue. arch.action.db_locked.confirmation=Unlock and continue arch.action.db_locked.error=It was not possible to unlock the database. arch.action.db_locked.title=Database locked +arch.action.disable_pkgbuild_edition=Unmark PKGBUILD as editable +arch.action.disable_pkgbuild_edition.confirm=Unmark PKGBUILD of {} as editable ? +arch.action.disable_pkgbuild_edition.status=Unmarking PKGBUILD as editable +arch.action.enable_pkgbuild_edition=Mark PKGBUILD as editable +arch.action.enable_pkgbuild_edition.confirm=Mark PKGBUILD of {} as editable ? +arch.action.enable_pkgbuild_edition.status=Marking PKGBUILD as editable arch.aur.install.pgp.body=Для установки {} необходимо получить следующие PGP ключи arch.aur.install.pgp.receive_fail=Не удалось получить PGP-ключ {} arch.aur.install.pgp.sign_fail=Не удалось подписать PGP-ключ {} @@ -26,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall +arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) +arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Ограничение сортировки зеркал arch.config.mirrors_sort_limit.tip=Определяет максимальное количество зеркал, которые будут использоваться для сортировки по скорости. Используйте 0 для No limit или оставьте его пустым, чтобы отключить сортировку. arch.config.optimize=Оптимизация diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index ebe03ea9..ce5ed856 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -3,6 +3,12 @@ arch.action.db_locked.body.l2=Devam etmek için veritabanı kilidini açmak gere arch.action.db_locked.confirmation=Veritabanı kilidini aç ve devam et arch.action.db_locked.error=Veritabanının kilidini açmak mümkün olmadı. arch.action.db_locked.title=Veritabanı kilitli +arch.action.disable_pkgbuild_edition=Unmark PKGBUILD as editable +arch.action.disable_pkgbuild_edition.confirm=Unmark PKGBUILD of {} as editable ? +arch.action.disable_pkgbuild_edition.status=Unmarking PKGBUILD as editable +arch.action.enable_pkgbuild_edition=Mark PKGBUILD as editable +arch.action.enable_pkgbuild_edition.confirm=Mark PKGBUILD of {} as editable ? +arch.action.enable_pkgbuild_edition.status=Marking PKGBUILD as editable arch.aur.install.pgp.body={} kurmak için aşağıdaki PGP anahtarlarını almak gereklidir arch.aur.install.pgp.receive_fail=PGP anahtarı alınamadı {} arch.aur.install.pgp.sign_fail=PGP anahtarı {} imzalanamadı @@ -26,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.clean_cache=Önbelleği temizle arch.config.clean_cache.tip=Disk üzerinde kurulu bir paketin eski sürümlerinin kaldırma sırasında kaldırılıp kaldırılmayacağı +arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) +arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Yansı sıralama sınırı arch.config.mirrors_sort_limit.tip=Hız sıralama için kullanılacak maksimum yansı sayısını tanımlar. Sınırsız olması için 0 kullanın veya sıralamayı devre dışı bırakmak için boş bırakın. arch.config.optimize=optimize diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 6bf69e60..43eea85a 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -40,10 +40,11 @@ def __init__(self, context: ApplicationContext): self.info_path = None self.custom_actions = [ CustomSoftwareAction(i18n_status_key='snap.action.refresh.status', - i18_label_key='snap.action.refresh.label', + i18n_label_key='snap.action.refresh.label', icon_path=resource.get_path('img/refresh.svg', context.get_view_path()), manager_method='refresh', - requires_root=True) + requires_root=True, + i18n_confirm_key='snap.action.refresh.confirm') ] def get_info_path(self) -> str: diff --git a/bauh/gems/snap/resources/locale/ca b/bauh/gems/snap/resources/locale/ca index d5c4966c..04da1593 100644 --- a/bauh/gems/snap/resources/locale/ca +++ b/bauh/gems/snap/resources/locale/ca @@ -1,4 +1,5 @@ gem.snap.info=Aplicacions publicades a https://snapcraft.io/store +snap.action.refresh.confirm=Actualitza {} ? snap.action.refresh.label=Actualitza snap.action.refresh.status=S’està actualitzant snap.info.commands=ordres diff --git a/bauh/gems/snap/resources/locale/de b/bauh/gems/snap/resources/locale/de index 5883a71a..da013a37 100644 --- a/bauh/gems/snap/resources/locale/de +++ b/bauh/gems/snap/resources/locale/de @@ -1,4 +1,5 @@ gem.snap.info=Anwendungen von https://snapcraft.io/store +snap.action.refresh.confirm=Erneuern {} ? snap.action.refresh.label=Erneuern snap.action.refresh.status=Erneuern snap.info.commands=Kommandos diff --git a/bauh/gems/snap/resources/locale/en b/bauh/gems/snap/resources/locale/en index 33dcfcbc..5dc82da9 100644 --- a/bauh/gems/snap/resources/locale/en +++ b/bauh/gems/snap/resources/locale/en @@ -1,4 +1,5 @@ gem.snap.info=Applications published at https://snapcraft.io/store +snap.action.refresh.confirm=Refresh {} ? snap.action.refresh.label=Refresh snap.action.refresh.status=Refreshing snap.info.commands=commands diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index 15319d96..f6494044 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -1,4 +1,5 @@ gem.snap.info=Aplicativos publicados en https://snapcraft.io/store +snap.action.refresh.confirm=Actualizar {} ? snap.action.refresh.label=Actualizar snap.action.refresh.status=Actualizando snap.info.commands=comandos diff --git a/bauh/gems/snap/resources/locale/it b/bauh/gems/snap/resources/locale/it index ed6a1d96..6d66aee1 100644 --- a/bauh/gems/snap/resources/locale/it +++ b/bauh/gems/snap/resources/locale/it @@ -1,4 +1,5 @@ gem.snap.info=Applicazioni pubblicate su https://snapcraft.io/store +snap.action.refresh.confirm=Ripristina {} ? snap.action.refresh.label=Ripristina snap.action.refresh.status=Ripristinare snap.info.commands=commands diff --git a/bauh/gems/snap/resources/locale/pt b/bauh/gems/snap/resources/locale/pt index 0b3ad650..35223efa 100644 --- a/bauh/gems/snap/resources/locale/pt +++ b/bauh/gems/snap/resources/locale/pt @@ -1,4 +1,5 @@ gem.snap.info=Aplicativos publicados em https://snapcraft.io/store +snap.action.refresh.confirm=Atualizar {} ? snap.action.refresh.label=Atualizar snap.action.refresh.status=Atualizando snap.info.commands=comandos diff --git a/bauh/gems/snap/resources/locale/ru b/bauh/gems/snap/resources/locale/ru index 1b3a3564..a7d60891 100644 --- a/bauh/gems/snap/resources/locale/ru +++ b/bauh/gems/snap/resources/locale/ru @@ -1,4 +1,5 @@ gem.snap.info=Приложения, опубликованные на https://snapcraft.io/store +snap.action.refresh.confirm=Обновить {} ? snap.action.refresh.label=Обновить snap.action.refresh.status=Обновляется snap.info.commands=Команды diff --git a/bauh/gems/snap/resources/locale/tr b/bauh/gems/snap/resources/locale/tr index 9e9e5fd7..9347c7ce 100644 --- a/bauh/gems/snap/resources/locale/tr +++ b/bauh/gems/snap/resources/locale/tr @@ -1,4 +1,5 @@ gem.snap.info=https://snapcraft.io/store adresinde yayınlanan uygulamalar +snap.action.refresh.confirm=Yenile {} ? snap.action.refresh.label=Yenile snap.action.refresh.status=Yenileniyor snap.info.commands=komutlar diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index b0ec0426..365fb8f5 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -69,7 +69,7 @@ def __init__(self, context: ApplicationContext, suggestions_downloader: Thread = self.env_thread = None self.suggestions_downloader = suggestions_downloader self.suggestions = {} - self.custom_actions = [CustomSoftwareAction(i18_label_key='web.custom_action.clean_env', + self.custom_actions = [CustomSoftwareAction(i18n_label_key='web.custom_action.clean_env', i18n_status_key='web.custom_action.clean_env.status', manager=self, manager_method='clean_environment', diff --git a/bauh/view/core/controller.py b/bauh/view/core/controller.py index b7379f9e..5affb4db 100755 --- a/bauh/view/core/controller.py +++ b/bauh/view/core/controller.py @@ -53,14 +53,14 @@ def __init__(self, managers: List[SoftwareManager], context: ApplicationContext, self.config = config self.settings_manager = settings_manager self.http_client = context.http_client - self.extra_actions = [CustomSoftwareAction(i18_label_key='action.reset', + self.extra_actions = [CustomSoftwareAction(i18n_label_key='action.reset', i18n_status_key='action.reset.status', manager_method='reset', manager=self, icon_path=resource.get_path('img/logo.svg'), requires_root=False, refresh=False)] - self.dynamic_extra_actions = {CustomSoftwareAction(i18_label_key='action.backups', + self.dynamic_extra_actions = {CustomSoftwareAction(i18n_label_key='action.backups', i18n_status_key='action.backups.status', manager_method='launch_timeshift', manager=self, diff --git a/bauh/view/core/settings.py b/bauh/view/core/settings.py index dddcfece..ef920d6a 100644 --- a/bauh/view/core/settings.py +++ b/bauh/view/core/settings.py @@ -12,6 +12,7 @@ from bauh.api.abstract.view import ViewComponent, TabComponent, InputOption, TextComponent, MultipleSelectComponent, \ PanelComponent, FormComponent, TabGroupComponent, SingleSelectComponent, SelectViewType, TextInputComponent, \ FileChooserComponent, RangeInputComponent +from bauh.commons.view_utils import new_select from bauh.view.core import config, timeshift from bauh.view.core.config import read_config from bauh.view.core.downloader import AdaptableFileDownloader @@ -100,7 +101,7 @@ def _gen_adv_settings(self, core_config: dict, screen_width: int, screen_height: max_width=default_width, id_="icon_exp") - select_trim_up = self._gen_select(label=self.i18n['core.config.trim.after_upgrade'], + select_trim_up = new_select(label=self.i18n['core.config.trim.after_upgrade'], tip=self.i18n['core.config.trim.after_upgrade.tip'], value=core_config['disk']['trim']['after_upgrade'], max_width=default_width, @@ -141,7 +142,7 @@ def _gen_multithread_client_select(self, core_config: dict, default_width: int) if current_mthread_client not in available_mthread_clients: current_mthread_client = None - return self._gen_select(label=self.i18n['core.config.download.multithreaded_client'], + return new_select(label=self.i18n['core.config.download.multithreaded_client'], tip=self.i18n['core.config.download.multithreaded_client.tip'], id_="mthread_client", max_width=default_width, @@ -293,7 +294,7 @@ def _gen_general_settings(self, core_config: dict, screen_width: int, screen_hei max_width=default_width, id_="sugs_by_type") - inp_reboot = self._gen_select(label=self.i18n['core.config.updates.reboot'], + inp_reboot = new_select(label=self.i18n['core.config.updates.reboot'], tip=self.i18n['core.config.updates.reboot.tip'], id_='ask_for_reboot', max_width=default_width, @@ -479,64 +480,51 @@ def _gen_backup_settings(self, core_config: dict, screen_width: int, screen_heig (self.i18n['no'].capitalize(), False, None), (self.i18n['ask'].capitalize(), None, None)] - install_mode = self._gen_select(label=self.i18n['core.config.backup.install'], - tip=None, - value=core_config['backup']['install'], - opts=ops_opts, - max_width=default_width, - id_='install') + install_mode = new_select(label=self.i18n['core.config.backup.install'], + tip=None, + value=core_config['backup']['install'], + opts=ops_opts, + max_width=default_width, + id_='install') - uninstall_mode = self._gen_select(label=self.i18n['core.config.backup.uninstall'], - tip=None, - value=core_config['backup']['uninstall'], - opts=ops_opts, - max_width=default_width, - id_='uninstall') + uninstall_mode = new_select(label=self.i18n['core.config.backup.uninstall'], + tip=None, + value=core_config['backup']['uninstall'], + opts=ops_opts, + max_width=default_width, + id_='uninstall') - upgrade_mode = self._gen_select(label=self.i18n['core.config.backup.upgrade'], - tip=None, - value=core_config['backup']['upgrade'], - opts=ops_opts, - max_width=default_width, - id_='upgrade') + upgrade_mode = new_select(label=self.i18n['core.config.backup.upgrade'], + tip=None, + value=core_config['backup']['upgrade'], + opts=ops_opts, + max_width=default_width, + id_='upgrade') - downgrade_mode = self._gen_select(label=self.i18n['core.config.backup.downgrade'], - tip=None, - value=core_config['backup']['downgrade'], - opts=ops_opts, - max_width=default_width, - id_='downgrade') - - mode = self._gen_select(label=self.i18n['core.config.backup.mode'], - tip=None, - value=core_config['backup']['mode'], - opts=[ - (self.i18n['core.config.backup.mode.incremental'], 'incremental', - self.i18n['core.config.backup.mode.incremental.tip']), - (self.i18n['core.config.backup.mode.only_one'], 'only_one', - self.i18n['core.config.backup.mode.only_one.tip']) - ], - max_width=default_width, - id_='mode') - type_ = self._gen_select(label=self.i18n['type'].capitalize(), - tip=None, - value=core_config['backup']['type'], - opts=[('rsync', 'rsync', None), ('btrfs', 'btrfs', None)], - max_width=default_width, - id_='type') + downgrade_mode = new_select(label=self.i18n['core.config.backup.downgrade'], + tip=None, + value=core_config['backup']['downgrade'], + opts=ops_opts, + max_width=default_width, + id_='downgrade') + + mode = new_select(label=self.i18n['core.config.backup.mode'], + tip=None, + value=core_config['backup']['mode'], + opts=[ + (self.i18n['core.config.backup.mode.incremental'], 'incremental', + self.i18n['core.config.backup.mode.incremental.tip']), + (self.i18n['core.config.backup.mode.only_one'], 'only_one', + self.i18n['core.config.backup.mode.only_one.tip']) + ], + max_width=default_width, + id_='mode') + type_ = new_select(label=self.i18n['type'].capitalize(), + tip=None, + value=core_config['backup']['type'], + opts=[('rsync', 'rsync', None), ('btrfs', 'btrfs', None)], + max_width=default_width, + id_='type') sub_comps = [FormComponent([enabled_opt, mode, type_, install_mode, uninstall_mode, upgrade_mode, downgrade_mode], spaces=False)] return TabComponent(self.i18n['core.config.tab.backup'].capitalize(), PanelComponent(sub_comps), None, 'core.bkp') - - def _gen_select(self, label: str, tip: str, id_: str, opts: List[tuple], value: object, max_width: int, type_: SelectViewType = SelectViewType.RADIO): - inp_opts = [InputOption(label=o[0].capitalize(), value=o[1], tooltip=o[2]) for o in opts] - def_opt = [o for o in inp_opts if o.value == value] - return SingleSelectComponent(label=label, - tooltip=tip, - options=inp_opts, - default_option=def_opt[0] if def_opt else inp_opts[0], - max_per_line=len(inp_opts), - max_width=max_width, - type_=type_, - id_=id_) - diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 6c717b3b..397f4165 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -12,7 +12,7 @@ from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.model import PackageStatus -from bauh.commons.html import strip_html +from bauh.commons.html import strip_html, bold from bauh.view.qt import dialog from bauh.view.qt.colors import GREEN, BROWN from bauh.view.qt.components import IconButton @@ -167,15 +167,20 @@ def ignore_updates(): if bool(pkg.model.get_custom_supported_actions()): for action in pkg.model.get_custom_supported_actions(): - item = QAction(self.i18n[action.i18_label_key]) + item = QAction(self.i18n[action.i18n_label_key]) if action.icon_path: item.setIcon(QIcon(action.icon_path)) def custom_action(): + if action.i18n_confirm_key: + body = self.i18n[action.i18n_confirm_key].format(bold(pkg.model.name)) + else: + body = '{} ?'.format(self.i18n[action.i18n_label_key]) + if dialog.ask_confirmation( - title=self.i18n[action.i18_label_key], - body=self._parag('{} {} ?'.format(self.i18n[action.i18_label_key], self._bold(str(pkg)))), + title=self.i18n[action.i18n_label_key], + body=self._parag(body), i18n=self.i18n): self.window.begin_execute_custom_action(pkg, action) diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 90603fdf..006d4f95 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -7,11 +7,11 @@ from PyQt5.QtGui import QIcon, QPixmap, QIntValidator, QCursor from PyQt5.QtWidgets import QRadioButton, QGroupBox, QCheckBox, QComboBox, QGridLayout, QWidget, \ QLabel, QSizePolicy, QLineEdit, QToolButton, QHBoxLayout, QFormLayout, QFileDialog, QTabWidget, QVBoxLayout, \ - QSlider, QScrollArea, QFrame, QAction, QSpinBox + QSlider, QScrollArea, QFrame, QAction, QSpinBox, QPlainTextEdit from bauh.api.abstract.view import SingleSelectComponent, InputOption, MultipleSelectComponent, SelectViewType, \ TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, TabGroupComponent, PanelComponent, \ - TwoStateButtonComponent, TextComponent, SpacerComponent, RangeInputComponent, ViewObserver + TwoStateButtonComponent, TextComponent, SpacerComponent, RangeInputComponent, ViewObserver, TextInputType from bauh.view.qt import css from bauh.view.qt.colors import RED from bauh.view.util import resource @@ -434,7 +434,24 @@ def __init__(self, **kwargs): super(QLineEditObserver, self).__init__(**kwargs) def on_change(self, change: str): - self.setText(change if change is not None else '') + if self.text() != change: + self.setText(change if change is not None else '') + + +class QPlainTextEditObserver(QPlainTextEdit, ViewObserver): + + def __init__(self, **kwargs): + super(QPlainTextEditObserver, self).__init__(**kwargs) + + def on_change(self, change: str): + self.setText(change) + + def setText(self, text: str): + if text != self.toPlainText(): + self.setPlainText(text if text is not None else '') + + def setCursorPosition(self, idx: int): + self.textCursor().setPosition(idx) class TextInputQt(QGroupBox): @@ -449,7 +466,7 @@ def __init__(self, model: TextInputComponent): if self.model.max_width > 0: self.setMaximumWidth(self.model.max_width) - self.text_input = QLineEditObserver() + self.text_input = QLineEditObserver() if model.type == TextInputType.SINGLE_LINE else QPlainTextEditObserver() if model.only_int: self.text_input.setValidator(QIntValidator()) @@ -469,8 +486,9 @@ def __init__(self, model: TextInputComponent): self.model.observers.append(self.text_input) self.layout().addWidget(self.text_input, 0, 1) - def _update_model(self, text: str): - self.model.set_value(val=text, caller=self) + def _update_model(self, *args): + change = args[0] if args else self.text_input.toPlainText() + self.model.set_value(val=change, caller=self) class MultipleSelectQt(QGroupBox): @@ -775,29 +793,29 @@ def gen_tip_icon(self, tip: str) -> QLabel: return tip_icon def _new_text_input(self, c: TextInputComponent) -> Tuple[QLabel, QLineEdit]: - line_edit = QLineEditObserver() + view = QLineEditObserver() if c.type == TextInputType.SINGLE_LINE else QPlainTextEditObserver() if c.only_int: - line_edit.setValidator(QIntValidator()) + view.setValidator(QIntValidator()) if c.tooltip: - line_edit.setToolTip(c.tooltip) + view.setToolTip(c.tooltip) if c.placeholder: - line_edit.setPlaceholderText(c.placeholder) + view.setPlaceholderText(c.placeholder) if c.value: - line_edit.setText(str(c.value) if c.value else '') - line_edit.setCursorPosition(0) + view.setText(str(c.value) if c.value else '') + view.setCursorPosition(0) if c.read_only: - line_edit.setEnabled(False) + view.setEnabled(False) def update_model(text: str): - c.set_value(val=text, caller=line_edit) + c.set_value(val=text, caller=view) - line_edit.textChanged.connect(update_model) - c.observers.append(line_edit) + view.textChanged.connect(update_model) + c.observers.append(view) label = QWidget() label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum) @@ -812,7 +830,7 @@ def update_model(text: str): if c.tooltip: label.layout().addWidget(self.gen_tip_icon(c.tooltip)) - return label, self._wrap(line_edit, c) + return label, self._wrap(view, c) def _new_range_input(self, model: RangeInputComponent) -> QSpinBox: spinner = QSpinBox() diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 33631520..5ff7b61d 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -1394,7 +1394,7 @@ def _update_progress(self, value: int): def begin_execute_custom_action(self, pkg: PackageView, action: CustomSoftwareAction): if pkg is None and not dialog.ask_confirmation(title=self.i18n['confirmation'].capitalize(), - body=self.i18n['custom_action.proceed_with'].capitalize().format('"{}"'.format(self.i18n[action.i18_label_key])), + body=self.i18n['custom_action.proceed_with'].capitalize().format('"{}"'.format(self.i18n[action.i18n_label_key])), icon=QIcon(action.icon_path) if action.icon_path else QIcon(resource.get_path('img/logo.svg')), i18n=self.i18n): return False @@ -1450,7 +1450,7 @@ def show_settings(self): self.settings_window.show() def _map_custom_action(self, action: CustomSoftwareAction) -> QAction: - custom_action = QAction(self.i18n[action.i18_label_key]) + custom_action = QAction(self.i18n[action.i18n_label_key]) if action.icon_path: try: diff --git a/bauh/view/resources/img/ignore_update.svg b/bauh/view/resources/img/ignore_update.svg index e1148552..804dafb4 100644 --- a/bauh/view/resources/img/ignore_update.svg +++ b/bauh/view/resources/img/ignore_update.svg @@ -7,128 +7,128 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - inkscape:version="1.0 (4035a4fb49, 2020-05-01)" - height="24" - width="23.515306" - sodipodi:docname="ignore_update.svg" - xml:space="preserve" - viewBox="0 0 23.515307 24" - y="0px" - x="0px" + version="1.1" id="Capa_1" - version="1.1">image/svg+xml + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="739" + id="namedview973" + showgrid="false" + showborder="true" + fit-margin-top="0" + fit-margin-left="0" + fit-margin-right="0" + fit-margin-bottom="0" + inkscape:zoom="9.4874926" + inkscape:cx="25.836555" + inkscape:cy="15.198259" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="Capa_1" /> + id="g942" + transform="translate(-99.310818,-31.505501)"> + id="g944" + transform="translate(-99.310818,-31.505501)"> + id="g946" + transform="translate(-99.310818,-31.505501)"> + id="g948" + transform="translate(-99.310818,-31.505501)"> + id="g950" + transform="translate(-99.310818,-31.505501)"> + id="g952" + transform="translate(-99.310818,-31.505501)"> + id="g954" + transform="translate(-99.310818,-31.505501)"> + id="g956" + transform="translate(-99.310818,-31.505501)"> + id="g958" + transform="translate(-99.310818,-31.505501)"> + id="g960" + transform="translate(-99.310818,-31.505501)"> + id="g962" + transform="translate(-99.310818,-31.505501)"> + id="g964" + transform="translate(-99.310818,-31.505501)"> + id="g966" + transform="translate(-99.310818,-31.505501)"> + id="g968" + transform="translate(-99.310818,-31.505501)"> + id="g970" + transform="translate(-99.310818,-31.505501)"> + id="g940"> + style="fill:#ff0000" + id="g938"> + d="M 213.333,0 C 95.467,0 0,95.467 0,213.333 0,331.199 95.467,426.666 213.333,426.666 331.199,426.666 426.667,331.2 426.667,213.333 426.667,95.466 331.2,0 213.333,0 Z M 42.667,213.333 c 0,-94.293 76.373,-170.667 170.667,-170.667 39.467,0 75.627,13.547 104.533,35.947 L 78.613,317.867 C 56.213,288.96 42.667,252.8 42.667,213.333 Z M 213.333,384 C 173.866,384 137.706,370.453 108.8,348.053 L 348.053,108.8 C 370.453,137.707 384,173.867 384,213.333 384,307.627 307.627,384 213.333,384 Z" /> diff --git a/pictures/releases/0.9.7/aur_pkgbuild.png b/pictures/releases/0.9.7/aur_pkgbuild.png new file mode 100644 index 0000000000000000000000000000000000000000..edfe89cdc852415bbf5e65b3ee8150924b45d5e3 GIT binary patch literal 38812 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�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

(th5;L6dE)=-6Y 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 B>9zm> literal 0 HcmV?d00001 diff --git a/pictures/releases/0.9.7/mark_pkgbuild.png b/pictures/releases/0.9.7/mark_pkgbuild.png new file mode 100644 index 0000000000000000000000000000000000000000..438068ce5708a08ef62e996812ec2651c6428f53 GIT binary patch literal 10951 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 K3Rei~|M)L7)X@k4 literal 0 HcmV?d00001 diff --git a/pictures/releases/0.9.7/unmark_pkgbuild.png b/pictures/releases/0.9.7/unmark_pkgbuild.png new file mode 100644 index 0000000000000000000000000000000000000000..6e356129ab85824d8ed756ce640fbdbb9e1376ed GIT binary patch literal 7561 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 literal 0 HcmV?d00001 From c8335403b0d2b18242752136b77e96a904349bc2 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 16 Aug 2020 12:54:03 -0300 Subject: [PATCH 25/99] Removing releases pictures and linking changelog to 'bauh-files' pictures --- CHANGELOG.md | 42 +++++++++--------- pictures/releases/0.9.2/ask_reboot.png | Bin 742273 -> 0 bytes pictures/releases/0.9.2/backup_mode.png | Bin 19333 -> 0 bytes pictures/releases/0.9.2/color_design.png | Bin 1671757 -> 0 bytes pictures/releases/0.9.2/files_conflict.png | Bin 766113 -> 0 bytes pictures/releases/0.9.2/prepare_icon.png | Bin 473876 -> 0 bytes pictures/releases/0.9.2/prepare_output.png | Bin 637751 -> 0 bytes pictures/releases/0.9.4/arch_repo_mthread.png | Bin 16255 -> 0 bytes pictures/releases/0.9.4/ignore_updates.png | Bin 22059 -> 0 bytes pictures/releases/0.9.4/mthread_tool.png | Bin 16585 -> 0 bytes .../releases/0.9.4/revert_ignored_updates.png | Bin 9256 -> 0 bytes .../0.9.4/updates_ignored_category.png | Bin 26049 -> 0 bytes .../0.9.4/version_ignored_updates.png | Bin 12813 -> 0 bytes pictures/releases/0.9.5/arch_providers.png | Bin 20717 -> 0 bytes pictures/releases/0.9.5/backup_action.png | Bin 10209 -> 0 bytes pictures/releases/0.9.6/appim_symlinks.png | Bin 12104 -> 0 bytes pictures/releases/0.9.6/scale.png | Bin 16421 -> 0 bytes .../releases/0.9.7/arch_install_reason.png | Bin 32027 -> 0 bytes pictures/releases/0.9.7/aur_pkgbuild.png | Bin 38812 -> 0 bytes pictures/releases/0.9.7/flatpak_refs.png | Bin 26246 -> 0 bytes pictures/releases/0.9.7/mark_pkgbuild.png | Bin 10951 -> 0 bytes pictures/releases/0.9.7/unmark_pkgbuild.png | Bin 7561 -> 0 bytes 22 files changed, 21 insertions(+), 21 deletions(-) delete mode 100755 pictures/releases/0.9.2/ask_reboot.png delete mode 100644 pictures/releases/0.9.2/backup_mode.png delete mode 100644 pictures/releases/0.9.2/color_design.png delete mode 100644 pictures/releases/0.9.2/files_conflict.png delete mode 100644 pictures/releases/0.9.2/prepare_icon.png delete mode 100644 pictures/releases/0.9.2/prepare_output.png delete mode 100644 pictures/releases/0.9.4/arch_repo_mthread.png delete mode 100644 pictures/releases/0.9.4/ignore_updates.png delete mode 100644 pictures/releases/0.9.4/mthread_tool.png delete mode 100644 pictures/releases/0.9.4/revert_ignored_updates.png delete mode 100644 pictures/releases/0.9.4/updates_ignored_category.png delete mode 100644 pictures/releases/0.9.4/version_ignored_updates.png delete mode 100644 pictures/releases/0.9.5/arch_providers.png delete mode 100644 pictures/releases/0.9.5/backup_action.png delete mode 100644 pictures/releases/0.9.6/appim_symlinks.png delete mode 100644 pictures/releases/0.9.6/scale.png delete mode 100644 pictures/releases/0.9.7/arch_install_reason.png delete mode 100644 pictures/releases/0.9.7/aur_pkgbuild.png delete mode 100644 pictures/releases/0.9.7/flatpak_refs.png delete mode 100644 pictures/releases/0.9.7/mark_pkgbuild.png delete mode 100644 pictures/releases/0.9.7/unmark_pkgbuild.png diff --git a/CHANGELOG.md b/CHANGELOG.md index e02a7045..ea7f6f48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,15 +10,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - AUR: - allowing to edit the PKGBUILD file of a package to be installed/upgraded/downgraded. If enabled, a popup will be displayed during this acctions allowing the PKGBUILD to be edited.

- +

- mark a given PKGBUILD of a package as editable (if the property above is enabled, the same behavior will be applied)

- +

- unmark a given PKGBUILD of a package as editable (it prevents the behavior described above to happen)

- +

### Improvements @@ -30,7 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Arch - upgrade summary: displaying the reason a given package must be installed

- +

- upgrade: - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues @@ -62,7 +62,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - crashing when an update size cannot be read [#130](https://github.com/vinifmor/bauh/issues/130) - installation fails when there are multiple references for a given package (e.g: openh264)

- +

- UI - crashing when nothing can be upgraded @@ -75,7 +75,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - creating a symlink for the installed applications at **~.local/bin** (the link will have the same name of the application. if the link already exists, it will be named as 'app_name-appimage') [#122](https://github.com/vinifmor/bauh/issues/122) - new initialization task that checks if the installed AppImage files have symlinks associated with (it creates new symlinks if needed)

- +

- able to update AppImages with continuous releases - UI @@ -91,7 +91,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - new property **ui.scale_factor** responsible for defining the interface scale factor. Useful if bauh looks small on the screen. It can be changed through the settings window (**Interface** tab):

- +

### Fixes @@ -119,14 +119,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Features - new custom action (**+**) to open the system backups (snapshots). It is just a shortcut to Timeshift.

- +

### Improvements - Arch - new **automatch_providers** settings: bauh will automatically choose which provider will be used for a package dependency when both names are equal (enabled by default).

- +

- UI @@ -160,30 +160,30 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Ignore updates: now it is possible to ignore updates from software packages through their actions button (**+**). Supported types: Arch packages, Flatpaks and AppImages

- +

- +

- Packages with ignored updates have their versions displayed with a brown shade

- +

- It is possible to filter all you packages with updates ignored through the new category **Updates ignored**

- +

- Arch - supporting multi-threaded download for repository packages (enabled by default)

- +

- Settings - [axel](https://github.com/axel-download-accelerator/axel) added as an alternative multi-threaded download tool. The download tool can be defined through the new field **Multi-threaded download tool** on the settings window **Advanced** tab (check **Default** for bauh to decide which one to use)

- +

@@ -246,29 +246,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - UI - it is possible to view details of some initialization tasks by clicking on their icons

- +

- +

### Improvements - Backup - new **type** field on settings to specify the Timeshift backup mode: **RSYNC** or **BTRFS**

- +

- Trim - the dialog is now displayed before the upgrading process (but the operation is only executed after a successful upgrade) - Settings - new option to disable the reboot dialog after a successful upgrade (`updates.ask_for_reboot`)

- +

- Arch - able to handle upgrade scenarios when a package wants to overwrite files of another installed package

- +

- displaying more upgrade substatus @@ -282,7 +282,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - icons, buttons and colors changes

- +

- more unnecessary **x** buttons were removed from dialogs diff --git a/pictures/releases/0.9.2/ask_reboot.png b/pictures/releases/0.9.2/ask_reboot.png deleted file mode 100755 index e70c9f69e3d128a9645709d511867b09a3a8a111..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 742273 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

_r0h&aPkv7+Ka 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 Ve0<*5ZKM|i2Mp@}-DP*Z@c$G`bl?C0 diff --git a/pictures/releases/0.9.2/files_conflict.png b/pictures/releases/0.9.2/files_conflict.png deleted file mode 100644 index 57242af893023cd0761378923eb9e03437906e7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 766113 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%0D5czpAOcV2Zc{lI1|8>c^=Hnn%PnoV{EA5n+= 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 diff --git a/pictures/releases/0.9.2/prepare_output.png b/pictures/releases/0.9.2/prepare_output.png deleted file mode 100644 index 7cecf1d23e0d903bcbc2d846dd630443c02080bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 637751 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 diff --git a/pictures/releases/0.9.4/arch_repo_mthread.png b/pictures/releases/0.9.4/arch_repo_mthread.png deleted file mode 100644 index 503211ffdcb4c29a4a23ed764919e7a4d07182a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16255 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�chz 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 G=l=jl#M}M= diff --git a/pictures/releases/0.9.4/version_ignored_updates.png b/pictures/releases/0.9.4/version_ignored_updates.png deleted file mode 100644 index 6bacb4e763538c3e8ac392802b202409f06d8b92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12813 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 diff --git a/pictures/releases/0.9.5/arch_providers.png b/pictures/releases/0.9.5/arch_providers.png deleted file mode 100644 index fca8674598829aa778d40363de1ba97fea4cd902..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20717 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 diff --git a/pictures/releases/0.9.5/backup_action.png b/pictures/releases/0.9.5/backup_action.png deleted file mode 100644 index 7d54436edf0d8f6abdf916ea56436e79931870d3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10209 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 N-bpG*l#3Yz{11dQxG4Yt diff --git a/pictures/releases/0.9.6/appim_symlinks.png b/pictures/releases/0.9.6/appim_symlinks.png deleted file mode 100644 index e9b6a710c9f407c9d7b00504a798a9c15ceaba4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12104 zcmb_?by!tjx9$dhl#-GnAtE3R(v2Y9-5?E{?hYj-r9^4jq;zb$yF);vrMtV&;kzFr}s5t3V(K`QY~gG!*dD?|Z=* zSO~Tf6H}5F6MO04WN&U|V+Mh|iS>;Yl%T#Wk#x%&ac zziCjd(u7orb3lrES(K)M{vX4!U_XpkAz__zz6$v9hopuVhev13-Ia|acf4MSseO

TUIpQ$^&9;A+>vQ_NTi38yPqSr1R;3Vr7Rh&akWf^oQVuIbeqa|AV_C`fkcY=VnN4}| zU;@K5U8Lp3QCCrL2$)$5tkH46(sLJaEf+C+TU#?b7l@dX znURZ`=}UJj7t5ED((+20AF+rake3kY_wUp^4EE+d^%BNtSLlezr-?8HaFqj&JGuEg6s$kSQ>!gUe0Jp(U{Lb2g$Nb&IUQc+i*T&=Di zb?~2Rhc9H{h=zT2oV>c$uV_W_sI_L+c7}UGtDV;7Al0~lXQF6J39mDQa71~iH6@XWRJTM&-7;dVm7bYD)f%SgPB^Bp z;S`TfegbY|%6$^EKers8q9-Ue8tFjmBXUOWKA`B-VWOl458ie-IzRCrQ^svQ zbimyItmvfhLQ>C#5{6=N{AZWq&70GlzdkRG77#9zcC#iXmK*Uz!_1p5H(Hdlrc|{& zx|OX8^`#dTg-;N>Ho|%R7mn!oqM>BArBjDT)!Fj0&=ORvn0LY6^>2mS(uixI!<%ss zG|ZnOd2CBNwwban^Z5mx{GZcF*i8^8tDK$x6yu2M5vkF0Dn1-h$HiSE9$qgR(5$@w z8K~h(@*9hSE#yas1Xn{-j?S-KYEGuuiArqn>R0rW59@;SO9pw8g$2bWo!NzqX}wZUqvzx; z3xC|R&hln#285XPLs?*d^^!yQ!Tr5oDqTr8O6tX?OaWK&Cs7IsEIxED9tV!_%C3c= zgk9L(6?Dy*kpDdndZ56*3`=0UPp%E6+a%$ zLXJ;xP>AE|VN9`C)$#YvYzix zPjAAltg$qY;Mm81e;0oNYKjc|uh?5=pq7k&lF1!~KIc4Px{b=W6)7XA@KZbqj|dwXK0J(df8H&Ny{b>8cD zK2_U-IT<$_!z+p-LRQ?@x_4VL?K7dyhkp&u_veHAENvzftuscpRG$l+gyIIATvv;)GA(z4Sf<=*qzK}j;x22dT zbs6b!qHau>Eof|hVlG}dCcmP1Q*L97cm&VQ?#0UZ##kvVIcS)&B`6+vdEMQO|4S+= z%%*!%!=gLlwBLcXwRVtIQw#9j-^s|FnwsavXC%p5{s|UNC7W(}K>ei>qpYLR-W~PA za$_$drY1&T{=F%_Sx!zu;#aQmr@ON?g`fR=_MlK}>vA1i;wTB|&qB78p{Q%Cy_0gS z;t4fPRaO0=VwgP@i*QPy@V;$DbzRTq)|U8)fv;BOHC1@{a9n&^7q?+)rFG#{MqXB9 zLT~;i&5fOGu}u6+VX~SV(Th+1j@uh=hZEm?QLK-BJhRwVsJqQ6}DbN6Hkpn^)oz6ZgVm?$Qsc{RWZ@{bb}_IkAPRGQTUd z&d)cE`O?G}e3tRnDJB#vzI#<30net8)VcB5uFIbsOud4<;3|4oD!gbjpF<8C(zs9` zv9S`UFJx1jmA}zvD8_n28C0twDmufk*ic`GON`%sb+WPbr<{F_V9f{ahY6uid}42J zpHp33jba}KVZ0^1l*)s>9uG3BZ_xv`CwNy!#A zjFidA^1X3*^27-N?a32cwY>4|d`@B#a9UFGzzQu=bq)=)dOlgwU>xq6cY&c}aBJ?{ zJEU&{S9ZY8JdUq|><^gX6N z|J8V0q2hDLa`&TzDF+zZ`{H$@ug~FeRSQWB*jYkA>&tHWZ=FQeLhlO4vrZ00U{>>? z0~#6{4gyziRCDLW930*(Eyd0)bXu?Nyb!U{mc|pTVhW|oFI6q9g*9w+QSF&)+i-E2 z>Uj+VBqS}Lwkh&(e?Fc&@`{5aa&FG%>zy~cMnv#<9{Y5;$sT?t>m9(ErA2u=HoVZ_wqw+~tT`hx5FT_JGa%T!2vM zf}Wb%tNF@{EX`ykVXwLQ-s9%-OWK|DNm^Abp?*ZEZrRsG4D^>1Q+^&^M{M~kU6Vz^ z%I#Ol-Ok@Y77@H&sbs0Np5k(QD4SQ_!faa<{vzCR?Cy85DgZ#suFeb}2^Djt)ZcSV zdAx9V37>2gFl}P-T(Ho9p+y7k+U)>!_9;qbIcs^10E&cmIyf|35tJXb8vk8* zpo2ibQIP(3O`?*RkkI=I{c%N7LJ3qYsd4`#;E1B-{rAn%BH)^X%(MT@rP?2NYb^eh ze0<4p>nVeZlKlLz&d&D|Q6#}bzbh0eb?Tg`{rvm}lR2MS>QfXKG-*G+K6T+6o5|tH ziE49q48=kI=d7x#go=u{^b8D%H-48(s0$t|m<@cD({j?%=v`f1No2&uTQE*IH^Wt{ zC}q(2Y?T$>QzGWoi2~Km?(XC5!qT_qgT#_U_$Yx0#TtckG-h7$ZY40;q2_}!^@SJ@lr$KEEsGLT3A@9T%ZQA?ifDw=VkbXOTZ`v1X6>a5~=9w&f54-u>)u9 zbS;&PA-9>U#Z8py`r8)B>$v<+C^m&1zEWFTo50O(rOlr*1Vu%~1)q~4bFU(Hw_Pn2 zRaND}Ni9Y~dV2aljh^hn_m?ISh|tZ^9~fkO_yMTo9*}^=+jVZ2o!?R8d5YZA-R6}? zy;L!WzhqgIwX~#sd<1xTdGQGepFMll9!lYd)++pPrI;(9()|$$m6+R3FWy)Gz!Zdy zt&C1f!n2Y2T@4r96y*z0x z^eNV_U%xyqmqMjwWmiW0@7Z;$ZH!iXW22Kf<1#WvR22n-C16W?HH+Qz^YhB_gn}8@ z1u~i9z6LQR!w=b@GNIMe--)(S)6_IP{v!`8s8N`ho&B2JYm@T&d{5TO%Bu2A9$@-l zhH%F5u@fPa&U+u9Mi@=f>e^Z`aAXL-`~E-Mgywp;wQhSeT_ySrIzF@7eQW?R{uM`H zJIc*1YhQJB;De1n4i6nTNH9?XKgLo?W{p{T03UfV>AFAnJRl(f-@tQ?prWFJ$7xjx zRxhjF?8DpOw$~24iApk>)qX($vR}7B&gZtfndv%iKAhUua(~9MK9VtVbI_s)aBTj3 zZccB%@k9ZFMIjU!5ivk*FDWj*RAl-Ob&!KsY0G`R)3A!UIgS6r%_3QqWqN2RDp=%; zjg3te@=czZ(Q)C^ht-3#6Ef?C<>k@ZH=Pl3a&q<~27i@NP(W!lE-y15BGD){7}}mJ zDHOD{i0SC)k}N&%V(Ekc*hVvIl?4nA%7O42+j6m)sX`a2BmW!q z{tszERz36AGo@m z|5pZ|fIwMSSJuG{%XD&8bAmIbrlt3XEhhz@bGY@0ySU{9(vnE z5ZWei9ds-#H#K^Y3i*DUtNNe>F8vI}69EW?64<8v9rWuvdMWZp>S}5qD&c*9Bg4a= zNK!v`=gzi-N&)&J&oNLy%q7f1&vH=hH44?QgoK31i#5DILqSgDPUcEkF0`neE}OR5 zJo92Cd@2FMjqeNUGFGk@fBzl_7x%|pwHD(yIhsfbD#8flCWBHNJdn+uEl&b|PU(Rb ztA2TBC!(~JrKCh2d-L@-u*>!FTm*ID?Scm=kF5nldN(koupv-mD!olhyj&my@ zkwa;pC8q&IO(UsnqgQ8m%XzHU`^$tYej3#cL`g@vb6+gOsB(HU!vnPqK^d}|E>FdN zBWXwGU>JXchj@=lm497&-6}0F-}Qm@4^x)q&)?4_m6*&bwRyiZBE;8e)UmX3liIUv z@GtS?{pInrU|}@3LQniD?;qJoZMsIW($K_kczkGGwYT}X7#zxZYIE_VP`M@NaL(#& z1vW&E@Haw3ptYZ%x`?gzD+=>P zA5%(+&Ln{n^PK@q;Yx{`m{5Z>;Hq}aSmVm%Ibp>bCVcFk4=ieg)z;VIPFcPDtRI3z zKS)T08rSmbK#qhSo=omovz*m@c$}!2z9MprA~W!`e1#4G*%$yAE;e%{`V}IIBc$UlvrytY#1#axd|P zY5c|2<>@~9k)b`^|J2NpVdQ&YcdFN4I9z$K$#l5nXccj-<2NpKXorRh#eZJeTLRnLc6OkbOF8)wnr+$Hf=q zQtZt*)X2;X)f`SDba?40NTBZ3Dshvk6>|0+OTv1EVYrFzgLZ`B+Qa72ye{8t0 z@@#1s4$#aa(2E;rk>Y4@Nr;Drdgs+U{!n5cIo~VyGmOmI4)pW)w`NOQ&Iv*`$@x=P zzVm;MAQ9Q0=J1(89;0hSg_M)s#CmO`9zRv&AQ5~>bx-)#D|G1<3K?bh!?5Olh|)3P zq;Y1tu0ZU@NREh@Nfc>8%W9A3`R@Crd7f6qKs(630cXbZnqhotOWPa$X>`{t0}dr~ z4k&__>4Yk$ZEEIixK{S}6+Aq6($mvl)6%YNZ~vU&Z*JXG#m9eEQ58TsNeeWet zjR!O9imtbjX)yfpv5Vc~Z%TeL=hVJVGK{)rg3Mz3NI7+5UDGan{81ggccb~Xb_zeM z9go-0D}>2=fm(p4`g6L??(LDuCnUbN(7&W|G)S`-{vR}#*0x4Q7gUd|h$5SJF&2Fu z?Da=ZI}eS zpN75>^=K@xTx&?}_87%Fzu0(Z;h`rWTiWHx!8-#ZKJc#IRW|?3s;sc0TiBfEPJC@0 z^i8QisK!d)5WT?blD-=z4R&?T9GQFwu!_pLtW<&~;mYiOI~QcpjS|Ex_mljJe&-+* zR}H%zjy(tGNhM`ha##Thb}_e|9gzak(Fqpig$;^O1Q?Ch8wmPqr<%2;2$dc~SD1d6>u zb9PYJfI7zPH8Oztcc!MV0YMNB>fEtn-5Q$nnLqt353Bj_=OFtjr^&Fm7sFAfmA>rv z59mE5(`ZkaQ+;%W>HGWpUkM11R!&EbS$0lMB~?{%P?s_9?>NnIZ*exy9I-qz4#6QC z8?c*An!SHFVn`^R+RyvGd*r5W@9S0n`DszpZK`|ZQ3v^Y$63TTo@K||MzO>_iXy|# zY~763S%xrY>bmUIIS{(f=`*u*6!j)V;_9Q@joHe#3yHq*?Z@<^K`(xc?B!nmw!3;! zxqn&t*7n=?JE}bA7*@MYvDX&6e|@ZksIEs;&eKxM(|#SdOQ`yQOjEt5+c%GwUfFN} zP|eCSqU8sUP!(=Yl6U$){lGiMteQ5{SPcpmJr7N0 zJW+lz_zhAU5~U2ubdR7)@i(Emb2EW_@sj6dxCw1_Y-$~nJ9pZzylIu3 ztT2=3vvhCzvz<*Tyxhn-hVqwEyqvn-Ky))iytd(bL1E_+-Di^CuDXFV!KC%4><)9k zX@wx#%m(%|H?j@$KBMK~h{~r)Oe*|^Sxv@JL(({JMa=xch{a-a|KUA5!XIZ3O!uh! zz>2;1zlAEEMzg#1mKJa&RBjxS3=-n$XI;lTIT2A^51ICSRG(uz=6*~<4S#t=Io6Vq z6DFZu#^n%K*h_3|Y!nRQfN2{VDyqO=zf?l7$Ye?lntFkyX=(Gup0p0Etu0B4!x#32 zA|~Zr<)4$+KUK9;=QMNUOjP$0@Vi?34Q%Gt(!ljMLiGNddcJs^!|E+?b+iyh?VPwtB^ohx}3P_)KI097bB>yy8AYaINU=`<* zAqXu!-YE~ley~i4opnN^!uKKcwZ(p>#nhPm?}Z{YdV&Q51Q2%g29edzMmOaN@6Bx=1oAi446+omrW5H8%8KR zIm7Szb5Ri^BcW`3PcgIPMzpV#a8uH4J0AE7KQY%xd8)~=)384_6Mdc3c+uZCc#jL* zO?l=7{Up-|*5_qfL$57tG-tGnXn^~QBJv^9(-+F}c<)zL%y)GWaBE^- z|N3OOZ#Bp609tyY=)1S?0@2DmfTION1Oqj?d4#q44GX{b~pT~VEXmSjZE!YKwm zn!LDNZkSRo)RzixP}dG#-Oq)&xASlv_z_{JaT}1|_j|rP^P-J3If!K`GqQFw9VJc6 zC8N4o901kkNym>#kSB0(lLPwH#^1xkmwXS57ROwD>W|c2lHE?jDPW9Gv9Py99YpXA zeJ=RObtps;`Ed#7#I4Bi#v}zy=pRjaOY6B^n8fTJ%&0N`9bo@Tg_D<_p>MMunkGmJr_aVM zh@lMNJZR?!QWt-g!n&7DLX?zqEcV<>VHyQn#9yUEH;f>)wY6-Gi*n}_x+D8yg-3Qo zoxS%L%iAjMcInNxk#8%|k0V~Qx$)?_mA2ZjJ=U`&yeTH4Kln#W(H#D=v2DH7D>^!9^qk#Y z8--LJM52rI5tJZ6l z_`d8^mdW3=+tvCbn#>0_^%X@;0N_=5L(%G_NA7S%=SXLK!b{Tu<3LVMKJojvzWmbi zR`c}@bMs*YJ zF4AB?Mn*o~ss|^+vz)}wu(o3^$y4B^S5@0O`xYf3hY|?-P{(nox`bCq6GTQM@CgMOPwk|WH5rz zXv^vW9S&XuuO;REw8K!!LyyN@Hh}d;b|jrxBGN&PCI}}@>Py&>d?fDon0l_&18}lQTJZ_GF{0BLq z?ZZnZCiy!3hqmA$E02%UmzN%O_vzrno6gvTmm1uKVr>84UO3w)vSG-a>eZ`HVPR-% z)m_GIrf-cnNXoSsH_xuQ9?Ad(i!+=vg!I$EdFhc`1-)={b8~Gymtbc6hi9T;DT*m5 zY8^3FHPbe3<8Gjhg^gKqPi`zPe;Tv2QFC_`)|>^*?GdHp!ycFk!F!wqI5}AXG-0%i zE)yVJM@L6#YP5?rP%ue40}znPv>4@o>#<;w3xs84P^kLXeRJ^g;wvvN7dYy~!_qMr z{rZ3nD1i~qr}@{Jb(f31y;5tig^rF67Hw~81lPZ<2)PPrGJriG<8%1{n777@1@Gu& zI2~L^9%Lm)M@OufK7D%uLBY>aQPgkW{!%WeMeOVAi=+@vKi`{;|MI1^<>BsN;3s_A zrtx~au(PWRmxQEW-+f-(&5aB4{rh)}7hI8m8a@0Qh;7V4g24J+?(|?W!|>sHN`7B4 zjA1hOAY48wbGv0bkcN|!Ah7L2O;}dJ3=~TauYgwdccKG zlZxGV9>;W=3exP z=d}J^X594z6xyDrW0HH@Zu@h6fc!$o#)|jp~>A?ajfH>f_1-;Kr`{Jnk zSBiiBT>fs6{=<5zBq${07oe)E?dJQt0NAMJ%C(J*#F!0yMTp9Sh>3}P=e8#Yl&rvf zy(>&LPvOzj--gU{V)D|x!hzERlJ^p@YiO95YnmaaTmpX;qW3}V)R(NQJ9>V9Xp zz*_XXVFE-S5K{CCbQ|0rf0s+)8rpYc)T{kcTgwBUp;hZZIa_VJ@>SRFk;F#Fqj}?R zX1Kdd*K$2;-*|*lX)%%u-v0Xa>%;wq1FmWE?FHhy+uPcM=9@>a6iQ0DO07-fcXwJJ zd`_c~+RJ~?2)mUl)+*1Mgj4w6vOx}-F31@)ivEHZECI578JPHy%mi;I_q#NL0Q~-F zP`Xj-f5-5YknyE}K;zhbP%X&}MlgfyPdSfB3C zrvUx+`@{W33*_;8Hl7Ug2L%TowIX5TJbNbU;!^diR3@k0xcrgJt+t)*9#<3|y1hEF z8U}pobgd&5n2V5rjg>jA$p~Go;z59-Z~K+SFghuT2#;35^k}&Yz&_WsqLmfh$;L-^VicyUw3x~Q#97Yl_|hD~0avo-dQegpKy`5$JgtWx>iqaXbMK;!Hr2#?2&+#e^k zuo`!wfek4D@Qfc>GZ(shDJV!50MsZ*TwL7lzP-FHzzsml0dsQ9kb@)p`3ZqQhx~(z zTIq3QT&2ULD0G#59GZ-yshg64s!s6+Q&Rq{rlY+*EG(?mh6|=qC@mw?{(<#NTpUnH zs$VF>xxQ@^5D_7{QXxvJdayJeB4P_1LV98-EdG=kf$%{*?4!;2z(L}<8Y>BmMFZU2 z=R( zlK7p3goJ8?odyVB5Ut|H8qUDQo3FPSfXf;YZKiP9E}u;q=GiYc7wN#Cfa(L(^p)VF ztLy7y284!&9s^fW@mtqb)m2r6&7ryqS1MiHRDNhf1ZvbUh=R{RwE_}R$7?!vpTz1m0#KqS`qx)C8{hEP6woYGL`?E$NH3P#_08rfF z(4VDWkI&AK|IXDpW6?hX3*_?V2I&n|<8?vvqqhSD$0HyJ?lPtn7R~@}X;Q9*_VlUs z+f)eXtLhL(-KnnN)6g0@?)fEB}xvY^9Wq{N3g6sO+O&30mVjXccHGB|%)O2*8 z!4se7zo4sYXsC4C)8Vq632OWB;reu|wxNMxPhU?jp-1iQ00ZOh=_Kga zV9y@?U~>$11;pD+AdA7~7U!Jnw1L=*0HO;qQ?6}NaU7w)Tviee%mZXW;CExIZeW^Y zd1d7aNTSpAt_%Pgax*g_0YHyrclaw39ucwUhZ7ZEze>i~ye0arrLy*4Zx$WbYHOahRLMStN+^C9E5PsnoLSU++vcyzqm;k%e~ z#wLjhi4ng41`d%9sG`S6`ttQ_h(@8=T&*Ke<(xOt+$$`{UePF~4^A2S7gvE;*f{E; z0Lu$)HeBrP`*RWuXu8n$Aasbp{pHFd6h{$yaU{gYcO=lMYSp{Y0fn`#s|y*B{iHPo zhBA__a;ax0Ar^x1=(0dY{n$h9Eyr8p zc`@(KFl%CL+&eJfti^9JOq#h5&j3jjU@I*RO^Dy!HQfzRjQdC2AMWoG$4=7)y?J~t zS%DraAH5J%t7l8DbOuYmZnfgE-PqfU0_@2AJejbtFsQbHQQ?J!*i{411t(Jky}1UG zSZLYUVnK6J2BHA`htDxF?i2X=DPMJJ?8tz*7_}>chzvYF0$Pn;^`m|B6^;9%FE_|T zjE2prSy@>$dB=cc>5o?wl2%Z_iV?crG!x7K=ZKuHv;f6!=M3CBaYa!+s@8>%40NWC zkF&_|B{VcNT=3i=6BQMO5OY}K=r_1^LE&5^f_J5$>@Np^2%P;9e(SFB84`SZess(*AzQ?JF~LbNUNzy_dK|M3OzxWQlG7`dpJ+&L5^8 z3m{Mof9J9#IcPkIhB@&AN%M4XmZ-@<5o9Z%Ttz3NISy5=-$~4UE19Mjg6l-}nL=GK z+ATTal9D7149KBCFDttac>7%rkeoRg1J7OOq!zpn1kVo^6)*PZfmYeCasH&^4q_olLDk>O^%b-3E3xE+{=;X^@Qaq##|NVS9(4Enruw=YsvSTj3uEhk;U)K4T rIQ)NxZ~pf{@c)|2`TxC?^MFl5m_fn)8u|d{sUgzh3hzrqKYaQxldZ;v diff --git a/pictures/releases/0.9.6/scale.png b/pictures/releases/0.9.6/scale.png deleted file mode 100644 index ade3c67fd0dc2898e083abfad24aa7c8c6e1e6a7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16421 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�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

(th5;L6dE)=-6Y zK7+P{SnAbwHz_zkV3Bft{fDCqAO~bBjZ1arKb)^9L;s-tnGE@m0ukWxzy}!(fA=kj z2wYuNnE)t5+B63aNm#Mo{R`%yl-=dTy4LbVoJif>9cQrDc9Uj}?y2KoKV=h_HXl1X z`>gsM1qB6wmw%)!y#?|Ks<=iE!LtaSUUpt*Cns6R8gb`3z=TclI#SaQm6=cOmJiRU z6d0$bg;00kECtgA0WmP_w#!3t+MXvsh9b?0nD`93pprrQE|3{;24~m6KV%kD=UCq1 zK*H|%m%m2_MKNa*p!p~shrfw*?S`3SaDMp1K@D=TQQ}rPR|m3RRy9`BX>#B|r4iSk z@Id6)wf&VB4tXuoQQAFQodjJQz@u~b9g2`hE;NeaD%@u=8my6)M%JZv;Ns7`6p&IU z8UMG1b@^bdSh8T20<>`Kf}V?#17?%eu|+0>-{=a!j4(1Xa^xX?<4XjLDCW0IufBc_ z0aHK%Fu+Sg8xQ_tBmn{9I+S$V<&f;VKvg*$8mBy#7$@me+kY0V?wF32>q9i1;GBlEu4r9EdX`<)hTd&mVvBOL!E~uEA0As zn{)5{AE)~v7bB}4c8N&D_l ztbaXT1t0%5m<%#nm~k8&AezP6toOTyx-d4;?{G72qtU!4F4m=uaLbYPB!pfrPq-jv ztIt{;iKm_7c5|%lt(l!Nfm64(W#ub|!r~d#{<1;W@3w)~xoyu-dDJsk--_)Nhipf_ zH-}tu8U0F1N?Il+pCLV6B?}*1&q%`(Cy+df-5kbba#zDdHf8;`U1yUK9b(X+V&yOuf4v; z^FOa)l31HveGMvG{La{xB=q=#nbm%1#Kfi=@ON3Jdz!LoX=|(ZE7YiVmv>R~-2doF z@8E*_^L{J#s>q+YZhQ-abRlMYaoH}h>FxB?X$S<~3(*$>^76EFbac7-`M)5%;p})< z7rMyA#*)DqYpl`VC+YMtQ^2FFrlu&f)vCPew5A}rE#_$5fw(^>!Xz4$G7s74XT?1JHah8_UU-8+ z52nlgr@Um~VGz6fM9f-NxMPFc{Jaah!T6S_&! zGv%G7<^7CvnbOWRQj^oZ#G>x`>Chk(~>W!-Ihg_XYpgG&U zL!dfNP-3EJ3?L4)Xlcqf-5nh+z#`}KOCk~F;O6elbtlts%y#x76gQ@|Szwq^d;2&L zJMb>ULj89KG=zs)Jfg~_vMw~e_sB=yK=Sk>)G_)b@rqo97WGLqa>LcSk1Y0AL+0m1 z(73r#`Ib9=Pu-xO_%;+79HXPIIY!0rN$WWKy{HzPAjL*K^ZG<7Ux?)i(0bz48kv%K#W$_V-SK9p2g@P*DRbWfjVH_O8Z zD=#n14^KQ~|N44rV?*lo>tLD4=BpsYx9vjrG|#wuyG$B_6@*MpGdnE);5ogPpvI*R z@nzO;CH!O{TJpxvXG{bV7oK6|*$USD67T0LfMwrCpl@O8ZOc0b(b%j#fGfyE>r9O8W-;^Gq~0qQ+kTEEg# z4*Sgs$&PQ&tnBUChI;C!WtO@U|9Dp#0NNFWQ9?lyMG)l0XMc9BzRCa5)^0z-c6~U) z;pA{ON(}F&N-Mu)bG)wv7WU#y(VWDpU&Y*_C&ol5?cV0`bKkjv{nbHE=e@@aReN{L z&CS_#b#!%yD;yXgm&a)dElnhWA4i;`czvklfT3olKsuH16LI+<6CZ@=E!q&GViQTN zpIi`Wv?*pW8KXYW*-)pE8|gYZ1%e~r|D5XRVbSWTT&gw#5)~57nzE4w?Pzw3E4i-y z#1j-;J;WAyN`$=b_W|RlYS2Ns$HesI<)x}{D!X&{z3-d>nr@&pI^8|5K94$(>8c^> zP%Sb1J%m$c%h_H&4t*SU`Ufk@R2Mg@hIiLB5U7>xkz6;gzCCv)aObfleB}}kee~kf z$)59G8uS?vK9BW6VRhesc5>4(D7K;|k0_envW*SZjq!dxGKyp6nEip}>H${U?Cfm1 z`eghQ#rrIcO^vb@)%?)m{rQMX6^wjru9>KIJIyDHM8F1>{w4$e^l-C_8CB~K+Jht0 zfibK$!Ol-T@XnxXQ!Z>(bsDC4#hrr85j*<$5ep8w>Rm(VAq8*kEKK()xwyVTzV_;7 z*|Jdia>2r%e07mkrG4p;*^emSEpj&2j*T<+`ym=Qks8tGI_(5$ekeD1FVWf*4C3Fy z8d+et$YyUkQ9Jad{>XmwFjJ{&z+kPAr~rKDy6rwV&4w0TFdUY9FUbNgTw??_vG2iX z%2X-#sphSK=gQE_XK#k1SGPulaOF7GjXrH|EtIC7ou6;uwy0{c=~7_9K-21MH(DDm z3z!#Q=q>c+kMH=_R{2ugw6U?_yalpf9T&6QiNY<;cb+TShz< zRM?Amz7DxkvQ>2WoSDS(x`g}TJu%=eO<{?W2xZWW3l0vZ(>>W=Zh@S~8$mv~^wBIH zx53&1t^e*T7q>le8jrVaVV8rDxM~@txZHm4_d;GhC;L)`?p3&(9*kqCEn|%k>h1&%VGlp)%!^0?++dWjMSfql`uWsp25n8GTZ%Bgr>R^TC zQnU2O7)<@GTp7R~dr$W1_ei$lvc{D-IXGTlY+Lua=TX#4AO9>;qQg&dXMHb*7yqjv8A0vf@bgWUM4k>`R55g*^NuKkL)5dWAWQ=0tQt)W z7w4W*86jaDl3#vrOjzDrM-;o4d5TFZCPhlitmcC|?MjLHV#frpPe8zIUrvp_aRiB! zlvL%Gnw?n0&TMsYTPItA39tSO+=VdDP`n$pvbLjd8QfoX5 z9O7T|xrJS&4LV3fuyUXpS5l=gMYlwQl=H+U{Io?(G+8=Y7_9<%^4oBxRQK}$ zcMF^((a!^grh{MfpD2W!5d4gGIMkjBPKe>S?H`}R;9gw~{-FCpJ;A+vYZJRq=i9=P zoeHR#iPP_ugoqGyjlBg7~HjnI+Fc+05(fJ=+-brkd{=LKg=CO4c;}EbBtv*Z? zvaMYawOxu#yEPQM`|K{Jjo~}H^RX8evPnMds|puEQN4V5L=p6$mePIXuHEI}0sy?V z7uaGgrbesy8TDsMu?Bcp%(?OzLdGPw{l-kP3Ip+{g|eS-XrX^Vqc#b} zM+#5u-LZlu=LsIU~94i1P;Z-Q1iWVBF0V1oX1WJ1mIOGQ1zIW!Bfo zxW1Z=iVuFx__l8kkiX4jIehY4)2-JsZ7RsO)C^Jiq*xKF3b**y25hv(`Z`(-_ z^MA9}xAq{YJzv5>#!FfpEVs-_Ux@DhsK9(6PuYTb>L&~>o<)Y5%1a6VpFBIA7w1i6H++kAYLL`K=9WyFC3zeRp-1o;~t{f2V6EAXIM6bfn@x3 z^KIHOoTuEp<#vtT-M2$pUaW>+GL6U=-@(OAU!3%ZNzQ0Df~8^79fP04R+P=}w89Eo z)3meBeS3#0D;dQ^mY6SS=EK6h_-1cMen@(i;Vhq~!3zzZj@}_55DChTJ%R?|n5v5e zV>EkbPG@J#?%qyQ;bwn#FAUJsK8)KewO@BmDHg^?CHoQQ!9skE)eyQuNknl3GAD~G z4@SV~QCm7TEe?)yaYu8Oe=Lw=;ZDiSwEOVY?XpWA9*IJe@vjiMxnl_7y5CR{xo%BT zXC%*RyGkB{5TVel|0~F%ZK#+r7z(0*2Prn6tb-FEKr>3&wAT++p&IYFat5NF$PV#R z@Vh`eS{Vp)@rGWP4SvUevL&AsyB)t-2spp#j>FlM+@=j{4C-}^yWcn3iiY#g#SyS* zc}sWScg0C`kx=*nEO&(fW z3<3gz#McPu^0A$3m07A(rMR7HGBBosPT$J5$YSec9nA%R$9eslXOXP)G-DukrA=BFy z4A~1)?U&{b2X*!Jo)C0uvvy=97Wx>a7EX8&uSUF4vcNHyg!3Mf0bmgBDy*Zt5|e5y z`i==X0{7ypcneZJTK)P>mb+8=yX1d@n#^sx{8?Zer%U)`XUu&1qxjlxCPg9pxVAT+ z>cW40;)M+L=GW9>4E;l)Xu4Su!xHbWTqoo;z41uuT7i2#^=i3chJry;r}}_E!SnZ5 z9G#+QY@2Fbpxd3|NRd&)e6FQnXY#o9j9lRj0tWTYTM^KN;&<4S$PP}1GNR$rdcJJ+ z3iB#b%SRDcuD|lB;bn1LJs?BwWyn=zyHc4hj{S4VX-!LtssqPeTdRcE34&@E5Vl$W z>2pML{ABln%WF{M5)b6GLGUCqU6Ayv?^<=q|ok9bWSsB_4{Vvtv%n)rSKeS-0F= z>SYM6YRbLpJ#U&%yFh1SjsAJ@$ABumLgr^O0}7zAeQMdXjO`(E>_19cnRp$b@q;Y zk?S4=kU)5(r(~dZa_7*{lu}t`PLnWSp3l-YeTpC94+^6VU}HEN-rL(m$QPK z;Wx4T?!9&AljYu2nl~#4$@ODz*CvDEWN3Z==phgdW3R;SAI;I+%W8F+o}ogFNp2h+ z-JLHFD7zv|{=!4UtcTEofq~6*#8h3OLO~K*8x7~#5YXUbM$mibIgdNT{;FRugh^6bVv`s%x)zKP@_?dhkP|?RGdZ z%s}`>LH|!iyPa#RwWkN)Db>oXKN+zY%mTr*Z)}8ncM*OM)Z1C7&WYk8@W^jGk9WU{ zcsw;Dc-+*+ym#)9ey$^Fny!&W3x~kn5f2Tjr9Zc2>F|F}yzzy;4?o<;BZdq0Nu)zP zVvt?9P3Mih0?3=7O|GM3*1w>gAw6tJ(GFUp@G#Ey$ zt?LYYFYV02#PQ*C2?WGGoRkWLO4RW6bqms+qdS4kiAe?>-`2(}B2d2p>WTZ42?cPujHQ&{QwF?}PElb4qk5rY2oWndJ zfphFt-CSr8L3?)LU}C~c>xWSIo}V7dRl7PtjhIEX(cJtBssD95VFihNElhhYaU5@` z#e)PXHf(crT`eth?|6BSH`YESy}|Vxv^fESyD#9}$$MJeP#E-R*wy@#&lo=?B_%}F zs95ywZf(_?vMrXIUgkR9J=uM{hqB@3uZV;>b{H(-?Xs)I1p&XgsQMT9=-*-Qie0i;||On$K0)_aop9s&PEUrN2m7S#Ii-vHFV B>9zm> diff --git a/pictures/releases/0.9.7/flatpak_refs.png b/pictures/releases/0.9.7/flatpak_refs.png deleted file mode 100644 index 299a50e47b97a4fa540c31d5e59d213cf88a08e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 26246 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

wWNii*N+OJ7@FUyp#pxj^rj(3rX;UhkS{tMI~5 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 K3Rei~|M)L7)X@k4 diff --git a/pictures/releases/0.9.7/unmark_pkgbuild.png b/pictures/releases/0.9.7/unmark_pkgbuild.png deleted file mode 100644 index 6e356129ab85824d8ed756ce640fbdbb9e1376ed..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7561 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 From 11540cf1e7414087899353860a03b67f1f346472 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 17 Aug 2020 15:14:12 -0300 Subject: [PATCH 26/99] [arch] fix -> AUR: multi-threaded download: not retrieving correctly some source files URLs --- CHANGELOG.md | 4 +++- bauh/gems/arch/aur.py | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7f6f48..c251de9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,7 +55,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - not displaying all packages that must be uninstalled - displaying "required size" for packages that must be uninstalled - some conflict resolution scenarios when upgrading several packages - - AUR: info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade + - AUR: + - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) + - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index 8206e0b0..f6678c8b 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -52,10 +52,9 @@ def map_srcinfo(string: str, fields: Set[str] = None) -> dict: else: field_re = RE_SRCINFO_KEYS - for match in field_re.finditer(string): - field = RE_SPLIT_DEP.split(match.group(0)) - key = field[0].strip() - val = field[1].strip() + for tupl in field_re.findall(string): + key = tupl[0].strip() + val = tupl[1].strip() if key not in info: info[key] = [val] if key in KNOWN_LIST_FIELDS else val @@ -65,6 +64,11 @@ def map_srcinfo(string: str, fields: Set[str] = None) -> dict: info[key].append(val) + pkgname = info.get('pkgname') + + if isinstance(pkgname, list): + info['pkgname'] = pkgname[0] + return info From 7e0ee26db1ae8d20c7bb34171edfa42742543191 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 17 Aug 2020 17:59:28 -0300 Subject: [PATCH 27/99] [arch] fix -> AUR: importing PGP keys --- CHANGELOG.md | 1 + bauh/gems/arch/__init__.py | 1 + bauh/gems/arch/controller.py | 8 ++++++-- bauh/gems/arch/gpg.py | 13 +++++++++++-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c251de9f..8b04fae5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - AUR: - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) + - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X diff --git a/bauh/gems/arch/__init__.py b/bauh/gems/arch/__init__.py index 6a1cb5c2..8471a7d6 100644 --- a/bauh/gems/arch/__init__.py +++ b/bauh/gems/arch/__init__.py @@ -10,6 +10,7 @@ ARCH_CACHE_PATH = CACHE_PATH + '/arch' CATEGORIES_FILE_PATH = ARCH_CACHE_PATH + '/categories.txt' URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/arch/categories.txt' +URL_GPG_SERVERS = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/arch/gpgservers.txt' CONFIG_DIR = '{}/.config/bauh/arch'.format(str(Path.home())) CUSTOM_MAKEPKG_FILE = '{}/makepkg.conf'.format(CONFIG_DIR) AUR_INDEX_FILE = '{}/arch.txt'.format(BUILD_DIR) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 06a3fe98..598f0858 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -33,7 +33,7 @@ from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, message, confirmation, disk, git, \ gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ CONFIG_FILE, get_icon_path, database, mirrors, sorting, cpu_manager, ARCH_CACHE_PATH, UPDATES_IGNORED_FILE, \ - CONFIG_DIR, EDITABLE_PKGBUILDS_FILE + CONFIG_DIR, EDITABLE_PKGBUILDS_FILE, URL_GPG_SERVERS from bauh.gems.arch.aur import AURClient from bauh.gems.arch.config import read_config from bauh.gems.arch.dependencies import DependenciesAnalyser @@ -1789,7 +1789,11 @@ def _handle_aur_package_deps_and_keys(self, context: TransactionContext) -> bool body=self.i18n['arch.install.aur.unknown_key.body'].format(bold(context.name), bold(check_res['gpg_key']))): context.watcher.change_substatus(self.i18n['arch.aur.install.unknown_key.status'].format(bold(check_res['gpg_key']))) self.logger.info("Importing GPG key {}".format(check_res['gpg_key'])) - if not context.handler.handle(gpg.receive_key(check_res['gpg_key'])): + + gpg_res = self.context.http_client.get(URL_GPG_SERVERS) + gpg_server = gpg_res.text.split('\n')[0] if gpg_res else None + + if not context.handler.handle(gpg.receive_key(check_res['gpg_key'], gpg_server)): self.logger.error("An error occurred while importing the GPG key {}".format(check_res['gpg_key'])) context.watcher.show_message(title=self.i18n['error'].capitalize(), body=self.i18n['arch.aur.install.unknown_key.receive_error'].format(bold(check_res['gpg_key']))) diff --git a/bauh/gems/arch/gpg.py b/bauh/gems/arch/gpg.py index 7b9e3378..01c7235c 100644 --- a/bauh/gems/arch/gpg.py +++ b/bauh/gems/arch/gpg.py @@ -1,5 +1,14 @@ +from typing import Optional + from bauh.commons.system import SystemProcess, new_subprocess -def receive_key(key: str) -> SystemProcess: - return SystemProcess(new_subprocess(['gpg', '--recv-key', key]), check_error_output=False) +def receive_key(key: str, server: Optional[str] = None) -> SystemProcess: + cmd = ['gpg'] + + if server: + cmd.extend(['--keyserver', server]) + + cmd.extend(['--recv-key', key]) + + return SystemProcess(new_subprocess(cmd), check_error_output=False) From 846eb567fc44f53f253b6c9de02e0543b73c5ebc Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 17 Aug 2020 19:55:35 -0300 Subject: [PATCH 28/99] Update CHANGELOG --- CHANGELOG.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b04fae5..7bcaf83f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Features - Arch - AUR: - - allowing to edit the PKGBUILD file of a package to be installed/upgraded/downgraded. If enabled, a popup will be displayed during this acctions allowing the PKGBUILD to be edited. + - allowing to edit the PKGBUILD file of a package to be installed/upgraded/downgraded. If enabled, a popup will be displayed during this actions allowing the PKGBUILD to be edited.

@@ -26,7 +26,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Manual file installation/upgrade: - default search path set to '~/Downloads' - trying to auto-fill the 'Name' and 'Version' fields - - Arch - upgrade summary: displaying the reason a given package must be installed

@@ -35,8 +34,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - upgrade: - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues - only removing packages after downloading the required ones - - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. - - AUR: caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) + - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. + - AUR: caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) - Flatpak - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) @@ -46,19 +45,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - AppImage - - manual file installation: + - manual file installation - crashing the application icon is not on the extracted folder root path [#132](https://github.com/vinifmor/bauh/issues/132) - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) - downloading some AUR packages sources twice when multi-threaded download is enabled - - upgrade summary: + - upgrade summary - not displaying all packages that must be uninstalled - displaying "required size" for packages that must be uninstalled - some conflict resolution scenarios when upgrading several packages - - AUR: + - AUR - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) - - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) + - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server address is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X From befcc79cb0341ab780d599ccb02b09284d0777f3 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 18 Aug 2020 18:15:49 -0300 Subject: [PATCH 29/99] [arch] improvement -> AUR: new settings property 'aur_build_dir' --- CHANGELOG.md | 10 ++++++++-- README.md | 1 + bauh/api/abstract/view.py | 10 +++++++++- bauh/gems/arch/__init__.py | 1 - bauh/gems/arch/config.py | 17 +++++++++++++++-- bauh/gems/arch/controller.py | 28 ++++++++++++++++++++-------- bauh/gems/arch/pacman.py | 5 ++--- bauh/gems/arch/resources/locale/ca | 2 ++ bauh/gems/arch/resources/locale/de | 2 ++ bauh/gems/arch/resources/locale/en | 2 ++ bauh/gems/arch/resources/locale/es | 2 ++ bauh/gems/arch/resources/locale/it | 2 ++ bauh/gems/arch/resources/locale/pt | 2 ++ bauh/gems/arch/resources/locale/ru | 2 ++ bauh/gems/arch/resources/locale/tr | 2 ++ bauh/gems/arch/worker.py | 4 ++-- bauh/view/qt/components.py | 5 +++-- 17 files changed, 76 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bcaf83f..2bd38056 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,8 +34,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - upgrade: - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues - only removing packages after downloading the required ones - - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. - - AUR: caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) + - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. + - AUR: + - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) + - new settings property **aur_build_dir** -> it allows to define a custom build dir. +

+ +

+ - preventing a possible error when the optional deps of a given package cannot be found - Flatpak - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) diff --git a/README.md b/README.md index 342ab957..09d02cb9 100644 --- a/README.md +++ b/README.md @@ -194,6 +194,7 @@ repositories: true # allows to manage packages from the configured repositories repositories_mthread_download: false # enable multi-threaded download for repository packages if aria2/axel is installed automatch_providers: true # if a possible provider for a given package dependency exactly matches its name, it will be chosen instead of asking for the user to decide (false). edit_aur_pkgbuild: false # if the AUR PKGBUILD file should be displayed for edition before the make process. true (PKGBUILD will always be displayed for edition), false (PKGBUILD never will be displayed), null (a popup will ask if the user want to edit the PKGBUILD) +aur_build_dir: null # defines a custom build directory for AUR packages (a null value will point to /tmp/bauh/arch (non-root user) or /tmp/bauh_root/arch (root user)) ``` - Required dependencies: - **pacman** diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index fe39b081..06ead95c 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -156,7 +156,8 @@ class TextInputType(Enum): class TextInputComponent(ViewComponent): def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip: str = None, read_only: bool =False, - id_: str = None, only_int: bool = False, max_width: int = -1, type_: TextInputType = TextInputType.SINGLE_LINE): + id_: str = None, only_int: bool = False, max_width: int = -1, type_: TextInputType = TextInputType.SINGLE_LINE, + capitalize_label: bool = True): super(TextInputComponent, self).__init__(id_=id_) self.label = label self.value = value @@ -166,6 +167,7 @@ def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip self.only_int = only_int self.max_width = max_width self.type = type_ + self.capitalize_label = capitalize_label def get_value(self) -> str: if self.value is not None: @@ -189,6 +191,12 @@ def get_int_value(self) -> int: if val: return int(self.value) + def get_label(self) -> str: + if not self.label: + return '' + else: + return self.label.capitalize() if self.capitalize_label else self.label + class FormComponent(ViewComponent): diff --git a/bauh/gems/arch/__init__.py b/bauh/gems/arch/__init__.py index 8471a7d6..9751ae48 100644 --- a/bauh/gems/arch/__init__.py +++ b/bauh/gems/arch/__init__.py @@ -6,7 +6,6 @@ ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) BUILD_DIR = '{}/arch'.format(TEMP_DIR) -PACKAGE_CACHE_DIR = '{}/pkg_cache'.format(BUILD_DIR) ARCH_CACHE_PATH = CACHE_PATH + '/arch' CATEGORIES_FILE_PATH = ARCH_CACHE_PATH + '/categories.txt' URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/arch/categories.txt' diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index f2d6b5a1..0cae1f54 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -1,5 +1,7 @@ +from pathlib import Path + from bauh.commons.config import read_config as read -from bauh.gems.arch import CONFIG_FILE +from bauh.gems.arch import CONFIG_FILE, BUILD_DIR def read_config(update_file: bool = False) -> dict: @@ -13,5 +15,16 @@ def read_config(update_file: bool = False) -> dict: 'mirrors_sort_limit': 5, 'repositories_mthread_download': False, 'automatch_providers': True, - 'edit_aur_pkgbuild': False} + 'edit_aur_pkgbuild': False, + 'aur_build_dir': None} return read(CONFIG_FILE, template, update_file=update_file) + + +def get_build_dir(arch_config: dict) -> str: + build_dir = arch_config.get('aur_build_dir') + + if not build_dir: + build_dir = BUILD_DIR + + 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 598f0858..db615b10 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -30,12 +30,12 @@ from bauh.commons.html import bold from bauh.commons.system import SystemProcess, ProcessHandler, new_subprocess, run_cmd, SimpleProcess from bauh.commons.view_utils import new_select -from bauh.gems.arch import BUILD_DIR, aur, pacman, makepkg, message, confirmation, disk, git, \ +from bauh.gems.arch import aur, pacman, makepkg, message, confirmation, disk, git, \ gpg, URL_CATEGORIES_FILE, CATEGORIES_FILE_PATH, CUSTOM_MAKEPKG_FILE, SUGGESTIONS_FILE, \ CONFIG_FILE, get_icon_path, database, mirrors, sorting, cpu_manager, ARCH_CACHE_PATH, UPDATES_IGNORED_FILE, \ - CONFIG_DIR, EDITABLE_PKGBUILDS_FILE, URL_GPG_SERVERS + CONFIG_DIR, EDITABLE_PKGBUILDS_FILE, URL_GPG_SERVERS, BUILD_DIR from bauh.gems.arch.aur import AURClient -from bauh.gems.arch.config import read_config +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 @@ -571,7 +571,7 @@ def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_app return SearchResult(pkgs, None, len(pkgs)) def _downgrade_aur_pkg(self, context: TransactionContext): - context.build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) + context.build_dir = '{}/build_{}'.format(get_build_dir(context.config), int(time.time())) try: if not os.path.exists(context.build_dir): @@ -1318,7 +1318,8 @@ def get_info(self, pkg: ArchPackage) -> dict: return self._get_info_repo_pkg(pkg) def _get_history_aur_pkg(self, pkg: ArchPackage) -> PackageHistory: - temp_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) + arch_config = read_config() + temp_dir = '{}/build_{}'.format(get_build_dir(arch_config), int(time.time())) try: Path(temp_dir).mkdir(parents=True) @@ -1814,7 +1815,7 @@ def _handle_aur_package_deps_and_keys(self, context: TransactionContext) -> bool return True def _install_optdeps(self, context: TransactionContext) -> bool: - odeps = pacman.map_optional_deps({context.name}, remote=False, not_installed=True)[context.name] + odeps = pacman.map_optional_deps({context.name}, remote=False, not_installed=True).get(context.name) if not odeps: return True @@ -2107,7 +2108,7 @@ def _import_pgp_keys(self, pkgname: str, root_password: str, handler: ProcessHan def _install_from_aur(self, context: TransactionContext) -> bool: self._optimize_makepkg(context.config, context.watcher) - context.build_dir = '{}/build_{}'.format(BUILD_DIR, int(time.time())) + context.build_dir = '{}/build_{}'.format(get_build_dir(context.config), int(time.time())) try: if not os.path.exists(context.build_dir): @@ -2484,7 +2485,14 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: value=local_config['edit_aur_pkgbuild'], max_width=max_width, type_=SelectViewType.RADIO, - capitalize_label=False) + capitalize_label=False), + TextInputComponent(id_='aur_build_dir', + label=self.i18n['arch.config.aur_build_dir'], + tooltip=self.i18n['arch.config.aur_build_dir.tip'].format(BUILD_DIR), + only_int=False, + max_width=max_width, + value=local_config.get('aur_build_dir', ''), + capitalize_label=False) ] @@ -2505,6 +2513,10 @@ def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: config['repositories_mthread_download'] = form_install.get_component('mthread_download').get_selected() config['automatch_providers'] = form_install.get_component('autoprovs').get_selected() config['edit_aur_pkgbuild'] = form_install.get_component('edit_aur_pkgbuild').get_selected() + config['aur_build_dir'] = form_install.get_component('aur_build_dir').get_value().strip() + + if not config['aur_build_dir']: + config['aur_build_dir'] = None try: save_config(config, CONFIG_FILE) diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 1c7b7aa0..f4474115 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -754,9 +754,8 @@ def remove_several(pkgnames: Iterable[str], root_password: str, skip_checks: boo def map_optional_deps(names: Iterable[str], remote: bool, not_installed: bool = False) -> Dict[str, Dict[str, str]]: output = run_cmd('pacman -{}i {}'.format('S' if remote else 'Q', ' '.join(names))) - + res = {} if output: - res = {} latest_name, deps = None, None for l in output.split('\n'): @@ -801,7 +800,7 @@ def map_optional_deps(names: Iterable[str], remote: bool, not_installed: bool = sev_deps = {dep.strip(): '' for dep in l.split(' ') if dep and (not not_installed or '[installed]' not in dep)} deps.update(sev_deps) - return res + return res def map_all_deps(names: Iterable[str], only_installed: bool = False) -> Dict[str, Set[str]]: diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index a8b0ae4d..6b46ef39 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -30,6 +30,8 @@ arch.config.aur=AUR packages arch.config.aur.tip=It allows to manage AUR packages arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. +arch.config.aur_build_dir=Build directory (AUR) +arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. arch.config.clean_cache=Elimina les versions antigues arch.config.clean_cache.tip=Si cal eliminar les versions antigues d'un paquet emmagatzemat al disc durant la desinstal·lació arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index add22c05..caacf7d8 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -30,6 +30,8 @@ arch.config.aur=AUR packages arch.config.aur.tip=It allows to manage AUR packages arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. +arch.config.aur_build_dir=Build directory (AUR) +arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 581655f1..f530e0c1 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -31,6 +31,8 @@ arch.config.aur=AUR packages arch.config.aur.tip=It allows to manage AUR packages arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. +arch.config.aur_build_dir=Build directory (AUR) +arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 01ffa1fe..a9f3f302 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -30,6 +30,8 @@ arch.config.aur=Paquetes de AUR arch.config.aur.tip=Permite gestionar paquetes del AUR arch.config.automatch_providers=Autodefinir proveedores de dependencia arch.config.automatch_providers.tip=Elige automáticamente qué proveedor se usará para una dependencia de paquete cuando ambos nombres son iguales. +arch.config.aur_build_dir=Directorio de compilación (AUR) +arch.config.aur_build_dir.tip=Define un directorio personalizado donde se construirán los paquetes AUR. Defecto: {}. arch.config.clean_cache=Eliminar versiones antiguas arch.config.clean_cache.tip=Si las versiones antiguas de un paquete almacenado en el disco deben ser eliminadas durante la desinstalación arch.config.edit_aur_pkgbuild=Editar PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 50301ffb..a74eb7f2 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -30,6 +30,8 @@ arch.config.aur=AUR packages arch.config.aur.tip=It allows to manage AUR packages arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. +arch.config.aur_build_dir=Build directory (AUR) +arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. arch.config.clean_cache=Rimuovi le vecchie versioni arch.config.clean_cache.tip=Se le vecchie versioni di un pacchetto memorizzate sul disco devono essere rimosse durante la disinstallazione arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 109e02ad..9b07819c 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -31,6 +31,8 @@ arch.config.aur=Pacotes do AUR arch.config.aur.tip=Permite gerenciar pacotes dos AUR arch.config.automatch_providers=Auto-definir provedores de dependências arch.config.automatch_providers.tip=Escolhe automaticamente qual provedor será utilizado para determinada dependência de um pacote caso os nomes de ambos sejam iguais. +arch.config.aur_build_dir=Diretório de construção (AUR) +arch.config.aur_build_dir.tip=Define um diretório personalizado onde pacotes do AUR serão construídos. Padrão: {}. arch.config.clean_cache=Remover versões antigas arch.config.clean_cache.tip=Se versões antigas de um pacote armazenadas em disco devem ser removidas durante a desinstalação arch.config.edit_aur_pkgbuild=Editar PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index cd80b584..2604e90a 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -30,6 +30,8 @@ arch.config.aur=пакеты AUR arch.config.aur.tip=Это позволяет управлять пакетами AUR arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. +arch.config.aur_build_dir=Build directory (AUR) +arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index ce5ed856..9e50bd93 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -30,6 +30,8 @@ arch.config.aur=AUR paketleri arch.config.aur.tip=AUR paketlerinin yönetilmesine izin verir arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. +arch.config.aur_build_dir=Build directory (AUR) +arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. arch.config.clean_cache=Önbelleği temizle arch.config.clean_cache.tip=Disk üzerinde kurulu bir paketin eski sürümlerinin kaldırma sırasında kaldırılıp kaldırılmayacağı arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index ea8487ac..29261571 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -13,8 +13,8 @@ from bauh.api.abstract.handler import TaskManager from bauh.commons.html import bold from bauh.commons.system import run_cmd, new_root_subprocess, ProcessHandler -from bauh.gems.arch import pacman, disk, CUSTOM_MAKEPKG_FILE, CONFIG_DIR, BUILD_DIR, \ - AUR_INDEX_FILE, get_icon_path, database, mirrors, ARCH_CACHE_PATH +from bauh.gems.arch import pacman, disk, CUSTOM_MAKEPKG_FILE, CONFIG_DIR, AUR_INDEX_FILE, get_icon_path, database, \ + mirrors, ARCH_CACHE_PATH, BUILD_DIR from bauh.gems.arch.aur import URL_INDEX from bauh.view.util.translation import I18n diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 006d4f95..616e754c 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -461,7 +461,8 @@ def __init__(self, model: TextInputComponent): self.model = model self.setLayout(QGridLayout()) self.setStyleSheet('QGridLayout {margin-left: 0} QLabel { font-weight: bold}') - self.layout().addWidget(QLabel(model.label.capitalize() + ' :' if model.label else ''), 0, 0) + + self.layout().addWidget(QLabel(model.get_label()), 0, 0) if self.model.max_width > 0: self.setMaximumWidth(self.model.max_width) @@ -825,7 +826,7 @@ def update_model(text: str): label.layout().addWidget(label_component) if label: - label_component.setText(c.label.capitalize()) + label_component.setText(c.get_label()) if c.tooltip: label.layout().addWidget(self.gen_tip_icon(c.tooltip)) From e20bfe461b01a8cfb726c5206075cab2112276e0 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 18 Aug 2020 18:41:27 -0300 Subject: [PATCH 30/99] [arch] -> improvement: 'aur_build_dir' property rendered as a file chooser --- bauh/api/abstract/view.py | 11 ++++++++++- bauh/gems/arch/controller.py | 22 +++++++++++----------- bauh/view/qt/components.py | 17 +++++++++++------ 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index 06ead95c..b442c4e1 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -215,7 +215,8 @@ def get_component(self, id_: str) -> ViewComponent: class FileChooserComponent(ViewComponent): def __init__(self, allowed_extensions: Set[str] = None, label: str = None, tooltip: str = None, - file_path: str = None, max_width: int = -1, id_: str = None, search_path: str = None): + file_path: str = None, max_width: int = -1, id_: str = None, search_path: str = None, capitalize_label: bool = True, + directory: bool = False): super(FileChooserComponent, self).__init__(id_=id_) self.label = label self.allowed_extensions = allowed_extensions @@ -223,6 +224,8 @@ def __init__(self, allowed_extensions: Set[str] = None, label: str = None, toolt self.tooltip = tooltip self.max_width = max_width self.search_path = search_path + self.capitalize_label = capitalize_label + self.directory = directory def set_file_path(self, fpath: str): self.file_path = fpath @@ -231,6 +234,12 @@ def set_file_path(self, fpath: str): for o in self.observers: o.on_change(self.file_path) + def get_label(self) -> str: + if not self.label: + return '' + else: + return self.label.capitalize() if self.capitalize_label else self.label + class TabComponent(ViewComponent): diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index db615b10..51b38dc9 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -22,7 +22,8 @@ from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus, \ SuggestionPriority, CustomSoftwareAction from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ - ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextComponent, TextInputType + ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextComponent, TextInputType, \ + FileChooserComponent from bauh.api.constants import TEMP_DIR from bauh.commons import user, internet from bauh.commons.category import CategoriesDownloader @@ -2415,7 +2416,7 @@ def _gen_bool_selector(self, id_: str, label_key: str, tooltip_key: str, value: def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: local_config = read_config() - max_width = floor(screen_width * 0.22) + max_width = floor(screen_width * 0.25) db_sync_start = self._gen_bool_selector(id_='sync_dbs_start', label_key='arch.config.sync_dbs', @@ -2486,14 +2487,13 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: max_width=max_width, type_=SelectViewType.RADIO, capitalize_label=False), - TextInputComponent(id_='aur_build_dir', - label=self.i18n['arch.config.aur_build_dir'], - tooltip=self.i18n['arch.config.aur_build_dir.tip'].format(BUILD_DIR), - only_int=False, - max_width=max_width, - value=local_config.get('aur_build_dir', ''), - 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), + max_width=max_width, + file_path=local_config['aur_build_dir'], + capitalize_label=False, + directory=True) ] return PanelComponent([FormComponent(fields, spaces=False)]) @@ -2513,7 +2513,7 @@ def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: config['repositories_mthread_download'] = form_install.get_component('mthread_download').get_selected() config['automatch_providers'] = form_install.get_component('autoprovs').get_selected() config['edit_aur_pkgbuild'] = form_install.get_component('edit_aur_pkgbuild').get_selected() - config['aur_build_dir'] = form_install.get_component('aur_build_dir').get_value().strip() + config['aur_build_dir'] = form_install.get_component('aur_build_dir').file_path if not config['aur_build_dir']: config['aur_build_dir'] = None diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 616e754c..d6f7bdd7 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -768,8 +768,11 @@ def _new_label(self, comp) -> QWidget: if hasattr(comp, 'size') and comp.size is not None: label_comp.setStyleSheet("QLabel { font-size: " + str(comp.size) + "px }") - attr = 'label' if hasattr(comp,'label') else 'value' - text = getattr(comp, attr) + if hasattr(comp, 'get_label'): + text = comp.get_label() + else: + attr = 'label' if hasattr(comp,'label') else 'value' + text = getattr(comp, attr) if text: if hasattr(comp, 'capitalize_label') and getattr(comp, 'capitalize_label'): @@ -868,17 +871,16 @@ def _new_file_chooser(self, c: FileChooserComponent) -> Tuple[QLabel, QLineEdit] if c.file_path: chooser.setText(c.file_path) + chooser.setCursorPosition(0) c.observers.append(chooser) chooser.setPlaceholderText(self.i18n['view.components.file_chooser.placeholder']) def open_chooser(e): - options = QFileDialog.Options() - if c.allowed_extensions: exts = ';;'.join({'*.{}'.format(e) for e in c.allowed_extensions}) else: - exts = '{}} (*);;'.format(self.i18n['all_files'].capitalize()) + exts = '{} (*);;'.format(self.i18n['all_files'].capitalize()) if c.file_path and os.path.isfile(c.file_path): cur_path = c.file_path @@ -887,7 +889,10 @@ def open_chooser(e): else: cur_path = str(Path.home()) - file_path, _ = QFileDialog.getOpenFileName(self, self.i18n['file_chooser.title'], cur_path, exts, options=options) + if c.directory: + file_path = QFileDialog.getExistingDirectory(self, self.i18n['file_chooser.title'], cur_path, options=QFileDialog.Options()) + else: + file_path, _ = QFileDialog.getOpenFileName(self, self.i18n['file_chooser.title'], cur_path, exts, options=QFileDialog.Options()) if file_path: c.set_file_path(file_path) From eb1a1520a43daf2dfd1d22a565cc8c4ac403aaf6 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 19 Aug 2020 14:59:14 -0300 Subject: [PATCH 31/99] [arch] fix -> some environment variables are not available during the common operations (install, upgrade, ...) --- CHANGELOG.md | 1 + bauh/commons/system.py | 23 +++++++++++++---------- bauh/gems/arch/controller.py | 6 ++++-- bauh/gems/arch/makepkg.py | 4 ++-- bauh/gems/arch/pacman.py | 16 ++++++++-------- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bd38056..1fadceba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server address is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) + - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg) - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 1684682b..af77396c 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -26,20 +26,20 @@ def gen_env(global_interpreter: bool, lang: str = DEFAULT_LANG, extra_paths: Set[str] = None) -> dict: - res = {} + custom_env = dict(os.environ) if lang: - res['LANG'] = lang + custom_env['LANG'] = lang if global_interpreter: # to avoid subprocess calls to the virtualenv python interpreter instead of the global one. - res['PATH'] = GLOBAL_INTERPRETER_PATH + custom_env['PATH'] = GLOBAL_INTERPRETER_PATH else: - res['PATH'] = PATH + custom_env['PATH'] = PATH if extra_paths: - res['PATH'] = ':'.join(extra_paths) + ':' + res['PATH'] + custom_env['PATH'] = ':'.join(extra_paths) + ':' + custom_env['PATH'] - return res + return custom_env class SystemProcess: @@ -65,9 +65,11 @@ 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): + extra_paths: Set[str] = None, error_phrases: Set[str] = None, wrong_error_phrases: Set[str] = None, + shell: bool = False): pwdin, final_cmd = None, [] + self.shell = shell if root_password is not None: final_cmd.extend(['sudo', '-S']) pwdin = self._new(['echo', root_password], cwd, global_interpreter, lang).stdout @@ -86,13 +88,14 @@ def _new(self, cmd: List[str], cwd: str, global_interpreter: bool, lang: str, st "stderr": subprocess.STDOUT, "bufsize": -1, "cwd": cwd, - "env": gen_env(global_interpreter, lang, extra_paths=extra_paths) + "env": gen_env(global_interpreter, lang, extra_paths=extra_paths), + "shell": self.shell } if stdin: args['stdin'] = stdin - return subprocess.Popen(cmd, **args) + return subprocess.Popen(args=' '.join(cmd) if self.shell else cmd, **args) class ProcessHandler: @@ -160,7 +163,7 @@ def handle(self, process: SystemProcess, error_output: StringIO = None, output_h return process.subproc.returncode is None or process.subproc.returncode == 0 def handle_simple(self, proc: SimpleProcess, output_handler=None) -> Tuple[bool, str]: - self._notify_watcher(' '.join(proc.instance.args) + '\n') + self._notify_watcher((proc.instance.args if proc.shell else ' '.join(proc.instance.args)) + '\n') output = StringIO() for o in proc.instance.stdout: diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 51b38dc9..09a4fb12 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -932,8 +932,10 @@ def _remove_transaction_packages(self, to_remove: Set[str], handler: ProcessHand pkgs_to_remove=len(to_remove)) output_handler.start() try: - success = handler.handle(pacman.remove_several(pkgnames=to_remove, root_password=root_password, skip_checks=True), - output_handler=output_handler.handle) + success, _ = handler.handle_simple(pacman.remove_several(pkgnames=to_remove, + root_password=root_password, + skip_checks=True), + output_handler=output_handler.handle) if not success: self.logger.error("Could not remove packages: {}".format(', '.join(to_remove))) diff --git a/bauh/gems/arch/makepkg.py b/bauh/gems/arch/makepkg.py index f76d96cb..a89964fe 100644 --- a/bauh/gems/arch/makepkg.py +++ b/bauh/gems/arch/makepkg.py @@ -28,7 +28,7 @@ def check(pkgdir: str, optimize: bool, missing_deps: bool, handler: ProcessHandl else: handler.watcher.print('Custom optimized makepkg.conf ( {} ) not found'.format(CUSTOM_MAKEPKG_FILE)) - success, output = handler.handle_simple(SimpleProcess(cmd, cwd=pkgdir)) + success, output = handler.handle_simple(SimpleProcess(cmd, cwd=pkgdir, shell=True)) if missing_deps and 'Missing dependencies' in output: res['missing_deps'] = RE_DEPS_PATTERN.findall(output) @@ -54,7 +54,7 @@ def make(pkgdir: str, optimize: bool, handler: ProcessHandler) -> Tuple[bool, st else: handler.watcher.print('Custom optimized makepkg.conf ( {} ) not found'.format(CUSTOM_MAKEPKG_FILE)) - return handler.handle_simple(SimpleProcess(cmd, cwd=pkgdir)) + return handler.handle_simple(SimpleProcess(cmd, cwd=pkgdir, shell=True)) def update_srcinfo(project_dir: str) -> bool: diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index f4474115..f0cfd0ef 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -149,7 +149,8 @@ def install_as_process(pkgpaths: Iterable[str], root_password: str, file: bool, return SimpleProcess(cmd=cmd, root_password=root_password, cwd=pkgdir, - error_phrases={"error: failed to prepare transaction", 'error: failed to commit transaction', 'error: target not found'}) + error_phrases={"error: failed to prepare transaction", 'error: failed to commit transaction', 'error: target not found'}, + shell=True) def list_desktop_entries(pkgnames: Set[str]) -> List[str]: @@ -731,25 +732,24 @@ def upgrade_several(pkgnames: Iterable[str], root_password: str, overwrite_confl return SimpleProcess(cmd=cmd, root_password=root_password, - error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}) + error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}, + shell=True) def download(root_password: str, *pkgnames: str) -> SimpleProcess: return SimpleProcess(cmd=['pacman', '-Swdd', *pkgnames, '--noconfirm'], root_password=root_password, - error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}) + error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}, + shell=True) -def remove_several(pkgnames: Iterable[str], root_password: str, skip_checks: bool = False) -> SystemProcess: +def remove_several(pkgnames: Iterable[str], root_password: str, skip_checks: bool = False) -> SimpleProcess: cmd = ['pacman', '-R', *pkgnames, '--noconfirm'] if skip_checks: cmd.append('-dd') - if root_password: - return SystemProcess(new_root_subprocess(cmd, root_password), wrong_error_phrase='warning:') - else: - return SystemProcess(new_subprocess(cmd), wrong_error_phrase='warning:') + return SimpleProcess(cmd=cmd, root_password=root_password, wrong_error_phrases={'warning:'}, shell=True) def map_optional_deps(names: Iterable[str], remote: bool, not_installed: bool = False) -> Dict[str, Dict[str, str]]: From f632aea55e53dead21646ec36598fb2db6b20dca Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 19 Aug 2020 19:05:44 -0300 Subject: [PATCH 32/99] [arch] fix -> AUR: not installing the correct package built when several are generated --- CHANGELOG.md | 1 + bauh/gems/arch/controller.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fadceba..6248d6ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server address is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) + - not installing the correct package built when several are generated (e.g: linux-xanmod-lts) - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg) - Flatpak - downgrading crashing with version 1.8.X diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 09a4fb12..583be3fe 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1680,7 +1680,21 @@ def _build(self, context: TransactionContext) -> bool: context.watcher.print('Could not find the built package. Aborting...') return False - context.install_file = '{}/{}'.format(context.project_dir, gen_file[0]) + file_to_install = gen_file[0] + + if len(gen_file) > 1: + srcinfo = aur.map_srcinfo(makepkg.gen_srcinfo(context.project_dir)) + 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 '' + name_start = '{}{}{}{}'.format(context.name, pkgver, pkgrel, arch) + + perfect_match = [f for f in gen_file if f.startswith(name_start)] + + if perfect_match: + file_to_install = perfect_match[0] + + context.install_file = '{}/{}'.format(context.project_dir, file_to_install) if self._install(context=context): self._save_pkgbuild(context) From 92384875cde290d29ce4c8188d6a4d40c187c24f Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 11:55:37 -0300 Subject: [PATCH 33/99] [arch] improvement -> new settings property 'aur_remove_build_dir' --- CHANGELOG.md | 1 + README.md | 13 +++++++------ bauh/gems/arch/config.py | 3 ++- bauh/gems/arch/controller.py | 11 +++++++++-- bauh/gems/arch/resources/locale/ca | 2 ++ bauh/gems/arch/resources/locale/de | 2 ++ bauh/gems/arch/resources/locale/en | 2 ++ bauh/gems/arch/resources/locale/es | 2 ++ bauh/gems/arch/resources/locale/it | 2 ++ bauh/gems/arch/resources/locale/pt | 2 ++ bauh/gems/arch/resources/locale/ru | 2 ++ bauh/gems/arch/resources/locale/tr | 2 ++ 12 files changed, 35 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6248d6ff..d25f0d66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

+ - new settings property **aur_remove_build_dir** -> it defines if a package's generated build directory should be removed after the operation is finished (installation, upgrading, ...). Default: true - preventing a possible error when the optional deps of a given package cannot be found - Flatpak diff --git a/README.md b/README.md index 09d02cb9..7077bead 100644 --- a/README.md +++ b/README.md @@ -189,12 +189,13 @@ sync_databases_startup: true # package databases synchronization once a day dur clean_cached: true # defines if old cached versions should be removed from the disk cache during a package uninstallation refresh_mirrors_startup: false # if the package mirrors should be refreshed during startup mirrors_sort_limit: 5 # defines the maximum number of mirrors that will be used for speed sorting. Use 0 for no limit or leave it blank to disable sorting. -aur: true # allows to manage AUR packages -repositories: true # allows to manage packages from the configured repositories -repositories_mthread_download: false # enable multi-threaded download for repository packages if aria2/axel is installed -automatch_providers: true # if a possible provider for a given package dependency exactly matches its name, it will be chosen instead of asking for the user to decide (false). -edit_aur_pkgbuild: false # if the AUR PKGBUILD file should be displayed for edition before the make process. true (PKGBUILD will always be displayed for edition), false (PKGBUILD never will be displayed), null (a popup will ask if the user want to edit the PKGBUILD) -aur_build_dir: null # defines a custom build directory for AUR packages (a null value will point to /tmp/bauh/arch (non-root user) or /tmp/bauh_root/arch (root user)) +aur: true. Default: true # allows to manage AUR packages +repositories: true # allows to manage packages from the configured repositories. Default: true +repositories_mthread_download: false # enable multi-threaded download for repository packages if aria2/axel is installed (otherwise pacman will download the packages). Default: false +automatch_providers: true # if a possible provider for a given package dependency exactly matches its name, it will be chosen instead of asking for the user to decide (false). Default: true. +edit_aur_pkgbuild: false # if the AUR PKGBUILD file should be displayed for edition before the make process. true (PKGBUILD will always be displayed for edition), false (PKGBUILD never will be displayed), null (a popup will ask if the user want to edit the PKGBUILD). Default: false. +aur_build_dir: null # defines a custom build directory for AUR packages (a null value will point to /tmp/bauh/arch (non-root user) or /tmp/bauh_root/arch (root user)). Default: null. +aur_remove_build_dir: true # it defines if a package's generated build directory should be removed after the operation is finished (installation, upgrading, ...). Options: true, false (default: true). ``` - Required dependencies: - **pacman** diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index 0cae1f54..d95ae12c 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -16,7 +16,8 @@ def read_config(update_file: bool = False) -> dict: 'repositories_mthread_download': False, 'automatch_providers': True, 'edit_aur_pkgbuild': False, - 'aur_build_dir': None} + 'aur_build_dir': None, + 'aur_remove_build_dir': True} return read(CONFIG_FILE, template, update_file=update_file) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 583be3fe..4beb5a93 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -642,7 +642,7 @@ def _downgrade_aur_pkg(self, context: TransactionContext): return False finally: - if os.path.exists(context.build_dir): + 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]))) return False @@ -2150,7 +2150,7 @@ def _install_from_aur(self, context: TransactionContext) -> bool: return self._build(context) finally: - if os.path.exists(context.build_dir): + if os.path.exists(context.build_dir) and context.config['aur_remove_build_dir']: context.handler.handle(SystemProcess(new_subprocess(['rm', '-rf', context.build_dir]))) return False @@ -2503,6 +2503,12 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: max_width=max_width, type_=SelectViewType.RADIO, capitalize_label=False), + self._gen_bool_selector(id_='aur_remove_build_dir', + label_key='arch.config.aur_remove_build_dir', + tooltip_key='arch.config.aur_remove_build_dir.tip', + value=bool(local_config['aur_remove_build_dir']), + max_width=max_width, + 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), @@ -2529,6 +2535,7 @@ def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: config['repositories_mthread_download'] = form_install.get_component('mthread_download').get_selected() config['automatch_providers'] = form_install.get_component('autoprovs').get_selected() config['edit_aur_pkgbuild'] = form_install.get_component('edit_aur_pkgbuild').get_selected() + config['aur_remove_build_dir'] = form_install.get_component('aur_remove_build_dir').get_selected() config['aur_build_dir'] = form_install.get_component('aur_build_dir').file_path if not config['aur_build_dir']: diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 6b46ef39..a65f0405 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -32,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_remove_build_dir=Remove build directory (AUR) +arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Elimina les versions antigues arch.config.clean_cache.tip=Si cal eliminar les versions antigues d'un paquet emmagatzemat al disc durant la desinstal·lació arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index caacf7d8..ecfd6a5c 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -32,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_remove_build_dir=Remove build directory (AUR) +arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index f530e0c1..0151fcf9 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -33,6 +33,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_remove_build_dir=Remove build directory (AUR) +arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index a9f3f302..e255ef77 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -32,6 +32,8 @@ arch.config.automatch_providers=Autodefinir proveedores de dependencia arch.config.automatch_providers.tip=Elige automáticamente qué proveedor se usará para una dependencia de paquete cuando ambos nombres son iguales. arch.config.aur_build_dir=Directorio de compilación (AUR) arch.config.aur_build_dir.tip=Define un directorio personalizado donde se construirán los paquetes AUR. Defecto: {}. +arch.config.aur_remove_build_dir=Eliminar directorio de compilación (AUR) +arch.config.aur_remove_build_dir.tip=Si el directorio de compilación generado para un paquete debe ser eliminado una vez finalizada la operación. arch.config.clean_cache=Eliminar versiones antiguas arch.config.clean_cache.tip=Si las versiones antiguas de un paquete almacenado en el disco deben ser eliminadas durante la desinstalación arch.config.edit_aur_pkgbuild=Editar PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index a74eb7f2..f3bf0c22 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -32,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_remove_build_dir=Remove build directory (AUR) +arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Rimuovi le vecchie versioni arch.config.clean_cache.tip=Se le vecchie versioni di un pacchetto memorizzate sul disco devono essere rimosse durante la disinstallazione arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 9b07819c..5ee94705 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -33,6 +33,8 @@ arch.config.automatch_providers=Auto-definir provedores de dependências arch.config.automatch_providers.tip=Escolhe automaticamente qual provedor será utilizado para determinada dependência de um pacote caso os nomes de ambos sejam iguais. arch.config.aur_build_dir=Diretório de construção (AUR) arch.config.aur_build_dir.tip=Define um diretório personalizado onde pacotes do AUR serão construídos. Padrão: {}. +arch.config.aur_remove_build_dir=Remover diretório de construção (AUR) +arch.config.aur_remove_build_dir.tip=Se o diretório gerado para a construção de um pacote do AUR deve ser removido após a operação ser finalizada. arch.config.clean_cache=Remover versões antigas arch.config.clean_cache.tip=Se versões antigas de um pacote armazenadas em disco devem ser removidas durante a desinstalação arch.config.edit_aur_pkgbuild=Editar PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 2604e90a..62700a0e 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -32,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_remove_build_dir=Remove build directory (AUR) +arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 9e50bd93..8105a722 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -32,6 +32,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_remove_build_dir=Remove build directory (AUR) +arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Önbelleği temizle arch.config.clean_cache.tip=Disk üzerinde kurulu bir paketin eski sürümlerinin kaldırma sırasında kaldırılıp kaldırılmayacağı arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) From 4544a46421ca1d367fd1a8e960d575466a32a3ce Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 12:04:49 -0300 Subject: [PATCH 34/99] [arch] improvement -> increasing PKGBUILD editing dialog dimensions --- bauh/api/abstract/view.py | 4 +++- bauh/gems/arch/controller.py | 3 ++- bauh/view/qt/components.py | 12 ++++++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/bauh/api/abstract/view.py b/bauh/api/abstract/view.py index b442c4e1..0918e5e8 100644 --- a/bauh/api/abstract/view.py +++ b/bauh/api/abstract/view.py @@ -157,7 +157,7 @@ class TextInputComponent(ViewComponent): def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip: str = None, read_only: bool =False, id_: str = None, only_int: bool = False, max_width: int = -1, type_: TextInputType = TextInputType.SINGLE_LINE, - capitalize_label: bool = True): + capitalize_label: bool = True, min_width: int = -1, min_height: int = -1): super(TextInputComponent, self).__init__(id_=id_) self.label = label self.value = value @@ -168,6 +168,8 @@ def __init__(self, label: str, value: str = '', placeholder: str = None, tooltip self.max_width = max_width self.type = type_ self.capitalize_label = capitalize_label + self.min_width = min_width + self.min_height = min_height def get_value(self) -> str: if self.value is not None: diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 4beb5a93..4de5646b 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1596,7 +1596,8 @@ def _display_pkgbuild_for_editing(self, pkgname: str, watcher: ProcessWatcher, p with open(pkgbuild_path) as f: pkgbuild = f.read() - pkgbuild_input = TextInputComponent(label='', value=pkgbuild, type_=TextInputType.MULTIPLE_LINES) + pkgbuild_input = TextInputComponent(label='', value=pkgbuild, type_=TextInputType.MULTIPLE_LINES, + min_width=500, min_height=350) watcher.request_confirmation(title='PKGBUILD ({})'.format(pkgname), body='', diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index d6f7bdd7..40b01f9a 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -475,6 +475,12 @@ def __init__(self, model: TextInputComponent): if model.placeholder: self.text_input.setPlaceholderText(model.placeholder) + if model.min_width >= 0: + self.text_input.setMinimumWidth(model.min_width) + + if model.min_height >= 0: + self.text_input.setMinimumHeight(model.min_height) + if model.tooltip: self.text_input.setToolTip(model.tooltip) @@ -799,6 +805,12 @@ def gen_tip_icon(self, tip: str) -> QLabel: def _new_text_input(self, c: TextInputComponent) -> Tuple[QLabel, QLineEdit]: view = QLineEditObserver() if c.type == TextInputType.SINGLE_LINE else QPlainTextEditObserver() + if c.min_width >= 0: + view.setMinimumWidth(c.min_width) + + if c.min_height >= 0: + view.setMinimumHeight(c.min_height) + if c.only_int: view.setValidator(QIntValidator()) From b99462d0e5e74e74354b7fc76ec931f85e967a46 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 12:37:21 -0300 Subject: [PATCH 35/99] [flatpak] fix -> some environment variables are not available during the common operations --- CHANGELOG.md | 1 + bauh/commons/system.py | 10 ++++++++- bauh/gems/flatpak/controller.py | 19 +++++++++-------- bauh/gems/flatpak/flatpak.py | 37 +++++++++++++-------------------- 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d25f0d66..2a676e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

+ - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall) - UI - crashing when nothing can be upgraded - random C++ wrapper errors with some forms due to missing references diff --git a/bauh/commons/system.py b/bauh/commons/system.py index af77396c..3437a479 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -66,7 +66,8 @@ 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): + shell: bool = False, + success_phrases: Set[str] = None): pwdin, final_cmd = None, [] self.shell = shell @@ -80,6 +81,7 @@ def __init__(self, cmd: List[str], cwd: str = '.', expected_code: int = 0, self.expected_code = expected_code self.error_phrases = error_phrases self.wrong_error_phrases = wrong_error_phrases + self.success_phrases = success_phrases def _new(self, cmd: List[str], cwd: str, global_interpreter: bool, lang: str, stdin = None, extra_paths: Set[str] = None) -> subprocess.Popen: @@ -186,6 +188,12 @@ def handle_simple(self, proc: SimpleProcess, output_handler=None) -> Tuple[bool, success = proc.instance.returncode == proc.expected_code string_output = output.read() + if proc.success_phrases: + for phrase in proc.success_phrases: + if phrase in string_output: + success = True + break + if not success and proc.wrong_error_phrases: for phrase in proc.wrong_error_phrases: if phrase in string_output: diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 3f72e818..31e6d8e2 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -18,7 +18,7 @@ from bauh.commons import user, internet from bauh.commons.config import save_config from bauh.commons.html import strip_html, bold -from bauh.commons.system import SystemProcess, ProcessHandler +from bauh.commons.system import ProcessHandler from bauh.gems.flatpak import flatpak, SUGGESTIONS_FILE, CONFIG_FILE, UPDATES_IGNORED_FILE, CONFIG_DIR, EXPORTS_PATH from bauh.gems.flatpak.config import read_config from bauh.gems.flatpak.constants import FLATHUB_API_URL @@ -186,9 +186,10 @@ def downgrade(self, pkg: FlatpakApplication, root_password: str, watcher: Proces commit = history.history[history.pkg_status_idx + 1]['commit'] watcher.change_substatus(self.i18n['flatpak.downgrade.reverting']) watcher.change_progress(50) - success = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.downgrade(pkg.ref, commit, pkg.installation, root_password), - success_phrases=['Changes complete.', 'Updates complete.'], - wrong_error_phrase='Warning')) + success, _ = ProcessHandler(watcher).handle_simple(flatpak.downgrade(pkg.ref, + commit, + pkg.installation, + root_password)) watcher.change_progress(100) return success @@ -212,10 +213,10 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher ref = req.pkg.base_ref try: - res = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.update(app_ref=ref, - installation=req.pkg.installation, - related=related, - deps=deps))) + res, _ = ProcessHandler(watcher).handle_simple(flatpak.update(app_ref=ref, + installation=req.pkg.installation, + related=related, + deps=deps)) watcher.change_substatus('') if not res: @@ -235,7 +236,7 @@ def uninstall(self, pkg: FlatpakApplication, root_password: str, watcher: Proces if not self._make_exports_dir(watcher): return TransactionResult.fail() - uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=flatpak.uninstall(pkg.ref, pkg.installation))) + uninstalled, _ = ProcessHandler(watcher).handle_simple(flatpak.uninstall(pkg.ref, pkg.installation)) if uninstalled: if self.suggestions_cache: diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index de3acf2a..cd9b425a 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -5,7 +5,7 @@ from typing import List, Dict, Set, Iterable, Optional from bauh.api.exception import NoInternetException -from bauh.commons.system import new_subprocess, run_cmd, new_root_subprocess, SimpleProcess, ProcessHandler +from bauh.commons.system import new_subprocess, run_cmd, SimpleProcess, ProcessHandler from bauh.commons.util import size_to_byte from bauh.gems.flatpak import EXPORTS_PATH @@ -153,12 +153,7 @@ def list_installed(version: str) -> List[dict]: return apps -def update(app_ref: str, installation: str, related: bool = False, deps: bool = False): - """ - Updates the app reference - :param app_ref: - :return: - """ +def update(app_ref: str, installation: str, related: bool = False, deps: bool = False) -> SimpleProcess: cmd = ['flatpak', 'update', '-y', app_ref, '--{}'.format(installation)] if not related: @@ -167,17 +162,13 @@ def update(app_ref: str, installation: str, related: bool = False, deps: bool = if not deps: cmd.append('--no-deps') - return new_subprocess(cmd=cmd, extra_paths={EXPORTS_PATH}) + return SimpleProcess(cmd=cmd, extra_paths={EXPORTS_PATH}, shell=True) -def uninstall(app_ref: str, installation: str): - """ - Removes the app by its reference - :param app_ref: - :return: - """ - return new_subprocess(cmd=['flatpak', 'uninstall', app_ref, '-y', '--{}'.format(installation)], - extra_paths={EXPORTS_PATH}) +def uninstall(app_ref: str, installation: str) -> SimpleProcess: + return SimpleProcess(cmd=['flatpak', 'uninstall', app_ref, '-y', '--{}'.format(installation)], + extra_paths={EXPORTS_PATH}, + shell=True) def list_updates_as_str(version: str) -> Dict[str, set]: @@ -231,13 +222,14 @@ def read_updates(version: str, installation: str) -> Dict[str, set]: return res -def downgrade(app_ref: str, commit: str, installation: str, root_password: str) -> subprocess.Popen: +def downgrade(app_ref: str, commit: str, installation: str, root_password: str) -> SimpleProcess: cmd = ['flatpak', 'update', '--no-related', '--no-deps', '--commit={}'.format(commit), app_ref, '-y', '--{}'.format(installation)] - if installation == 'system': - return new_root_subprocess(cmd=cmd, root_password=root_password, extra_paths={EXPORTS_PATH}) - else: - return new_subprocess(cmd=cmd, extra_paths={EXPORTS_PATH}) + return SimpleProcess(cmd=cmd, + root_password=root_password if installation=='system' else None, + extra_paths={EXPORTS_PATH}, + success_phrases={'Changes complete.', 'Updates complete.'}, + wrong_error_phrases={'Warning'}) def get_app_commits(app_ref: str, origin: str, installation: str, handler: ProcessHandler) -> List[str]: @@ -360,7 +352,8 @@ def search(version: str, word: str, installation: str, app_id: bool = False) -> def install(app_id: str, origin: str, installation: str) -> SimpleProcess: return SimpleProcess(cmd=['flatpak', 'install', origin, app_id, '-y', '--{}'.format(installation)], extra_paths={EXPORTS_PATH}, - wrong_error_phrases={'Warning'}) + wrong_error_phrases={'Warning'}, + shell=True) def set_default_remotes(installation: str, root_password: str = None) -> SimpleProcess: From f45a47a4662c5991d228d305f4b8c37050205965 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 14:12:43 -0300 Subject: [PATCH 36/99] [snap] fix -> some environment variables are not available during the common operations --- CHANGELOG.md | 2 ++ bauh/gems/snap/controller.py | 6 +++--- bauh/gems/snap/snap.py | 25 +++++++++++++++---------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a676e67..0522f707 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall) +- Snap + - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) - UI - crashing when nothing can be upgraded - random C++ wrapper errors with some forms due to missing references diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 43eea85a..6ad82683 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -138,13 +138,13 @@ def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_app return SearchResult([], None, 0) def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: - return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.downgrade_and_stream(pkg.name, root_password), wrong_error_phrase=None)) + 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__)) def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher, disk_loader: DiskCacheLoader) -> TransactionResult: - uninstalled = ProcessHandler(watcher).handle(SystemProcess(subproc=snap.uninstall_and_stream(pkg.name, root_password))) + uninstalled = ProcessHandler(watcher).handle_simple(snap.uninstall_and_stream(pkg.name, root_password))[0] if uninstalled: if self.suggestions_cache: @@ -254,7 +254,7 @@ def requires_root(self, action: str, pkg: SnapApplication): return action not in ('search', 'prepare') def refresh(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: - return ProcessHandler(watcher).handle(SystemProcess(subproc=snap.refresh_and_stream(pkg.name, root_password))) + return ProcessHandler(watcher).handle_simple(snap.refresh_and_stream(pkg.name, root_password))[0] def _start_category_task(self, task_man: TaskManager): if task_man: diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index 7c52ec19..7db03872 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -5,7 +5,7 @@ from io import StringIO from typing import List, Tuple, Set -from bauh.commons.system import new_root_subprocess, run_cmd, new_subprocess, SimpleProcess +from bauh.commons.system import run_cmd, new_subprocess, SimpleProcess from bauh.gems.snap.model import SnapApplication BASE_CMD = 'snap' @@ -171,8 +171,10 @@ def search(word: str, exact_name: bool = False) -> List[dict]: return apps -def uninstall_and_stream(app_name: str, root_password: str): - return new_root_subprocess([BASE_CMD, 'remove', app_name], root_password) +def uninstall_and_stream(app_name: str, root_password: str) -> SimpleProcess: + return SimpleProcess(cmd=[BASE_CMD, 'remove', app_name], + root_password=root_password, + shell=True) def install_and_stream(app_name: str, confinement: str, root_password: str) -> SimpleProcess: @@ -182,16 +184,19 @@ def install_and_stream(app_name: str, confinement: str, root_password: str) -> S if confinement == 'classic': install_cmd.append('--classic') - # return new_root_subprocess(install_cmd, root_password) - return SimpleProcess(install_cmd, root_password=root_password) + return SimpleProcess(install_cmd, root_password=root_password, shell=True) -def downgrade_and_stream(app_name: str, root_password: str) -> subprocess.Popen: - return new_root_subprocess([BASE_CMD, 'revert', app_name], root_password) +def downgrade_and_stream(app_name: str, root_password: str) -> SimpleProcess: + return SimpleProcess(cmd=[BASE_CMD, 'revert', app_name], + root_password=root_password, + shell=True) -def refresh_and_stream(app_name: str, root_password: str) -> subprocess.Popen: - return new_root_subprocess([BASE_CMD, 'refresh', app_name], root_password) +def refresh_and_stream(app_name: str, root_password: str) -> SimpleProcess: + return SimpleProcess(cmd=[BASE_CMD, 'refresh', app_name], + root_password=root_password, + shell=True) def run(app: SnapApplication, logger: logging.Logger): @@ -220,7 +225,7 @@ def run(app: SnapApplication, logger: logging.Logger): if command: logger.info("Running '{}'".format(command)) - subprocess.Popen([BASE_CMD, 'run', command]) + subprocess.Popen('{} run {}'.format(BASE_CMD, command), shell=True) return logger.error("No valid command found for '{}'".format(app_name)) From a6cbfd4a3fe6e24d12fdeae395953c66f8c52ff1 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 14:15:55 -0300 Subject: [PATCH 37/99] [appimage] fix -> some environment variable are not available during the launch process --- CHANGELOG.md | 1 + bauh/gems/appimage/controller.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0522f707..4d9d67b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - AppImage - manual file installation - crashing the application icon is not on the extracted folder root path [#132](https://github.com/vinifmor/bauh/issues/132) + - some environment variable are not available during the launch process - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) - downloading some AUR packages sources twice when multi-threaded download is enabled diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 36738aea..f4e90866 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -670,7 +670,7 @@ def launch(self, pkg: AppImage): appimag_path = util.find_appimage_file(installation_dir) if appimag_path: - subprocess.Popen([appimag_path]) + subprocess.Popen(args=appimag_path, shell=True) else: self.logger.error("Could not find the AppImage file of '{}' in '{}'".format(pkg.name, installation_dir)) From e06c677602ab31d758dc5180015a448ea76d7fc3 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 14:18:45 -0300 Subject: [PATCH 38/99] [flatpak] fix -> not properly checking the return result when setting the default remotes at the system level --- CHANGELOG.md | 1 + bauh/gems/flatpak/controller.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d9d67b5..5255730b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall) + - minor fixes - Snap - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) - UI diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 31e6d8e2..34011d57 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -369,7 +369,7 @@ def install(self, pkg: FlatpakApplication, root_password: str, disk_loader: Disk watcher.print('Operation aborted') return TransactionResult(success=False, installed=[], removed=[]) else: - if not handler.handle_simple(flatpak.set_default_remotes('system', user_password)): + if not handler.handle_simple(flatpak.set_default_remotes('system', user_password))[0]: watcher.show_message(title=self.i18n['error'].capitalize(), body=self.i18n['flatpak.remotes.system_flathub.error'], type_=MessageType.ERROR) From 3691a2162c74d0b2afc7f354daf1f348119dcbbd Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 14:27:24 -0300 Subject: [PATCH 39/99] [flatpak] fix -> some env vars not available during the launch process --- CHANGELOG.md | 2 +- bauh/gems/flatpak/flatpak.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5255730b..c6192e87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,7 +76,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall) + - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) - minor fixes - Snap - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) diff --git a/bauh/gems/flatpak/flatpak.py b/bauh/gems/flatpak/flatpak.py index cd9b425a..00425922 100755 --- a/bauh/gems/flatpak/flatpak.py +++ b/bauh/gems/flatpak/flatpak.py @@ -1,3 +1,4 @@ +import os import re import subprocess import traceback @@ -384,7 +385,7 @@ def list_remotes() -> Dict[str, Set[str]]: def run(app_id: str): - subprocess.Popen(['flatpak', 'run', app_id]) + subprocess.Popen(['flatpak run {}'.format(app_id)], shell=True, env={**os.environ}) def map_update_download_size(app_ids: Iterable[str], installation: str, version: str) -> Dict[str, int]: From a8eb6dca72f503fb74a869100bd7983ddce85038 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 14:32:06 -0300 Subject: [PATCH 40/99] [snap] fix -> not informing the env vars during the launch process --- bauh/gems/snap/snap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index 7db03872..d114b5fa 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -225,7 +225,7 @@ def run(app: SnapApplication, logger: logging.Logger): if command: logger.info("Running '{}'".format(command)) - subprocess.Popen('{} run {}'.format(BASE_CMD, command), shell=True) + subprocess.Popen(['{} run {}'.format(BASE_CMD, command)], shell=True, env={**os.environ}) return logger.error("No valid command found for '{}'".format(app_name)) From 912550e0eda873ada3f68bf770e683e3fbea9741 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 14:34:01 -0300 Subject: [PATCH 41/99] [appimage] fix -> not informing the env vars during the launch process --- bauh/gems/appimage/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index f4e90866..a1da2aeb 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -670,7 +670,7 @@ def launch(self, pkg: AppImage): appimag_path = util.find_appimage_file(installation_dir) if appimag_path: - subprocess.Popen(args=appimag_path, shell=True) + subprocess.Popen(args=[appimag_path], shell=True, env={**os.environ}) else: self.logger.error("Could not find the AppImage file of '{}' in '{}'".format(pkg.name, installation_dir)) From c4e20e17c7cc2abbf1bd4d614387b1b53c790d1f Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 14:37:49 -0300 Subject: [PATCH 42/99] [web] fix -> not informing the env vars during the launch process --- CHANGELOG.md | 2 ++ bauh/gems/web/controller.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6192e87..fcac1531 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -80,6 +80,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - minor fixes - Snap - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) +- Web + - some environment variable are not available during the launch process - UI - crashing when nothing can be upgraded - random C++ wrapper errors with some forms due to missing references diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index 365fb8f5..6361aec2 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -26,7 +26,7 @@ from bauh.api.abstract.view import MessageType, MultipleSelectComponent, InputOption, SingleSelectComponent, \ SelectViewType, TextInputComponent, FormComponent, FileChooserComponent, ViewComponent, PanelComponent from bauh.api.constants import DESKTOP_ENTRIES_DIR -from bauh.commons import resource, user +from bauh.commons import resource from bauh.commons.config import save_config from bauh.commons.html import bold from bauh.commons.system import ProcessHandler, get_dir_size, get_human_size_str, SimpleProcess @@ -949,7 +949,7 @@ def is_default_enabled(self) -> bool: return True def launch(self, pkg: WebApplication): - subprocess.Popen(pkg.get_command(), shell=user.is_root()) + subprocess.Popen(args=[pkg.get_command()], shell=True, env={**os.environ}) def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: pass From f16870afd3d47a57717833a6b293914261a62670 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 14:47:32 -0300 Subject: [PATCH 43/99] fix -> not passing the subprocess command arg as an array when shell is set --- bauh/commons/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 3437a479..1d4c3911 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -97,7 +97,7 @@ def _new(self, cmd: List[str], cwd: str, global_interpreter: bool, lang: str, st if stdin: args['stdin'] = stdin - return subprocess.Popen(args=' '.join(cmd) if self.shell else cmd, **args) + return subprocess.Popen(args=[' '.join(cmd)] if self.shell else cmd, **args) class ProcessHandler: From 72573cf300a46f3950eb474ffac1bbbd4d83d50d Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 15:02:52 -0300 Subject: [PATCH 44/99] [ui] fix -> not properly checking if a command is an array or str --- bauh/commons/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 1d4c3911..fc6460bd 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -165,7 +165,7 @@ def handle(self, process: SystemProcess, error_output: StringIO = None, output_h return process.subproc.returncode is None or process.subproc.returncode == 0 def handle_simple(self, proc: SimpleProcess, output_handler=None) -> Tuple[bool, str]: - self._notify_watcher((proc.instance.args if proc.shell else ' '.join(proc.instance.args)) + '\n') + self._notify_watcher((proc.instance.args if isinstance(proc.instance.args, str) else ' '.join(proc.instance.args)) + '\n') output = StringIO() for o in proc.instance.stdout: From 20f049315cc829057fe57deebc3062a7c300928b Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 17:28:31 -0300 Subject: [PATCH 45/99] [arch] fix -> not informing the env vars during the launch process --- CHANGELOG.md | 2 +- bauh/gems/arch/controller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fcac1531..1ccdbfff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -67,7 +67,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server address is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) - not installing the correct package built when several are generated (e.g: linux-xanmod-lts) - - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg) + - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 4de5646b..78576241 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -2412,7 +2412,7 @@ def is_default_enabled(self) -> bool: def launch(self, pkg: ArchPackage): if pkg.command: - subprocess.Popen(pkg.command.split(' ')) + subprocess.Popen(args=[pkg.command], shell=True, env={**os.environ}) def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: pass From dcd2cfd8df96cd6b78cfdbd2568e240d61010a0c Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 20 Aug 2020 17:33:51 -0300 Subject: [PATCH 46/99] [arch] improvement -> AUR: edit pkgbuild dialog with 'No' as the default button --- bauh/gems/arch/controller.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 78576241..c7c0282f 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1616,9 +1616,10 @@ def _display_pkgbuild_for_editing(self, pkgname: str, watcher: ProcessWatcher, p def _ask_for_pkgbuild_edition(self, pkgname: str, arch_config: dict, watcher: ProcessWatcher, pkgbuild_path: str) -> bool: if pkgbuild_path: if arch_config['edit_aur_pkgbuild'] is None: - if watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(), - body=self.i18n['arch.aur.action.edit_pkgbuild.body'].format( - bold(pkgname))): + if not watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(), + body=self.i18n['arch.aur.action.edit_pkgbuild.body'].format(bold(pkgname)), + confirmation_label=self.i18n['no'].capitalize(), + deny_label=self.i18n['yes'].capitalize()): return self._display_pkgbuild_for_editing(pkgname, watcher, pkgbuild_path) elif arch_config['edit_aur_pkgbuild']: return self._display_pkgbuild_for_editing(pkgname, watcher, pkgbuild_path) From 634f3dc9327c20f63274d872c6165b52bfdaa3c7 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 15:12:05 -0300 Subject: [PATCH 47/99] [arch] improvment -> initializing task 'Organizing data from installed packages' is taking about 80% less time --- CHANGELOG.md | 1 + bauh/gems/arch/controller.py | 6 +- bauh/gems/arch/disk.py | 257 +++++++++++------------------ bauh/gems/arch/pacman.py | 44 ++--- bauh/gems/arch/resources/locale/ca | 9 +- bauh/gems/arch/resources/locale/de | 9 +- bauh/gems/arch/resources/locale/en | 9 +- bauh/gems/arch/resources/locale/es | 9 +- bauh/gems/arch/resources/locale/it | 9 +- bauh/gems/arch/resources/locale/pt | 9 +- bauh/gems/arch/resources/locale/ru | 9 +- bauh/gems/arch/resources/locale/tr | 9 +- bauh/gems/arch/worker.py | 39 ++--- 13 files changed, 175 insertions(+), 244 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ccdbfff..94ad2806 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - default search path set to '~/Downloads' - trying to auto-fill the 'Name' and 'Version' fields - Arch + - initializing task "Organizing data from installed packages" is taking about 80% less time (now is called "Indexing packages data") - upgrade summary: displaying the reason a given package must be installed

diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index c7c0282f..1317a49c 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -886,7 +886,7 @@ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str] maintainer=repo, categories=self.categories.get(name)) - disk.save_several(pkg_map, overwrite=True, maintainer=None) + disk.write_several(pkgs=pkg_map, overwrite=True, maintainer=None) return True elif 'conflicting files' in upgrade_output: files = self._map_conflicting_file(upgrade_output) @@ -1532,7 +1532,7 @@ def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]] if installed: pkg_map = {d[0]: ArchPackage(name=d[0], repository=d[1], maintainer=d[1], categories=self.categories.get(d[0])) for d in repo_deps} - disk.save_several(pkg_map, overwrite=True, maintainer=None) + disk.write_several(pkg_map, overwrite=True, maintainer=None) progress += len(repo_deps) * progress_increment self._update_progress(context, progress) else: @@ -2078,7 +2078,7 @@ def _install(self, context: TransactionContext) -> bool: maintainer=dep[1] if dep[1] != 'aur' else (aur_data[dep[0]].get('Maintainer') if aur_data else None), categories=self.categories.get(context.name)) - disk.save_several(pkgs=cache_map, maintainer=None, overwrite=True) + disk.write_several(pkgs=cache_map, maintainer=None, overwrite=True) context.watcher.change_substatus('') self._update_progress(context, 100) diff --git a/bauh/gems/arch/disk.py b/bauh/gems/arch/disk.py index f0b1e9e3..657dda8f 100644 --- a/bauh/gems/arch/disk.py +++ b/bauh/gems/arch/disk.py @@ -2,184 +2,127 @@ import os import re from pathlib import Path -from typing import List, Dict +from typing import List, Dict, Tuple, Optional, Callable from bauh.gems.arch import pacman from bauh.gems.arch.model import ArchPackage -RE_DESKTOP_ENTRY = re.compile(r'(Exec|Icon|NoDisplay)\s*=\s*(.+)') +RE_DESKTOP_ENTRY = re.compile(r'[\n^](Exec|Icon|NoDisplay)\s*=\s*(.+)') RE_CLEAN_NAME = re.compile(r'[+*?%]') -def write(pkg: ArchPackage): - data = pkg.get_data_to_cache() - - Path(pkg.get_disk_cache_path()).mkdir(parents=True, exist_ok=True) - - with open(pkg.get_disk_data_path(), 'w+') as f: - f.write(json.dumps(data)) - - -def fill_icon_path(pkg: ArchPackage, icon_paths: List[str], only_exact_match: bool): - clean_name = RE_CLEAN_NAME.sub('', pkg.name) - ends_with = re.compile(r'.+/{}\.(png|svg|xpm)$'.format(pkg.icon_path if pkg.icon_path else clean_name), re.IGNORECASE) - - for path in icon_paths: - if ends_with.match(path): - pkg.icon_path = path - return - - if not only_exact_match: - pkg_icons_path = pacman.list_icon_paths({pkg.name}) - - if pkg_icons_path: - pkg.set_icon(pkg_icons_path) - - -def set_icon_path(pkg: ArchPackage, icon_name: str = None): - installed_icons = pacman.list_icon_paths({pkg.name}) - - if installed_icons: - exact_match = re.compile(r'.+/{}\..+$'.format(icon_name.split('.')[0] if icon_name else pkg.name)) - for icon_path in installed_icons: - if exact_match.match(icon_path): - pkg.icon_path = icon_path - break - - -def save_several(pkgs: Dict[str, ArchPackage], overwrite: bool = True, maintainer: str = None, when_prepared=None, after_written=None) -> int: +def write_several(pkgs: Dict[str, ArchPackage], overwrite: bool = True, maintainer: str = None, after_desktop_files: Optional[Callable] = None, after_written: Optional[Callable[[str], None]] = None) -> int: if overwrite: to_cache = {p.name for p in pkgs.values()} else: to_cache = {p.name for p in pkgs.values() if not os.path.exists(p.get_disk_cache_path())} - desktop_files = pacman.list_desktop_entries(to_cache) - - no_desktop_files = set() - - to_write = [] - - if desktop_files: - desktop_matches, no_exact_match = {}, set() - for pkg in to_cache: # first try to find exact matches - try: - clean_name = RE_CLEAN_NAME.sub('', pkg) - ends_with = re.compile(r'/usr/share/applications/{}.desktop$'.format(clean_name), re.IGNORECASE) - except: - raise - - for f in desktop_files: - if ends_with.match(f) and os.path.isfile(f): - desktop_matches[pkg] = f - break - - if pkg not in desktop_matches: - no_exact_match.add(pkg) - if no_exact_match: # check every not matched app individually - for pkgname in no_exact_match: - entries = pacman.list_desktop_entries({pkgname}) - - if entries: - if len(entries) > 1: - for e in entries: - if e.startswith('/usr/share/applications') and os.path.isfile(e): - desktop_matches[pkgname] = e - break - else: - if os.path.isfile(entries[0]): - desktop_matches[pkgname] = entries[0] - - if not desktop_matches: - no_desktop_files = to_cache - else: - if len(desktop_matches) != len(to_cache): - no_desktop_files = {p for p in to_cache if p not in desktop_matches} - - instances, apps_icons_noabspath = [], [] - - for pkgname, file in desktop_matches.items(): - p = pkgs[pkgname] - - with open(file) as f: - try: - desktop_entry = f.read() - p.desktop_entry = file - - for field in RE_DESKTOP_ENTRY.findall(desktop_entry): - if field[0] == 'Exec': - p.command = field[1].strip().replace('"', '') - elif field[0] == 'Icon': - p.icon_path = field[1].strip() - - if p.icon_path and '/' not in p.icon_path: # if the icon full path is not defined - apps_icons_noabspath.append(p) - elif field[0] == 'NoDisplay' and field[1].strip().lower() == 'true': - p.command = None - - if p.icon_path: - apps_icons_noabspath.remove(p.icon_path) - p.icon_path = None - except: - continue - - instances.append(p) - - if when_prepared: - when_prepared(p.name) - - if apps_icons_noabspath: - icon_paths = pacman.list_icon_paths({app.name for app in apps_icons_noabspath}) - if icon_paths: - for p in apps_icons_noabspath: - fill_icon_path(p, icon_paths, False) - - for p in instances: - to_write.append(p) - else: - no_desktop_files = {n for n in to_cache} + desktop_files = pacman.map_desktop_files(*to_cache) + + if after_desktop_files: + after_desktop_files() - if no_desktop_files: - bin_paths = pacman.list_bin_paths(no_desktop_files) - icon_paths = pacman.list_icon_paths(no_desktop_files) + if not desktop_files: + for pkgname in to_cache: + write(pkg=pkgs[pkgname], maintainer=maintainer, after_written=after_written) - for n in no_desktop_files: - p = pkgs[n] + return len(to_cache) + else: + for pkgname in to_cache: + pkgfiles = desktop_files.get(pkgname) - if bin_paths: - clean_name = RE_CLEAN_NAME.sub('', p.name) - ends_with = re.compile(r'.+/{}$'.format(clean_name), re.IGNORECASE) + if not pkgfiles: + write(pkg=pkgs[pkgname], maintainer=maintainer, after_written=after_written) + else: + desktop_entry = find_best_desktop_entry(pkgname, pkgfiles) - for path in bin_paths: - if ends_with.match(path): - p.command = path - break - if icon_paths: - fill_icon_path(p, icon_paths, only_exact_match=True) + if desktop_entry: + write(pkg=pkgs[pkgname], maintainer=maintainer, after_written=after_written, + desktop_file=desktop_entry[0], command=desktop_entry[1], icon=desktop_entry[2]) + else: + write(pkg=pkgs[pkgname], maintainer=maintainer, after_written=after_written) - to_write.append(p) + return len(to_cache) - if when_prepared: - when_prepared(p.name) - if to_write: - written = set() - for p in to_write: - if maintainer and not p.maintainer: - p.maintainer = maintainer +def find_best_desktop_entry(pkgname: str, desktop_files: List[str]) -> Optional[Tuple[str, str, str]]: + if len(desktop_files) == 1: + exec_icon = read_desktop_exec_and_icon(pkgname, desktop_files[0]) - write(p) + if exec_icon: + return desktop_files[0], exec_icon[0], exec_icon[1] + else: + # trying to find the exact name match: + for dfile in desktop_files: + if dfile.endswith('{}.desktop'.format(pkgname)): + exec_icon = read_desktop_exec_and_icon(pkgname, dfile) + + if exec_icon: + return dfile, exec_icon[0], exec_icon[1] + + # trying to find a close name match: + clean_name = RE_CLEAN_NAME.sub('', pkgname) + for dfile in desktop_files: + if dfile.endswith('{}.desktop'.format(clean_name)): + exec_icon = read_desktop_exec_and_icon(clean_name, dfile) + + if exec_icon: + return dfile, exec_icon[0], exec_icon[1] + + # finding any match: + for dfile in desktop_files: + exec_icon = read_desktop_exec_and_icon(pkgname, dfile) + + if exec_icon: + return dfile, exec_icon[0], exec_icon[1] + + +def read_desktop_exec_and_icon(pkgname: str, desktop_file: str) -> Optional[Tuple[str, str]]: + if os.path.isfile(desktop_file): + with open(desktop_file) as f: + possibilities = set() + + content = f.read() + cmd, icon = None, None + for field in RE_DESKTOP_ENTRY.findall(content): + if field[0] == 'Exec': + cmd = field[1].strip().replace('"', '') + elif field[0] == 'Icon': + icon = field[1].strip() + elif field[0] == 'NoDisplay' and field[1].strip().lower() == 'true': + cmd, icon = None, None + + if cmd and icon: + possibilities.add((cmd, icon)) + cmd, icon = None, None + + if possibilities: + if len(possibilities) == 1: + return [*possibilities][0] + else: + # trying to find the exact name x command match + for p in possibilities: + if p[0].startswith('{} '.format(pkgname)): + return p + + return sorted(possibilities)[0] # returning any possibility + + +def write(pkg: ArchPackage, desktop_file: Optional[str] = None, command: Optional[str] = None, + icon: Optional[str] = None, maintainer: Optional[str] = None, after_written: Optional[callable] = None): + pkg.desktop_entry = desktop_file + pkg.command = command + pkg.icon_path = icon + + if maintainer and not pkg.maintainer: + pkg.maintainer = maintainer - if after_written: - after_written(p.name) + Path(pkg.get_disk_cache_path()).mkdir(parents=True, exist_ok=True) - written.add(p.name) + data = pkg.get_data_to_cache() - if len(to_write) != len(to_cache): - for pkgname in to_cache: - if pkgname not in written: - Path(ArchPackage.disk_cache_path(pkgname)).mkdir(parents=True, exist_ok=True) - if after_written: - after_written(pkgname) + with open(pkg.get_disk_data_path(), 'w+') as f: + f.write(json.dumps(data)) - return len(to_write) - return 0 + if after_written: + after_written(pkg.name) diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index f0cfd0ef..323722eb 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -19,6 +19,7 @@ RE_REMOVE_TRANSITIVE_DEPS = re.compile(r'removing\s([\w\-_]+)\s.+required\sby\s([\w\-_]+)\n?') RE_AVAILABLE_MIRRORS = re.compile(r'.+\s+OK\s+.+\s+(\d+:\d+)\s+.+(http.+)') RE_PACMAN_SYNC_FIRST = re.compile(r'SyncFirst\s*=\s*(.+)') +RE_DESKTOP_FILES = re.compile(r'\n?([\w\-_]+)\s+(/usr/share/.+\.desktop)') def is_available() -> bool: @@ -153,42 +154,19 @@ def install_as_process(pkgpaths: Iterable[str], root_password: str, file: bool, shell=True) -def list_desktop_entries(pkgnames: Set[str]) -> List[str]: - if pkgnames: - installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]) - - desktop_files = [] - for out in new_subprocess(['grep', '-E', ".desktop$"], stdin=installed_files.stdout).stdout: - if out: - desktop_files.append(out.decode().strip()) - - return desktop_files - - -def list_icon_paths(pkgnames: Set[str]) -> List[str]: - installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]) - - icon_files = [] - for out in new_subprocess(['grep', '-E', '.(png|svg|xpm)$'], stdin=installed_files.stdout).stdout: - if out: - line = out.decode().strip() - if line: - icon_files.append(line) - - return icon_files - +def map_desktop_files(*pkgnames) -> Dict[str, List[str]]: + res = {} -def list_bin_paths(pkgnames: Set[str]) -> List[str]: - installed_files = new_subprocess(['pacman', '-Qlq', *pkgnames]) + if pkgnames: + output = run_cmd('pacman -Ql {}'.format(' '.join(pkgnames)), print_error=False) - bin_paths = [] - for out in new_subprocess(['grep', '-E', '^/usr/bin/.+'], stdin=installed_files.stdout).stdout: - if out: - line = out.decode().strip() - if line: - bin_paths.append(line) + if output: + for match in RE_DESKTOP_FILES.findall(output): + pkgfiles = res.get(match[0], []) + res[match[0]] = pkgfiles + pkgfiles.append(match[1]) - return bin_paths + return res def list_installed_files(pkgname: str) -> List[str]: diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index a65f0405..c799c0fe 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -181,10 +181,11 @@ arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks arch.sync_databases.substatus=Synchronizing package databases arch.sync_databases.substatus.error=It was not possible to synchronize the package database -arch.task.disk_cache=Organitzen dades dels paquets instal·lats -arch.task.disk_cache.indexed=Preparats -arch.task.disk_cache.prepared=Llegits -arch.task.disk_cache.reading=Determinant els paquets instal·lats +arch.task.disk_cache=Indexing packages data +arch.task.disk_cache.checking=Checking index +arch.task.disk_cache.indexed=Indexed +arch.task.disk_cache.indexing=Indexing +arch.task.disk_cache.reading_files=Reading files arch.task.mirrors=Refreshing mirrors arch.task.optimizing=Optimizing {} arch.task.sync_databases.waiting=Waiting for {} diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index ecfd6a5c..6fdada78 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -181,10 +181,11 @@ arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks arch.sync_databases.substatus=Synchronizing package databases arch.sync_databases.substatus.error=It was not possible to synchronize the package database -arch.task.disk_cache=Organizing data from installed packages -arch.task.disk_cache.indexed=Ready -arch.task.disk_cache.prepared=Read -arch.task.disk_cache.reading=Determining installed packages +arch.task.disk_cache=Indexing packages data +arch.task.disk_cache.checking=Checking index +arch.task.disk_cache.indexed=Indexed +arch.task.disk_cache.indexing=Indexing +arch.task.disk_cache.reading_files=Reading files arch.task.mirrors=Refreshing mirrors arch.task.optimizing=Optimizing {} arch.task.sync_databases.waiting=Waiting for {} diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 0151fcf9..e96cc144 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -182,10 +182,11 @@ arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks arch.sync_databases.substatus=Synchronizing package databases arch.sync_databases.substatus.error=It was not possible to synchronize the package database -arch.task.disk_cache=Organizing data from installed packages -arch.task.disk_cache.indexed=Ready -arch.task.disk_cache.prepared=Read -arch.task.disk_cache.reading=Determining installed packages +arch.task.disk_cache=Indexing packages data +arch.task.disk_cache.checking=Checking index +arch.task.disk_cache.indexed=Indexed +arch.task.disk_cache.indexing=Indexing +arch.task.disk_cache.reading_files=Reading files arch.task.mirrors=Refreshing mirrors arch.task.optimizing=Optimizing {} arch.task.sync_databases.waiting=Waiting for {} diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index e255ef77..a3a0ff49 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -181,10 +181,11 @@ arch.substatus.loading_files=Cargando archivos de los paquetes arch.substatus.pre_hooks=Ejecutando ganchos pre-transacción arch.sync_databases.substatus=Sincronizando bases de paquetes arch.sync_databases.substatus.error=No fue posible sincronizar la base de paquetes -arch.task.disk_cache=Organizando datos de paquetes instalados -arch.task.disk_cache.indexed=Listos -arch.task.disk_cache.prepared=Lidos -arch.task.disk_cache.reading=Determinando los paquetes instalados +arch.task.disk_cache=Indexando datos de paquetes +arch.task.disk_cache.checking=Comprobando índice +arch.task.disk_cache.indexed=Indexados +arch.task.disk_cache.indexing=Indexando +arch.task.disk_cache.reading_files=Lendo archivos arch.task.mirrors=Actualizando espejos arch.task.optimizing=Optimizando {} arch.task.sync_databases.waiting=Esperando por {} diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index f3bf0c22..84dbe716 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -181,10 +181,11 @@ arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks arch.sync_databases.substatus=Synchronizing package databases arch.sync_databases.substatus.error=It was not possible to synchronize the package database -arch.task.disk_cache=Organizzando i dati dai pacchetti installati -arch.task.disk_cache.indexed=Pronti -arch.task.disk_cache.prepared=Letti -arch.task.disk_cache.reading=Determinando i pacchetti installati +arch.task.disk_cache=Indexing packages data +arch.task.disk_cache.checking=Checking index +arch.task.disk_cache.indexed=Indexed +arch.task.disk_cache.indexing=Indexing +arch.task.disk_cache.reading_files=Reading files arch.task.mirrors=Aggiornando i mirror arch.task.optimizing=Ottimizzando {} arch.task.sync_databases.waiting=Waiting for {} diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 5ee94705..3ef9422f 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -181,10 +181,11 @@ arch.substatus.loading_files=Carregando os arquivos dos pacotes arch.substatus.pre_hooks=Executando ganchos pré-transação arch.sync_databases.substatus=Sincronizando bases de pacotes arch.sync_databases.substatus.error=Não foi possível sincronizar as bases de pacotes -arch.task.disk_cache=Organizando dados dos pacotes instalados -arch.task.disk_cache.indexed=Prontos -arch.task.disk_cache.prepared=Lidos -arch.task.disk_cache.reading=Determinando pacotes instalados +arch.task.disk_cache=Indexando dados de pacotes +arch.task.disk_cache.checking=Verificando índice +arch.task.disk_cache.indexed=Indexados +arch.task.disk_cache.indexing=Indexando +arch.task.disk_cache.reading_files=Lendo arquivos arch.task.mirrors=Atualizando espelhos arch.task.optimizing=Otimizando {} arch.task.sync_databases.waiting=Aguardando por {} diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 62700a0e..f353aa61 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -181,10 +181,11 @@ arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks arch.sync_databases.substatus=Синхронизация баз данных пакетов arch.sync_databases.substatus.error=Синхронизировать базу данных пакета не удалось -arch.task.disk_cache=Organizing data from installed packages -arch.task.disk_cache.indexed=Ready -arch.task.disk_cache.prepared=Read -arch.task.disk_cache.reading=Determining installed packages +arch.task.disk_cache=Indexing packages data +arch.task.disk_cache.checking=Checking index +arch.task.disk_cache.indexed=Indexed +arch.task.disk_cache.indexing=Indexing +arch.task.disk_cache.reading_files=Reading files arch.task.mirrors=Обновление зеркал arch.task.optimizing=Optimizing {} arch.task.sync_databases.waiting=Waiting for {} diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 8105a722..5b702b3c 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -181,10 +181,11 @@ arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks arch.sync_databases.substatus=Paket veritabanı eşitleniyor arch.sync_databases.substatus.error=Paket veritabanı eşitlenemedi -arch.task.disk_cache=Kurulu paket verileri düzenleniyor -arch.task.disk_cache.indexed=Hazır -arch.task.disk_cache.prepared=okunan -arch.task.disk_cache.reading=Kurulu paketler belirleniyor +arch.task.disk_cache=Indexing packages data +arch.task.disk_cache.checking=Checking index +arch.task.disk_cache.indexed=Indexed +arch.task.disk_cache.indexing=Indexing +arch.task.disk_cache.reading_files=Reading files arch.task.mirrors=Yansılar yenileniyor arch.task.optimizing=Uygun hale getiriliyor {} arch.task.sync_databases.waiting=Waiting for {} diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index 29261571..2a505101 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -66,8 +66,6 @@ def __init__(self, task_man: TaskManager, arch_config: dict, i18n: I18n, logger: self.task_man = task_man self.task_id = 'arch_cache_up' self.i18n = i18n - self.prepared = 0 - self.prepared_template = self.i18n['arch.task.disk_cache.prepared'] + ': {}/ {}' self.indexed = 0 self.indexed_template = self.i18n['arch.task.disk_cache.indexed'] + ': {}/ {}' self.to_index = 0 @@ -79,19 +77,18 @@ def __init__(self, task_man: TaskManager, arch_config: dict, i18n: I18n, logger: self.installed_hash_path = '{}/installed.sha1'.format(ARCH_CACHE_PATH) self.installed_cache_dir = '{}/installed'.format(ARCH_CACHE_PATH) - def update_prepared(self, pkgname: str, add: bool = True): - if add: - self.prepared += 1 - - sub = self.prepared_template.format(self.prepared, self.to_index) - progress = ((self.prepared + self.indexed) / self.progress) * 100 if self.progress > 0 else 0 - self.task_man.update_progress(self.task_id, progress, sub) - def update_indexed(self, pkgname: str): self.indexed += 1 sub = self.indexed_template.format(self.indexed, self.to_index) - progress = ((self.prepared + self.indexed) / self.progress) * 100 if self.progress > 0 else 0 - self.task_man.update_progress(self.task_id, progress, sub) + self.progress = self.progress + (self.indexed / self.to_index) * 50 + self.task_man.update_progress(self.task_id, self.progress, sub) + + def _update_progress(self, progress: float, msg: str): + self.progress = progress + self.task_man.update_progress(self.task_id, self.progress, msg) + + def _notify_reading_files(self): + self._update_progress(50, self.i18n['arch.task.disk_cache.indexing']) def run(self): if not any([self.aur, self.repositories]): @@ -100,19 +97,21 @@ def run(self): ti = time.time() self.task_man.register_task(self.task_id, self.i18n['arch.task.disk_cache'], get_icon_path()) - self.task_man.update_progress(self.task_id, 1, '') - self.logger.info("Checking already cached package data") + self._update_progress(1, self.i18n['arch.task.disk_cache.checking']) cache_dirs = [fpath for fpath in glob.glob('{}/*'.format(self.installed_cache_dir)) if os.path.isdir(fpath)] not_cached_names = None + self._update_progress(15, self.i18n['arch.task.disk_cache.checking']) if cache_dirs: # if there are cache data installed_names = pacman.list_installed_names() cached_pkgs = {cache_dir.split('/')[-1] for cache_dir in cache_dirs} not_cached_names = installed_names.difference(cached_pkgs) + self._update_progress(20, self.i18n['arch.task.disk_cache.checking']) + if not not_cached_names: self.task_man.update_progress(self.task_id, 100, '') self.task_man.finish_task(self.task_id) @@ -123,21 +122,23 @@ def run(self): self.logger.info('Pre-caching installed Arch packages data to disk') + self._update_progress(20, self.i18n['arch.task.disk_cache.checking']) installed = self.controller.read_installed(disk_loader=None, internet_available=self.internet_available, only_apps=False, pkg_types=None, limit=-1, names=not_cached_names, wait_disk_cache=False).installed - self.task_man.update_progress(self.task_id, 0, self.i18n['arch.task.disk_cache.reading']) + self._update_progress(35, self.i18n['arch.task.disk_cache.checking']) saved = 0 - pkgs = {p.name: p for p in installed if ((self.aur and p.repository == 'aur') or (self.repositories and p.repository != 'aur')) and not os.path.exists(p.get_disk_cache_path())} + pkgs = {p.name: p for p in installed if ((self.aur and p.repository == 'aur') or (self.repositories and p.repository != 'aur')) and not os.path.exists(p.get_disk_cache_path())} self.to_index = len(pkgs) - self.progress = self.to_index * 2 - self.update_prepared(None, add=False) # overwrite == True because the verification already happened - saved += disk.save_several(pkgs, when_prepared=self.update_prepared, after_written=self.update_indexed, overwrite=True) + self._update_progress(40, self.i18n['arch.task.disk_cache.reading_files']) + saved += disk.write_several(pkgs=pkgs, + after_desktop_files=self._notify_reading_files, + after_written=self.update_indexed, overwrite=True) self.task_man.update_progress(self.task_id, 100, None) self.task_man.finish_task(self.task_id) From 5b85c3716ffc39524d9bf6d4f647f9bd39b2d1cc Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 16:19:27 -0300 Subject: [PATCH 48/99] [ui] fix -> icons that cannot be rendered are being displayed as an empty space --- CHANGELOG.md | 1 + bauh/view/qt/apps_table.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 94ad2806..83f260e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - UI - crashing when nothing can be upgraded - random C++ wrapper errors with some forms due to missing references + - icons that cannot be rendered are being displayed as an empty space (now the type icon is displayed instead) ## [0.9.6] 2020-06-26 diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 397f4165..380e05a4 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -426,7 +426,11 @@ def _set_col_name(self, col: int, pkg: PackageView): else: try: icon = QIcon.fromTheme(icon_path) - self.icon_cache.add_non_existing(pkg.model.icon_url, {'icon': icon, 'bytes': None}) + + if icon.isNull(): + icon = QIcon(pkg.model.get_default_icon_path()) + else: + self.icon_cache.add_non_existing(pkg.model.icon_url, {'icon': icon, 'bytes': None}) except: icon = QIcon(pkg.model.get_default_icon_path()) From d8944e016cd438fa39a4bdca2bb08c29f0357cbc Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 16:42:14 -0300 Subject: [PATCH 49/99] Updating [CHANGELOG] --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83f260e6..3cc68dcc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [0.9.7] 2020 ### Features - Arch - - AUR: + - AUR - allowing to edit the PKGBUILD file of a package to be installed/upgraded/downgraded. If enabled, a popup will be displayed during this actions allowing the PKGBUILD to be edited.

@@ -86,7 +86,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - UI - crashing when nothing can be upgraded - random C++ wrapper errors with some forms due to missing references - - icons that cannot be rendered are being displayed as an empty space (now the type icon is displayed instead) + - application icons that cannot be rendered are being displayed as an empty space (now the type icon is displayed instead) ## [0.9.6] 2020-06-26 From c5fcf18be03f1983c5dc9f34bb52bce6502d584e Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 17:05:05 -0300 Subject: [PATCH 50/99] [ui] fix -> some application icons without a full path are not being rendered on the 'Upgrade summary' --- CHANGELOG.md | 1 + bauh/view/qt/components.py | 10 ++++++++-- bauh/view/qt/thread.py | 5 ++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cc68dcc..c3d10c95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - crashing when nothing can be upgraded - random C++ wrapper errors with some forms due to missing references - application icons that cannot be rendered are being displayed as an empty space (now the type icon is displayed instead) + - some application icons without a full path are not being rendered on the 'Upgrade summary' ## [0.9.6] 2020-06-26 diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 40b01f9a..ab219268 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -268,7 +268,10 @@ def __init__(self, model: InputOption, model_parent: SingleSelectComponent): self.toggled.connect(self._set_checked) if model.icon_path: - self.setIcon(QIcon(model.icon_path)) + if model.icon_path.startswith('/'): + self.setIcon(QIcon(model.icon_path)) + else: + self.setIcon(QIcon.fromTheme(model.icon_path)) if self.model.read_only: self.setAttribute(Qt.WA_TransparentForMouseEvents) @@ -291,7 +294,10 @@ def __init__(self, model: InputOption, model_parent: MultipleSelectComponent, ca self.setToolTip(model.tooltip) if model.icon_path: - self.setIcon(QIcon(model.icon_path)) + if model.icon_path.startswith('/'): + self.setIcon(QIcon(model.icon_path)) + else: + self.setIcon(QIcon.fromTheme(model.icon_path)) if model.read_only: self.setAttribute(Qt.WA_TransparentForMouseEvents) diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 10a66c95..818d74b8 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -9,6 +9,7 @@ import requests from PyQt5.QtCore import QThread, pyqtSignal +from PyQt5.QtGui import QIcon from bauh import LOGS_PATH from bauh.api.abstract.cache import MemoryCache @@ -196,7 +197,9 @@ def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_t if req.pkg.installed: icon_path = req.pkg.get_disk_icon_path() - if not icon_path or not os.path.isfile(icon_path): + if not icon_path: + icon_path = req.pkg.get_type_icon_path() + elif not os.path.isfile(icon_path) and QIcon.fromTheme(icon_path).isNull(): icon_path = req.pkg.get_type_icon_path() else: From a2f28afb13782c72660d3a228819c1a42332d8e8 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 17:14:59 -0300 Subject: [PATCH 51/99] [ui] improvement -> initializing dialog: changing the progress and substatus columns positions --- bauh/view/qt/prepare.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bauh/view/qt/prepare.py b/bauh/view/qt/prepare.py index 04d6c04d..e8d8d0c3 100644 --- a/bauh/view/qt/prepare.py +++ b/bauh/view/qt/prepare.py @@ -320,20 +320,20 @@ def _show_output(): lb_status.setStyleSheet("QLabel { font-weight: bold; }") self.table.setCellWidget(task_row, 1, lb_status) - lb_sub = QLabel() - lb_status.setCursor(Qt.WaitCursor) - lb_sub.setContentsMargins(10, 0, 10, 0) - lb_sub.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - lb_sub.setMinimumWidth(50) - self.table.setCellWidget(task_row, 2, lb_sub) - lb_progress = QLabel('{0:.2f}'.format(0) + '%') lb_progress.setCursor(Qt.WaitCursor) lb_progress.setContentsMargins(10, 0, 10, 0) lb_progress.setStyleSheet("QLabel { font-weight: bold; }") lb_progress.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) - self.table.setCellWidget(task_row, 3, lb_progress) + self.table.setCellWidget(task_row, 2, lb_progress) + + lb_sub = QLabel() + lb_status.setCursor(Qt.WaitCursor) + lb_sub.setContentsMargins(10, 0, 10, 0) + lb_sub.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) + lb_sub.setMinimumWidth(50) + self.table.setCellWidget(task_row, 3, lb_sub) self.tasks[id_] = {'bt_icon': bt_icon, 'lb_status': lb_status, @@ -352,7 +352,7 @@ def update_progress(self, task_id: str, progress: float, substatus: str): task['lb_prog'].setText('{0:.2f}'.format(progress) + '%') if substatus: - task['lb_sub'].setText('( {} )'.format(substatus)) + task['lb_sub'].setText('({})'.format(substatus)) else: task['lb_sub'].setText('') From fa316e3fb84bd7fbde5fa994004635e91b77aaf6 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 17:17:30 -0300 Subject: [PATCH 52/99] [arch] fix -> indexing task progress number --- bauh/gems/arch/worker.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bauh/gems/arch/worker.py b/bauh/gems/arch/worker.py index 2a505101..38867518 100644 --- a/bauh/gems/arch/worker.py +++ b/bauh/gems/arch/worker.py @@ -80,8 +80,8 @@ def __init__(self, task_man: TaskManager, arch_config: dict, i18n: I18n, logger: def update_indexed(self, pkgname: str): self.indexed += 1 sub = self.indexed_template.format(self.indexed, self.to_index) - self.progress = self.progress + (self.indexed / self.to_index) * 50 - self.task_man.update_progress(self.task_id, self.progress, sub) + progress = self.progress + (self.indexed / self.to_index) * 50 + self.task_man.update_progress(self.task_id, progress, sub) def _update_progress(self, progress: float, msg: str): self.progress = progress From 3978bfc0efac106c90ff4ad4210c0eaa9a2761f6 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 17:21:36 -0300 Subject: [PATCH 53/99] Updating CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3d10c95..34afa926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - default search path set to '~/Downloads' - trying to auto-fill the 'Name' and 'Version' fields - Arch - - initializing task "Organizing data from installed packages" is taking about 80% less time (now is called "Indexing packages data") + - initializing task "Organizing data from installed packages" is taking about 80% less time (now is called "Indexing packages data") [#131](https://github.com/vinifmor/bauh/issues/131) - upgrade summary: displaying the reason a given package must be installed

From 55876172f6e39551daceda8acb17279df11e0933 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 17:27:03 -0300 Subject: [PATCH 54/99] [ui] improvement -> settings dialog: not displaying point hand cursors for the lower buttons --- bauh/view/qt/settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bauh/view/qt/settings.py b/bauh/view/qt/settings.py index 43180933..d3192b4c 100644 --- a/bauh/view/qt/settings.py +++ b/bauh/view/qt/settings.py @@ -2,6 +2,7 @@ from io import StringIO from PyQt5.QtCore import QSize, Qt, QCoreApplication +from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QWidget, QVBoxLayout, QToolBar, QSizePolicy, QPushButton from bauh import __app_name__ @@ -36,6 +37,7 @@ def __init__(self, manager: SoftwareManager, i18n: I18n, screen_size: QSize, win action_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) bt_close = QPushButton() + bt_close.setCursor(QCursor(Qt.PointingHandCursor)) bt_close.setText(self.i18n['close'].capitalize()) bt_close.clicked.connect(lambda: self.close()) action_bar.addWidget(bt_close) @@ -43,6 +45,7 @@ def __init__(self, manager: SoftwareManager, i18n: I18n, screen_size: QSize, win action_bar.addWidget(new_spacer()) bt_change = QPushButton() + bt_change.setCursor(QCursor(Qt.PointingHandCursor)) bt_change.setStyleSheet(css.OK_BUTTON) bt_change.setText(self.i18n['change'].capitalize()) bt_change.clicked.connect(self._save_settings) From 019444347040f6768bb444e29b36558f135fe0dd Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 21 Aug 2020 17:36:43 -0300 Subject: [PATCH 55/99] [flatpak] fix -> not able to make the exports directory (missing parents argument) --- bauh/gems/flatpak/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 34011d57..81b86365 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -324,7 +324,7 @@ def _make_exports_dir(self, watcher: ProcessWatcher) -> bool: self.logger.info("Creating dir '{}'".format(EXPORTS_PATH)) watcher.print('Creating dir {}'.format(EXPORTS_PATH)) try: - os.mkdir(EXPORTS_PATH) + Path(EXPORTS_PATH).mkdir(parents=True, exist_ok=True) except: watcher.print('Error while creating the directory {}'.format(EXPORTS_PATH)) return False From 50893bff99543aff0cdeb58783109a38749b5037 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 26 Aug 2020 12:54:56 -0300 Subject: [PATCH 56/99] [snap] improvement -> full support refactored to use the Snapd socket instead of the Ubuntu's old Snap API --- CHANGELOG.md | 3 + bauh/api/abstract/controller.py | 4 +- bauh/gems/snap/constants.py | 1 - bauh/gems/snap/controller.py | 250 +++++++++++++++-------------- bauh/gems/snap/model.py | 47 ++++-- bauh/gems/snap/resources/locale/ca | 5 +- bauh/gems/snap/resources/locale/de | 5 +- bauh/gems/snap/resources/locale/en | 5 +- bauh/gems/snap/resources/locale/es | 5 +- bauh/gems/snap/resources/locale/it | 5 +- bauh/gems/snap/resources/locale/pt | 5 +- bauh/gems/snap/resources/locale/ru | 5 +- bauh/gems/snap/resources/locale/tr | 5 +- bauh/gems/snap/snap.py | 213 +----------------------- bauh/gems/snap/snapd.py | 136 ++++++++++++++++ bauh/gems/snap/worker.py | 70 -------- 16 files changed, 346 insertions(+), 418 deletions(-) delete mode 100644 bauh/gems/snap/constants.py create mode 100644 bauh/gems/snap/snapd.py delete mode 100644 bauh/gems/snap/worker.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 34afa926..7ecab792 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Flatpak - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) - downgrade function refactored +- Snap + - full support refactored to use the Snapd socket instead of the Ubuntu's old Snap API (which was recently disabled). Now the 'read' operations are faster, a only the icon is cached to the disk. - minor UI improvements @@ -80,6 +82,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) - minor fixes - Snap + - not able to install classic Snaps due to Ubuntu's old Snaps API shutdown - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) - Web - some environment variable are not available during the launch process diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 3b7d5774..8ce3f477 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -3,7 +3,7 @@ import shutil from abc import ABC, abstractmethod from pathlib import Path -from typing import List, Set, Type, Tuple +from typing import List, Set, Type, Tuple, Optional import yaml @@ -17,7 +17,7 @@ class SearchResult: - def __init__(self, installed: List[SoftwarePackage], new: List[SoftwarePackage], total: int): + def __init__(self, installed: Optional[List[SoftwarePackage]], new: Optional[List[SoftwarePackage]], total: int): """ :param installed: already installed packages :param new: new packages found diff --git a/bauh/gems/snap/constants.py b/bauh/gems/snap/constants.py deleted file mode 100644 index 099a232e..00000000 --- a/bauh/gems/snap/constants.py +++ /dev/null @@ -1 +0,0 @@ -SNAP_API_URL = 'https://search.apps.ubuntu.com/api/v1' diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 6ad82683..048380b8 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -1,25 +1,23 @@ import re import time -from datetime import datetime from threading import Thread -from typing import List, Set, Type +from typing import List, Set, Type, Optional from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \ TransactionResult from bauh.api.abstract.disk import DiskCacheLoader from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ - SuggestionPriority, CustomSoftwareAction + SuggestionPriority, CustomSoftwareAction, PackageStatus from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption from bauh.commons import resource, internet from bauh.commons.category import CategoriesDownloader from bauh.commons.html import bold -from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess +from bauh.commons.system import SystemProcess, ProcessHandler, new_root_subprocess, get_human_size_str from bauh.gems.snap import snap, URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH, SUGGESTIONS_FILE, \ - get_icon_path -from bauh.gems.snap.constants import SNAP_API_URL + get_icon_path, snapd from bauh.gems.snap.model import SnapApplication -from bauh.gems.snap.worker import SnapAsyncDataLoader +from bauh.gems.snap.snapd import SnapdClient RE_AVAILABLE_CHANNELS = re.compile(re.compile(r'(\w+)\s+(snap install.+)')) @@ -47,36 +45,12 @@ def __init__(self, context: ApplicationContext): i18n_confirm_key='snap.action.refresh.confirm') ] - def get_info_path(self) -> str: - if self.info_path is None: - self.info_path = snap.get_app_info_path() - - return self.info_path - - def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoader, internet: bool = True) -> SnapApplication: - app = SnapApplication(publisher=app_json.get('publisher'), - rev=app_json.get('rev'), - notes=app_json.get('notes'), - has_apps_field=app_json.get('apps_field', False), - id=app_json.get('name'), - name=app_json.get('name'), - version=app_json.get('version'), - latest_version=app_json.get('version'), - description=app_json.get('description', app_json.get('summary')), - verified_publisher=app_json.get('developer_validation', '') == 'verified', - extra_actions=self.custom_actions) - - if app.publisher and app.publisher.endswith('*'): - app.verified_publisher = True - app.publisher = app.publisher.replace('*', '') - + def _fill_categories(self, app: SnapApplication): categories = self.categories.get(app.name.lower()) if categories: app.categories = categories - app.installed = installed - if not app.is_application(): categories = app.categories @@ -87,70 +61,67 @@ def map_json(self, app_json: dict, installed: bool, disk_loader: DiskCacheLoade if 'runtime' not in categories: categories.append('runtime') - api_data = self.api_cache.get(app_json['name']) - expired_data = api_data and api_data.get('expires_at') and api_data['expires_at'] <= datetime.utcnow() - - if (not api_data or expired_data) and app.is_application(): - if disk_loader and app.installed: - disk_loader.fill(app) - - if internet: - SnapAsyncDataLoader(app=app, api_cache=self.api_cache, manager=self, context=self.context).start() - else: - app.fill_cached_data(api_data) - - return app - def search(self, words: str, disk_loader: DiskCacheLoader, limit: int = -1, is_url: bool = False) -> SearchResult: - if is_url: + if is_url or (not snap.is_installed() and not snapd.is_running()): return SearchResult([], [], 0) - if snap.is_snapd_running(): - installed = self.read_installed(disk_loader).installed + snapd_client = SnapdClient(self.logger) + apps_found = snapd_client.query(words) - res = SearchResult([], [], 0) + res = SearchResult([], [], 0) - for app_json in snap.search(words): + if apps_found: + installed = self.read_installed(disk_loader).installed + for app_json in apps_found: already_installed = None if installed: - already_installed = [i for i in installed if i.id == app_json.get('name')] + already_installed = [i for i in installed if i.id == app_json.get('id')] already_installed = already_installed[0] if already_installed else None if already_installed: res.installed.append(already_installed) else: - res.new.append(self.map_json(app_json, installed=False, disk_loader=disk_loader)) + res.new.append(self._map_to_app(app_json, installed=False)) - res.total = len(res.installed) + len(res.new) - return res - else: - return SearchResult([], [], 0) + res.total = len(res.installed) + len(res.new) + return res def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_apps: bool = False, pkg_types: Set[Type[SoftwarePackage]] = None, internet_available: bool = None) -> SearchResult: - info_path = self.get_info_path() - - if snap.is_snapd_running() and info_path: - installed = [self.map_json(app_json, installed=True, disk_loader=disk_loader, internet=internet_available) for app_json in snap.read_installed(info_path)] + if snap.is_installed() and snapd.is_running(): + snapd_client = SnapdClient(self.logger) + app_names = {a['snap'] for a in snapd_client.list_only_apps()} + installed = [self._map_to_app(app_json=appjson, + installed=True, + disk_loader=disk_loader, + is_application=app_names and appjson['name'] in app_names) for appjson in snapd_client.list_all_snaps()] return SearchResult(installed, None, len(installed)) else: return SearchResult([], None, 0) def downgrade(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: + if not snap.is_installed(): + watcher.print("'snap' seems not to be installed") + return False + if not snapd.is_running(): + watcher.print("'snapd' seems not to be running") + return False + 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__)) def uninstall(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher, disk_loader: DiskCacheLoader) -> TransactionResult: - uninstalled = ProcessHandler(watcher).handle_simple(snap.uninstall_and_stream(pkg.name, root_password))[0] + if snap.is_installed() and snapd.is_running(): + uninstalled = ProcessHandler(watcher).handle_simple(snap.uninstall_and_stream(pkg.name, root_password))[0] - if uninstalled: - if self.suggestions_cache: - self.suggestions_cache.delete(pkg.name) + if uninstalled: + if self.suggestions_cache: + self.suggestions_cache.delete(pkg.name) - return TransactionResult(success=True, installed=None, removed=[pkg]) + return TransactionResult(success=True, installed=None, removed=[pkg]) return TransactionResult.fail() @@ -162,17 +133,29 @@ def clean_cache_for(self, pkg: SnapApplication): self.api_cache.delete(pkg.id) def get_info(self, pkg: SnapApplication) -> dict: - info = snap.get_info(pkg.name, attrs=('license', 'contact', 'commands', 'snap-id', 'tracking', 'installed')) - info['description'] = pkg.description - info['publisher'] = pkg.publisher - info['revision'] = pkg.rev - info['name'] = pkg.name - - if info.get('commands'): - info['commands'] = ' '.join(info['commands']) - - if info.get('license') and info['license'] == 'unset': - del info['license'] + info = { + 'description': pkg.description, + 'developer': pkg.developer, + 'license': pkg.license, + 'contact': pkg.contact, + 'snap-id': pkg.id, + 'name': pkg.name, + 'publisher': pkg.publisher, + 'revision': pkg.rev, + 'tracking': pkg.tracking, + 'channel': pkg.channel, + 'type': pkg.type + } + + if pkg.installed: + commands = [*{c['name'] for c in SnapdClient(self.logger).list_commands(pkg.name)}] + commands.sort() + info['commands'] = commands + + if pkg.installed_size: + info['installed_size']: get_human_size_str(pkg.installed_size) + elif pkg.download_size: + info['download_size'] = get_human_size_str(pkg.download_size) return info @@ -180,13 +163,16 @@ def get_history(self, pkg: SnapApplication) -> PackageHistory: raise Exception("'get_history' is not supported by {}".format(pkg.__class__.__name__)) def install(self, pkg: SnapApplication, root_password: str, disk_loader: DiskCacheLoader, watcher: ProcessWatcher) -> TransactionResult: - info_path = self.get_info_path() + # retrieving all installed so it will be possible to know the additional installed runtimes after the operation succeeds + if not snap.is_installed(): + watcher.print("'snap' seems not to be installed") + return TransactionResult.fail() - if not info_path: - self.logger.warning('Information directory was not found. It will not be possible to determine if the installed application can be launched') + if not snapd.is_running(): + watcher.print("'snapd' seems not to be running") + return TransactionResult.fail() - # retrieving all installed so it will be possible to know the additional installed runtimes after the operation succeeds - installed_names = snap.list_installed_names() + installed_names = {s['name'] for s in SnapdClient(self.logger).list_all_snaps()} res, output = ProcessHandler(watcher).handle_simple(snap.install_and_stream(pkg.name, pkg.confinement, root_password)) @@ -208,17 +194,10 @@ def install(self, pkg: SnapApplication, root_password: str, disk_loader: DiskCac deny_label=self.i18n['cancel']): self.logger.info("Installing '{}' with the custom command '{}'".format(pkg.name, channel_select.value)) res = ProcessHandler(watcher).handle(SystemProcess(new_root_subprocess(channel_select.value.value.split(' '), root_password=root_password))) - - if res and info_path: - pkg.has_apps_field = snap.has_apps_field(pkg.name, info_path) - 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)) - else: - if info_path: - pkg.has_apps_field = snap.has_apps_field(pkg.name, info_path) return self._gen_installation_response(success=res, pkg=pkg, installed=installed_names, disk_loader=disk_loader) @@ -278,7 +257,7 @@ def list_updates(self, internet_available: bool) -> List[PackageUpdate]: def list_warnings(self, internet_available: bool) -> List[str]: if snap.is_installed(): - if not snap.is_snapd_running(): + 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(), @@ -291,24 +270,57 @@ def list_warnings(self, internet_available: bool) -> List[str]: self.logger.warning('It seems Snap API is not available. Search output: {}'.format(output)) return [self.i18n['snap.notifications.api.unavailable'].format(bold('Snaps'), bold('Snap'))] - def _fill_suggestion(self, pkg_name: str, priority: SuggestionPriority, out: List[PackageSuggestion]): - res = self.http_client.get_json(SNAP_API_URL + '/search?q=package_name:{}'.format(pkg_name)) + def _fill_suggestion(self, name: str, priority: SuggestionPriority, snapd_client: SnapdClient, out: List[PackageSuggestion]): + res = snapd_client.find_by_name(name) - if res and res['_embedded']['clickindex:package']: - pkg = res['_embedded']['clickindex:package'][0] - pkg['rev'] = pkg['revision'] - pkg['name'] = pkg_name + if res: + if len(res) == 1: + app_json = res[0] + else: + jsons_found = [p for p in res if p['name'] == name] + app_json = jsons_found[0] if jsons_found else None - sug = PackageSuggestion(self.map_json(pkg, installed=False, disk_loader=None), priority) - self.suggestions_cache.add(pkg_name, sug) - out.append(sug) - else: - self.logger.warning("Could not retrieve suggestion '{}'".format(pkg_name)) + if app_json: + sug = PackageSuggestion(self._map_to_app(app_json, False), priority) + self.suggestions_cache.add(name, sug) + out.append(sug) + return + + self.logger.warning("Could not retrieve suggestion '{}'".format(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'), + name=app_json.get('name'), + license=app_json.get('license'), + version=app_json.get('version'), + latest_version=app_json.get('version'), + description=app_json.get('description', app_json.get('summary')), + installed=installed, + rev=app_json.get('revision'), + publisher=app_json['publisher'].get('display-name', app_json['publisher'].get('username')), + verified_publisher=app_json['publisher'].get('validation') == 'verified', + icon_url=app_json.get('icon'), + screenshots={m['url'] for m in app_json.get('media', []) if m['type'] == 'screenshot'}, + download_size=app_json.get('download-size'), + channel=app_json.get('channel'), + confinement=app_json.get('confinement'), + app_type=app_json.get('type'), + app=is_application, + installed_size=app_json.get('installed-size'), + extra_actions=self.custom_actions) + + if disk_loader and app.installed: + disk_loader.fill(app) + + self._fill_categories(app) + + app.status = PackageStatus.READY + return app def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSuggestion]: res = [] - if snap.is_snapd_running(): + if snapd.is_running(): self.logger.info('Downloading suggestions file {}'.format(SUGGESTIONS_FILE)) file = self.http_client.get(SUGGESTIONS_FILE) @@ -319,7 +331,8 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSu self.logger.info('Mapping suggestions') suggestions, threads = [], [] - installed = {i.name.lower() for i in self.read_installed(disk_loader=None).installed} if filter_installed else None + snapd_client = SnapdClient(self.logger) + installed = {s['name'].lower() for s in snapd_client.list_all_snaps()} for l in file.text.split('\n'): if l: @@ -333,7 +346,7 @@ def list_suggestions(self, limit: int, filter_installed: bool) -> List[PackageSu if cached_sug: res.append(cached_sug) else: - t = Thread(target=self._fill_suggestion, args=(name, SuggestionPriority(int(sug[0])), res)) + t = Thread(target=self._fill_suggestion, args=(name, SuggestionPriority(int(sug[0])), snapd_client, res)) t.start() threads.append(t) time.sleep(0.001) # to avoid being blocked @@ -350,22 +363,21 @@ def is_default_enabled(self) -> bool: return True def launch(self, pkg: SnapApplication): - snap.run(pkg, self.context.logger) + commands = SnapdClient(self.logger).list_commands(pkg.name) - def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: - res = self.http_client.get_json('{}/search?q={}'.format(SNAP_API_URL, pkg.name)) - - if res: - if res.get('_embedded') and res['_embedded'].get('clickindex:package'): - snap_data = res['_embedded']['clickindex:package'][0] + if commands: + if len(commands) == 1: + cmd = commands[0]['name'] + else: + desktop_cmd = [c for c in commands if 'desktop-file' in c] - if snap_data.get('screenshot_urls'): - return snap_data['screenshot_urls'] + if desktop_cmd: + cmd = desktop_cmd[0]['name'] else: - self.logger.warning("No 'screenshots_urls' defined for {}".format(pkg)) - else: - self.logger.error('It seems the API is returning a different response: {}'.format(res)) - else: - self.logger.warning('Could not retrieve data for {}'.format(pkg)) + cmd = commands[0]['name'] + + self.logger.info("Running '{}': {}".format(pkg.name, cmd)) + snap.run(cmd) - return [] + def get_screenshots(self, pkg: SnapApplication) -> List[str]: + return pkg.screenshots if pkg.has_screenshots() else [] diff --git a/bauh/gems/snap/model.py b/bauh/gems/snap/model.py index e3ec5984..58e787af 100644 --- a/bauh/gems/snap/model.py +++ b/bauh/gems/snap/model.py @@ -1,26 +1,46 @@ -from typing import List +from typing import List, Optional, Set from bauh.api.abstract.model import SoftwarePackage, CustomSoftwareAction from bauh.commons import resource from bauh.gems.snap import ROOT_DIR -KNOWN_RUNTIME_NAMES = {'snapd', 'snapcraft', 'multipass'} - class SnapApplication(SoftwarePackage): def __init__(self, id: str = None, name: str = None, version: str = None, latest_version: str = None, description: str = None, publisher: str = None, rev: str = None, notes: str = None, - confinement: str = None, has_apps_field: bool = None, verified_publisher: bool = False, extra_actions: List[CustomSoftwareAction] = None): + confinement: str = None, verified_publisher: bool = False, + extra_actions: List[CustomSoftwareAction] = None, + screenshots: Optional[Set[str]] = None, + license: Optional[str] = None, + installed: bool = False, + icon_url: Optional[str] = None, + download_size: Optional[int] = None, + developer: Optional[str] = None, + contact: Optional[str] = None, + tracking: Optional[str] = None, + app_type: Optional[str] = None, + channel: Optional[str] = None, + app: bool = False, + installed_size: Optional[int] = None): super(SnapApplication, self).__init__(id=id, name=name, version=version, - latest_version=latest_version, description=description) + latest_version=latest_version, description=description, + license=license, installed=installed, icon_url=icon_url) self.publisher = publisher self.rev = rev self.notes = notes self.confinement = confinement - self.has_apps_field = has_apps_field self.verified_publisher = verified_publisher self.extra_actions = extra_actions + self.screenshots = screenshots + self.download_size = download_size + self.developer = developer + self.contact = contact + self.tracking = tracking + self.type = app_type + self.channel = channel + self.app = app + self.installed_size = installed_size def supports_disk_cache(self): return self.installed @@ -44,7 +64,10 @@ def get_type_icon_path(self): return self.get_default_icon_path() def is_application(self) -> bool: - return not self.installed or ((self.has_apps_field is None or self.has_apps_field) and self.name.lower() not in KNOWN_RUNTIME_NAMES) + if self.installed: + return self.app + else: + return self.type == 'app' def get_disk_cache_path(self): return super(SnapApplication, self).get_disk_cache_path() + '/installed/' + self.name @@ -54,20 +77,17 @@ def is_trustable(self) -> bool: def get_data_to_cache(self): return { - "icon_url": self.icon_url, - 'confinement': self.confinement, - 'description': self.description, 'categories': self.categories } def fill_cached_data(self, data: dict): if data: - for base_attr in ('icon_url', 'description', 'confinement', 'categories'): + for base_attr in self.get_data_to_cache().keys(): if data.get(base_attr): setattr(self, base_attr, data[base_attr]) def can_be_run(self) -> bool: - return self.installed and self.is_application() + return bool(self.installed and self.is_application()) def get_publisher(self): return self.publisher @@ -79,6 +99,9 @@ def get_custom_supported_actions(self) -> List[CustomSoftwareAction]: def supports_backup(self) -> bool: return True + def has_screenshots(self) -> bool: + return not self.installed and self.screenshots + def __eq__(self, other): if isinstance(other, SnapApplication): return self.name == other.name diff --git a/bauh/gems/snap/resources/locale/ca b/bauh/gems/snap/resources/locale/ca index 04da1593..935f9ae1 100644 --- a/bauh/gems/snap/resources/locale/ca +++ b/bauh/gems/snap/resources/locale/ca @@ -2,15 +2,18 @@ gem.snap.info=Aplicacions publicades a https://snapcraft.io/store snap.action.refresh.confirm=Actualitza {} ? snap.action.refresh.label=Actualitza snap.action.refresh.status=S’està actualitzant +snap.info.channel=channel snap.info.commands=ordres snap.info.contact=contacte snap.info.description=descripció +snap.info.download_size=Size (Download) snap.info.installed=instal·lada +snap.info.installed_size=Size (Instalattion) snap.info.license=llicència snap.info.publisher=publicador snap.info.revision=revisió -snap.info.size=mida snap.info.tracking=seguiment +snap.info.type=type snap.info.version=versió snap.install.available_channels.help=Seleccioneu-ne un si voleu continuar snap.install.available_channels.message=No hi ha un canal {} (stable) disponible per a {}. Però més avall n’hi ha els següents diff --git a/bauh/gems/snap/resources/locale/de b/bauh/gems/snap/resources/locale/de index da013a37..7a672965 100644 --- a/bauh/gems/snap/resources/locale/de +++ b/bauh/gems/snap/resources/locale/de @@ -2,15 +2,18 @@ gem.snap.info=Anwendungen von https://snapcraft.io/store snap.action.refresh.confirm=Erneuern {} ? snap.action.refresh.label=Erneuern snap.action.refresh.status=Erneuern +snap.info.channel=channel snap.info.commands=Kommandos snap.info.contact=Kontakt snap.info.description=Beschreibung +snap.info.download_size=Size (Download) snap.info.installed=installiert +snap.info.installed_size=Size (Instalattion) snap.info.license=Lizenz snap.info.publisher=Herausgeber snap.info.revision=Revision -snap.info.size=Größe snap.info.tracking=Tracking +snap.info.type=type snap.info.version=Version snap.install.available_channels.help=Wähle einen aus um fortzufahren snap.install.available_channels.message=Es ist kein {} Channel verfügbar für {}. Es gibt jedoch folgende diff --git a/bauh/gems/snap/resources/locale/en b/bauh/gems/snap/resources/locale/en index 5dc82da9..46ed39d0 100644 --- a/bauh/gems/snap/resources/locale/en +++ b/bauh/gems/snap/resources/locale/en @@ -2,15 +2,18 @@ gem.snap.info=Applications published at https://snapcraft.io/store snap.action.refresh.confirm=Refresh {} ? snap.action.refresh.label=Refresh snap.action.refresh.status=Refreshing +snap.info.channel=channel snap.info.commands=commands snap.info.contact=contact snap.info.description=description +snap.info.download_size=Size (Download) snap.info.installed=installed +snap.info.installed_size=Size (Instalattion) snap.info.license=license snap.info.publisher=publisher snap.info.revision=revision -snap.info.size=size snap.info.tracking=tracking +snap.info.type=type snap.info.version=version snap.install.available_channels.help=Select one if you want to continue snap.install.available_channels.message=There is no {} channel available for {}. But there are these below diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index f6494044..d2d612f3 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -2,15 +2,18 @@ gem.snap.info=Aplicativos publicados en https://snapcraft.io/store snap.action.refresh.confirm=Actualizar {} ? snap.action.refresh.label=Actualizar snap.action.refresh.status=Actualizando +snap.info.channel=canal snap.info.commands=comandos snap.info.contact=contacto snap.info.description=descripción +snap.info.download_size=Tamaño (Descarga) snap.info.installed=instalado +snap.info.installed_size=Tamaño (Instalación) snap.info.license=licencia snap.info.publisher=publicador snap.info.revision=revisión -snap.info.size=tamaño snap.info.tracking=tracking +snap.info.type=tipo snap.info.version=versión snap.install.available_channels.help=Seleccione uno si desea continuar snap.install.available_channels.message=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros abajo diff --git a/bauh/gems/snap/resources/locale/it b/bauh/gems/snap/resources/locale/it index 6d66aee1..254cfadc 100644 --- a/bauh/gems/snap/resources/locale/it +++ b/bauh/gems/snap/resources/locale/it @@ -2,15 +2,18 @@ gem.snap.info=Applicazioni pubblicate su https://snapcraft.io/store snap.action.refresh.confirm=Ripristina {} ? snap.action.refresh.label=Ripristina snap.action.refresh.status=Ripristinare +snap.info.channel=channel snap.info.commands=commands snap.info.contact=contact snap.info.description=description +snap.info.download_size=Size (Download) snap.info.installed=installed +snap.info.installed_size=Size (Instalattion) snap.info.license=license snap.info.publisher=publisher snap.info.revision=revision -snap.info.size=size snap.info.tracking=tracking +snap.info.type=type snap.info.version=version snap.install.available_channels.help=Selezionane uno se vuoi continuare snap.install.available_channels.message=Non esiste un {} canale disponibile per {}. Ma ci sono questi sotto diff --git a/bauh/gems/snap/resources/locale/pt b/bauh/gems/snap/resources/locale/pt index 35223efa..54fafa34 100644 --- a/bauh/gems/snap/resources/locale/pt +++ b/bauh/gems/snap/resources/locale/pt @@ -2,15 +2,18 @@ gem.snap.info=Aplicativos publicados em https://snapcraft.io/store snap.action.refresh.confirm=Atualizar {} ? snap.action.refresh.label=Atualizar snap.action.refresh.status=Atualizando +snap.info.channel=canal snap.info.commands=comandos snap.info.contact=contato snap.info.description=descrição +snap.info.download_size=Tamanho (Download) snap.info.installed=instalado +snap.info.installed_size=Tamanho (Instalação) snap.info.license=licença snap.info.publisher=publicador snap.info.revision=revisão -snap.info.size=tamanho snap.info.tracking=tracking +snap.info.type=tipo snap.info.version=versão snap.install.available_channels.help=Selecione algum se quiser continuar snap.install.available_channels.message=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros abaixo diff --git a/bauh/gems/snap/resources/locale/ru b/bauh/gems/snap/resources/locale/ru index a7d60891..76fc3d96 100644 --- a/bauh/gems/snap/resources/locale/ru +++ b/bauh/gems/snap/resources/locale/ru @@ -2,15 +2,18 @@ gem.snap.info=Приложения, опубликованные на https://sn snap.action.refresh.confirm=Обновить {} ? snap.action.refresh.label=Обновить snap.action.refresh.status=Обновляется +snap.info.channel=channel snap.info.commands=Команды snap.info.contact=Контакт snap.info.description=Описание +snap.info.download_size=Size (Download) snap.info.installed=Размер установки +snap.info.installed_size=Size (Instalattion) snap.info.license=Лицензия snap.info.publisher=Издатель snap.info.revision=Ревизия -snap.info.size=Размер snap.info.tracking=Отслеживание +snap.info.type=type snap.info.version=Версия snap.install.available_channels.help=Выберите один, если вы хотите продолжить snap.install.available_channels.message=Нет одного канала {}, доступного для {}. Но есть такие ниже diff --git a/bauh/gems/snap/resources/locale/tr b/bauh/gems/snap/resources/locale/tr index 9347c7ce..9799d921 100644 --- a/bauh/gems/snap/resources/locale/tr +++ b/bauh/gems/snap/resources/locale/tr @@ -2,15 +2,18 @@ gem.snap.info=https://snapcraft.io/store adresinde yayınlanan uygulamalar snap.action.refresh.confirm=Yenile {} ? snap.action.refresh.label=Yenile snap.action.refresh.status=Yenileniyor +snap.info.channel=channel snap.info.commands=komutlar snap.info.contact=iletişim snap.info.description=açıklama +snap.info.download_size=Size (Download) snap.info.installed=yüklü +snap.info.installed_size=Size (Instalattion) snap.info.license=lisans snap.info.publisher=yayıncı snap.info.revision=revizyon -snap.info.size=boyut snap.info.tracking=izleme +snap.info.type=type snap.info.version=sürüm snap.install.available_channels.help=Devam etmek istiyorsanız birini seçin snap.install.available_channels.message={} için hiçbir {} kanalı yok. Ama aşağıda bunlar var diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index d114b5fa..74e0e890 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -1,174 +1,15 @@ -import logging import os -import re import subprocess from io import StringIO -from typing import List, Tuple, Set +from typing import Tuple -from bauh.commons.system import run_cmd, new_subprocess, SimpleProcess -from bauh.gems.snap.model import SnapApplication +from bauh.commons.system import run_cmd, SimpleProcess BASE_CMD = 'snap' -RE_SNAPD_STATUS = re.compile('\s+') -RE_SNAPD_SERVICES = re.compile(r'snapd\.\w+.+') -def is_installed(): - res = run_cmd('which snap', print_error=False) - return res and not res.strip().startswith('which ') - - -def is_snapd_running() -> bool: - output = run_cmd('systemctl list-units') - - snapd_services = RE_SNAPD_SERVICES.findall(output) - - socket, socket_running, service, service_running = False, False, False, False - if snapd_services: - for service_line in snapd_services: - line_split = RE_SNAPD_STATUS.split(service_line) - - running = line_split[3] in {'listening', 'running'} - - if line_split[0] == 'snapd.service': - service = True - service_running = running - elif line_split[0] == 'snapd.socket': - socket = True - socket_running = running - - return socket and socket_running and (not service or service_running) - - -def app_str_to_json(app: str) -> dict: - app_data = [word for word in app.split(' ') if word] - app_json = { - 'name': app_data[0], - 'version': app_data[1], - 'rev': app_data[2], - 'tracking': app_data[3], - 'publisher': app_data[4] if len(app_data) >= 5 else None, - 'notes': app_data[5] if len(app_data) >= 6 else None - } - - return app_json - - -def get_info(app_name: str, attrs: tuple = None): - full_info_lines = run_cmd('{} info {}'.format(BASE_CMD, app_name)) - - data = {} - - if full_info_lines: - re_attrs = r'\w+' if not attrs else '|'.join(attrs) - info_map = re.findall(r'({}):\s+(.+)'.format(re_attrs), full_info_lines) - - for info in info_map: - val = info[1].strip() - - if info[0] == 'installed': - val_split = [s for s in val.split(' ') if s] - data['version'] = val_split[0] - - if len(val_split) > 2: - data['size'] = val_split[2] - else: - data[info[0]] = val - - if not attrs or 'description' in attrs: - desc = re.findall(r'\|\n+((\s+.+\n+)+)', full_info_lines) - data['description'] = ''.join([w.strip() for w in desc[0][0].strip().split('\n')]).replace('.', '.\n') if desc else None - - if not attrs or 'commands' in attrs: - commands = re.findall(r'commands:\s*\n*((\s+-\s.+\s*\n)+)', full_info_lines) - data['commands'] = commands[0][0].strip().replace('- ', '').split('\n') if commands else None - - return data - - -def read_installed(info_path: str) -> List[dict]: - res = run_cmd('{} list'.format(BASE_CMD), print_error=False) - - apps = [] - - if res and len(res) > 0: - lines = res.split('\n') - - if not lines[0].startswith('error'): - for idx, app_str in enumerate(lines): - if idx > 0 and app_str: - apps.append(app_str_to_json(app_str)) - - info_out = new_subprocess(['cat', *[info_path.format(a['name']) for a in apps]]).stdout - - idx = -1 - for o in new_subprocess(['grep', '-E', '(summary|apps)', '--colour=never'], stdin=info_out).stdout: - if o: - line = o.decode() - - if line.startswith('summary:'): - idx += 1 - apps[idx]['summary'] = line.split(':')[1].strip() - else: - apps[idx]['apps_field'] = True - - return apps - - -def get_app_info_path() -> str: - if os.path.exists('/snap'): - return '/snap/{}/current/meta/snap.yaml' - elif os.path.exists('/var/lib/snapd/snap'): - return '/var/lib/snapd/snap/{}/current/meta/snap.yaml' - else: - return None - - -def has_apps_field(name: str, info_path: str) -> bool: - info_out = new_subprocess(['cat', info_path.format(name)]).stdout - - res = False - for o in new_subprocess(['grep', '-E', 'apps', '--colour=never'], stdin=info_out).stdout: - if o: - line = o.decode() - - if line.startswith('apps:'): - res = True - - return res - - -def search(word: str, exact_name: bool = False) -> List[dict]: - apps = [] - - res = run_cmd('{} find "{}"'.format(BASE_CMD, word), print_error=False) - - if res: - res = res.split('\n') - - if not res[0].startswith('No matching'): - for idx, app_str in enumerate(res): - if idx > 0 and app_str: - app_data = [word for word in app_str.split(' ') if word] - - if exact_name and app_data[0] != word: - continue - - apps.append({ - 'name': app_data[0], - 'version': app_data[1], - 'publisher': app_data[2], - 'notes': app_data[3] if app_data[3] != '-' else None, - 'summary': app_data[4] if len(app_data) == 5 else '', - 'rev': None, - 'tracking': None, - 'type': None - }) - - if exact_name and len(apps) > 0: - break - - return apps +def is_installed() -> bool: + return bool(run_cmd('which {}'.format(BASE_CMD), print_error=False)) def uninstall_and_stream(app_name: str, root_password: str) -> SimpleProcess: @@ -199,56 +40,16 @@ def refresh_and_stream(app_name: str, root_password: str) -> SimpleProcess: shell=True) -def run(app: SnapApplication, logger: logging.Logger): - info = get_info(app.name, 'commands') - app_name = app.name.lower() - - if info.get('commands'): - - logger.info('Available commands found for {}: {}'.format(app_name, info['commands'])) - - commands = [c.strip() for c in info['commands']] - - # trying to find an exact match command: - command = None - - for c in commands: - if c.lower() == app_name: - command = c - logger.info("Found exact match command for '{}'".format(app_name)) - break - - if not command: - for c in commands: - if not c.endswith('.apm'): - command = c - - if command: - logger.info("Running '{}'".format(command)) - subprocess.Popen(['{} run {}'.format(BASE_CMD, command)], shell=True, env={**os.environ}) - return - - logger.error("No valid command found for '{}'".format(app_name)) - else: - logger.error("No command found for '{}'".format(app_name)) +def run(cmd: str): + subprocess.Popen(['snap run {}'.format(cmd)], shell=True, env={**os.environ}) def is_api_available() -> Tuple[bool, str]: output = StringIO() - for o in SimpleProcess(['snap', 'search']).instance.stdout: + for o in SimpleProcess([BASE_CMD, 'search']).instance.stdout: if o: output.write(o.decode()) output.seek(0) output = output.read() return 'error:' not in output, output - - -def list_installed_names() -> Set[str]: - res = run_cmd('{} list'.format(BASE_CMD), print_error=False) - - if res: - lines = res.split('\n') - - if not lines[0].startswith('error'): - return {l.split(' ')[0].strip() for l in lines[1:] if l} diff --git a/bauh/gems/snap/snapd.py b/bauh/gems/snap/snapd.py new file mode 100644 index 00000000..091d8fc0 --- /dev/null +++ b/bauh/gems/snap/snapd.py @@ -0,0 +1,136 @@ +import re +import socket +import traceback +from logging import Logger +from typing import Optional, List + +from requests import Session +from requests.adapters import HTTPAdapter +from urllib3.connection import HTTPConnection +from urllib3.connectionpool import HTTPConnectionPool + +from bauh.commons.system import run_cmd + +URL_BASE = 'http://snapd/v2' +RE_SNAPD_STATUS = re.compile('\s+') +RE_SNAPD_SERVICES = re.compile(r'snapd\.\w+.+') + + +class SnapdConnection(HTTPConnection): + def __init__(self): + super(SnapdConnection, self).__init__('localhost') + + def connect(self): + self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.connect("/run/snapd.socket") + + +class SnapdConnectionPool(HTTPConnectionPool): + def __init__(self): + super(SnapdConnectionPool, self).__init__('localhost') + + def _new_conn(self): + return SnapdConnection() + + +class SnapdAdapter(HTTPAdapter): + + def get_connection(self, url, proxies=None): + return SnapdConnectionPool() + + +class SnapdClient: + + def __init__(self, logger: Logger): + self.logger = logger + self.session = self._new_session() + + def _new_session(self) -> Optional[Session]: + try: + session = Session() + session.mount("http://snapd/", SnapdAdapter()) + return session + except: + self.logger.error("Could not establish a connection to 'snapd.socker'") + traceback.print_exc() + + 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}) + + if res.status_code == 200: + json_res = res.json() + + if json_res['status-code'] == 200: + return json_res['result'] + + 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)) + + if res.status_code == 200: + json_res = res.json() + + if json_res['status-code'] == 200: + return json_res['result'] + + def list_all_snaps(self) -> List[dict]: + if self.session: + res = self.session.get('{}/snaps'.format(URL_BASE)) + + if res.status_code == 200: + json_res = res.json() + + if json_res['status-code'] == 200: + return json_res['result'] + + return [] + + def list_only_apps(self) -> List[dict]: + if self.session: + res = self.session.get('{}/apps'.format(URL_BASE)) + + if res.status_code == 200: + json_res = res.json() + + if json_res['status-code'] == 200: + return json_res['result'] + return [] + + def list_commands(self, name: str) -> List[dict]: + if self.session: + res = self.session.get('{}/apps?names={}'.format(URL_BASE, name)) + + if res.status_code == 200: + json_res = res.json() + + if json_res['status-code'] == 200: + return [r for r in json_res['result'] if r['snap'] == name] + return [] + + +def is_running() -> bool: + output = run_cmd('systemctl list-units', print_error=False) + + if not output: + return False + + snapd_services = RE_SNAPD_SERVICES.findall(output) + + snap_socket, socket_running, service, service_running = False, False, False, False + if snapd_services: + for service_line in snapd_services: + line_split = RE_SNAPD_STATUS.split(service_line) + + running = line_split[3] in {'listening', 'running'} + + if line_split[0] == 'snapd.service': + service = True + service_running = running + elif line_split[0] == 'snapd.socket': + snap_socket = True + socket_running = running + + return snap_socket and socket_running and (not service or service_running) diff --git a/bauh/gems/snap/worker.py b/bauh/gems/snap/worker.py deleted file mode 100644 index 72dd76c2..00000000 --- a/bauh/gems/snap/worker.py +++ /dev/null @@ -1,70 +0,0 @@ -import traceback -from threading import Thread - -from bauh.api.abstract.cache import MemoryCache -from bauh.api.abstract.context import ApplicationContext -from bauh.api.abstract.controller import SoftwareManager -from bauh.api.abstract.model import PackageStatus -from bauh.gems.snap import snap -from bauh.gems.snap.constants import SNAP_API_URL -from bauh.gems.snap.model import SnapApplication - - -class SnapAsyncDataLoader(Thread): - - def __init__(self, app: SnapApplication, manager: SoftwareManager, api_cache: MemoryCache, - context: ApplicationContext): - super(SnapAsyncDataLoader, self).__init__(daemon=True) - self.app = app - self.id_ = '{}#{}'.format(self.__class__.__name__, id(self)) - self.manager = manager - self.http_client = context.http_client - self.api_cache = api_cache - self.persist = False - self.download_icons = context.download_icons - self.logger = context.logger - - def run(self): - if self.app: - self.app.status = PackageStatus.LOADING_DATA - - try: - res = self.http_client.session.get('{}/search?q={}'.format(SNAP_API_URL, self.app.name)) - - if res: - try: - snap_list = res.json()['_embedded']['clickindex:package'] - except: - self.logger.warning('Snap API response responded differently from expected for app: {}'.format(self.app.name)) - return - - if not snap_list: - self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode())) - else: - snap_data = snap_list[0] - - api_data = { - 'confinement': snap_data.get('confinement'), - 'description': snap_data.get('description'), - 'icon_url': snap_data.get('icon_url') if self.download_icons else None - } - - self.api_cache.add(self.app.id, api_data) - self.app.confinement = api_data['confinement'] - self.app.icon_url = api_data['icon_url'] - - if not api_data.get('description'): - api_data['description'] = snap.get_info(self.app.name, ('description',)).get('description') - - self.app.description = api_data['description'] - self.persist = self.app.supports_disk_cache() - else: - self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode())) - except: - self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id)) - traceback.print_exc() - - self.app.status = PackageStatus.READY - - if self.persist: - self.manager.cache_to_disk(pkg=self.app, icon_bytes=None, only_icon=False) From 1f68c9c9d4e95a9cc2598e486f7d51cde6c01a18 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 26 Aug 2020 12:57:49 -0300 Subject: [PATCH 57/99] refresh app action: not returning an error when there is no update available --- CHANGELOG.md | 1 + bauh/gems/snap/snap.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ecab792..7b13ffc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Snap - not able to install classic Snaps due to Ubuntu's old Snaps API shutdown - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) + - refresh app action: not returning an error when there is no update available - Web - some environment variable are not available during the launch process - UI diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index 74e0e890..0558df23 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -37,6 +37,7 @@ def downgrade_and_stream(app_name: str, root_password: str) -> SimpleProcess: def refresh_and_stream(app_name: str, root_password: str) -> SimpleProcess: return SimpleProcess(cmd=[BASE_CMD, 'refresh', app_name], root_password=root_password, + error_phrases={'no updates available'}, shell=True) From 81d7dba7ea0e68f3bae59c9401f1b3aa106307e4 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 26 Aug 2020 15:06:10 -0300 Subject: [PATCH 58/99] [snap] fix -> not updating the table with the installed runtimes after a first Snap installation --- CHANGELOG.md | 1 + bauh/gems/snap/controller.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b13ffc7..aa0097a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - not able to install classic Snaps due to Ubuntu's old Snaps API shutdown - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, launch) - refresh app action: not returning an error when there is no update available + - not updating the table with the installed runtimes after a first Snap installation - Web - some environment variable are not available during the launch process - UI diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 048380b8..0bc67433 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -1,5 +1,6 @@ import re import time +import traceback from threading import Thread from typing import List, Set, Type, Optional @@ -205,16 +206,16 @@ def _gen_installation_response(self, success: bool, pkg: SnapApplication, instal if success: new_installed = [pkg] - if installed: - try: - current_installed = self.read_installed(disk_loader=disk_loader, internet_available=internet.is_available()).installed - except: - current_installed = None + try: + current_installed = self.read_installed(disk_loader=disk_loader, internet_available=internet.is_available()).installed + except: + traceback.print_exc() + current_installed = None - if current_installed and (not installed or len(current_installed) > len(installed) + 1): - for p in current_installed: - if p.name != pkg.name and (not installed or p.name not in installed): - new_installed.append(p) + if current_installed: + for p in current_installed: + if p.name != pkg.name and (not installed or p.name not in installed): + new_installed.append(p) return TransactionResult(success=success, installed=new_installed, removed=[]) else: From feb06e6e437e1d48e320068751f6687c7cd22494 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 26 Aug 2020 15:41:05 -0300 Subject: [PATCH 59/99] [snap] improvement -> snapd.is_running() refactored --- bauh/gems/snap/snapd.py | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/bauh/gems/snap/snapd.py b/bauh/gems/snap/snapd.py index 091d8fc0..2f2f0959 100644 --- a/bauh/gems/snap/snapd.py +++ b/bauh/gems/snap/snapd.py @@ -1,4 +1,3 @@ -import re import socket import traceback from logging import Logger @@ -12,8 +11,6 @@ from bauh.commons.system import run_cmd URL_BASE = 'http://snapd/v2' -RE_SNAPD_STATUS = re.compile('\s+') -RE_SNAPD_SERVICES = re.compile(r'snapd\.\w+.+') class SnapdConnection(HTTPConnection): @@ -112,25 +109,12 @@ def list_commands(self, name: str) -> List[dict]: def is_running() -> bool: - output = run_cmd('systemctl list-units', print_error=False) - - if not output: + status = run_cmd('systemctl is-active snapd.service snapd.socket', print_error=False) + if not status: return False + else: + for status in status.split('\n'): + if status.strip().lower() == 'active': + return True - snapd_services = RE_SNAPD_SERVICES.findall(output) - - snap_socket, socket_running, service, service_running = False, False, False, False - if snapd_services: - for service_line in snapd_services: - line_split = RE_SNAPD_STATUS.split(service_line) - - running = line_split[3] in {'listening', 'running'} - - if line_split[0] == 'snapd.service': - service = True - service_running = running - elif line_split[0] == 'snapd.socket': - snap_socket = True - socket_running = running - - return snap_socket and socket_running and (not service or service_running) + return False From 879d5f2c27495c5f8a4be40ec8d528cd1cfeb19f Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 26 Aug 2020 16:25:33 -0300 Subject: [PATCH 60/99] [ui] improvement -> faster initialization dialog: improved the way it checks for finished tasks --- CHANGELOG.md | 4 +++- bauh/view/qt/prepare.py | 43 +++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa0097a5..55f2e6d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,7 +51,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Snap - full support refactored to use the Snapd socket instead of the Ubuntu's old Snap API (which was recently disabled). Now the 'read' operations are faster, a only the icon is cached to the disk. -- minor UI improvements +- UI + - faster initialization dialog: improved the way it checks for finished tasks + - minor improvements ### Fixes - AppImage diff --git a/bauh/view/qt/prepare.py b/bauh/view/qt/prepare.py index e8d8d0c3..f662f81b 100644 --- a/bauh/view/qt/prepare.py +++ b/bauh/view/qt/prepare.py @@ -24,7 +24,7 @@ class Prepare(QThread, TaskManager): signal_register = pyqtSignal(str, str, object) signal_update = pyqtSignal(str, float, str) signal_finished = pyqtSignal(str) - signal_started = pyqtSignal() + signal_started = pyqtSignal(int) signal_ask_password = pyqtSignal() signal_output = pyqtSignal(str, str) @@ -35,6 +35,7 @@ def __init__(self, context: ApplicationContext, manager: SoftwareManager, i18n: self.context = context self.waiting_password = False self.password_response = None + self._registered = 0 def ask_password(self) -> Tuple[str, bool]: self.waiting_password = True @@ -58,7 +59,7 @@ def run(self): QCoreApplication.exit(1) self.manager.prepare(self, root_pwd, None) - self.signal_started.emit() + self.signal_started.emit(self._registered) def update_progress(self, task_id: str, progress: float, substatus: str): self.signal_update.emit(task_id, progress, substatus) @@ -67,6 +68,7 @@ def update_output(self, task_id: str, output: str): self.signal_output.emit(task_id, output) def register_task(self, id_: str, label: str, icon_path: str): + self._registered += 1 self.signal_register.emit(id_, label, icon_path) def finish_task(self, task_id: str): @@ -78,23 +80,19 @@ class CheckFinished(QThread): def __init__(self): super(CheckFinished, self).__init__() - self.total = None - self.finished = None + self.total = 0 + self.finished = 0 def run(self): - self.sleep(3) while True: if self.total == self.finished: break - self.msleep(10) + self.msleep(5) self.signal_finished.emit() - def update(self, total: int, finished: int): - if total is not None: - self.total = total - + def update(self, finished: int): if finished is not None: self.finished = finished @@ -116,7 +114,7 @@ def run(self): class PreparePanel(QWidget, TaskManager): - signal_status = pyqtSignal(object, object) + signal_status = pyqtSignal(int) signal_password_response = pyqtSignal(str, bool) def __init__(self, context: ApplicationContext, manager: SoftwareManager, screen_size: QSize, i18n: I18n, manage_window: QWidget): @@ -133,7 +131,7 @@ def __init__(self, context: ApplicationContext, manager: SoftwareManager, screen self.manager = manager self.tasks = {} self.output = {} - self.ntasks = 0 + self.added_tasks = 0 self.ftasks = 0 self.self_close = False @@ -259,23 +257,24 @@ def show(self): self.prepare_thread.start() centralize(self) - def start(self): - self.ref_bt_close.setVisible(True) + def start(self, tasks: int): + self.check_thread.total = tasks self.check_thread.start() self.skip_thread.start() - self.ref_progress_bar.setVisible(True) self.progress_thread.start() + self.ref_bt_close.setVisible(True) + self.ref_progress_bar.setVisible(True) + def closeEvent(self, QCloseEvent): if not self.self_close: QCoreApplication.exit() def register_task(self, id_: str, label: str, icon_path: str): - self.ntasks += 1 - self.table.setRowCount(self.ntasks) - - task_row = self.ntasks - 1 + self.added_tasks += 1 + self.table.setRowCount(self.added_tasks) + task_row = self.added_tasks - 1 icon_widget = QWidget() icon_widget.setLayout(QHBoxLayout()) @@ -342,8 +341,6 @@ def _show_output(): 'lb_sub': lb_sub, 'finished': False} - self.signal_status.emit(self.ntasks, self.ftasks) - def update_progress(self, task_id: str, progress: float, substatus: str): task = self.tasks[task_id] @@ -385,9 +382,9 @@ def finish_task(self, task_id: str): self._resize_columns() self.ftasks += 1 - self.signal_status.emit(self.ntasks, self.ftasks) + self.signal_status.emit(self.ftasks) - if self.ntasks == self.ftasks: + if self.table.rowCount() == self.ftasks: self.label_top.setText(self.i18n['ready'].capitalize()) def finish(self): From bb5abf5d869d082c520f042a5a87c07977ec783e Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 26 Aug 2020 18:53:09 -0300 Subject: [PATCH 61/99] [arch] feature -> new 'Check Snaps support' action --- CHANGELOG.md | 4 ++ bauh/commons/system.py | 22 ++++++- bauh/gems/arch/controller.py | 99 +++++++++++++++++++++++++++++- bauh/gems/arch/pacman.py | 4 ++ bauh/gems/arch/resources/locale/ca | 8 +++ bauh/gems/arch/resources/locale/de | 8 +++ bauh/gems/arch/resources/locale/en | 8 +++ bauh/gems/arch/resources/locale/es | 8 +++ bauh/gems/arch/resources/locale/it | 8 +++ bauh/gems/arch/resources/locale/pt | 8 +++ bauh/gems/arch/resources/locale/ru | 8 +++ bauh/gems/arch/resources/locale/tr | 8 +++ 12 files changed, 189 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f2e6d1..093f7333 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

+ - new "Check Snaps support" action: checks all system requirements for Snaps to work properly (only available if the 'snapd' package is installed) +

+ +

### Improvements - AppImage diff --git a/bauh/commons/system.py b/bauh/commons/system.py index fc6460bd..719b6f25 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -4,7 +4,7 @@ import time from io import StringIO from subprocess import PIPE -from typing import List, Tuple, Set +from typing import List, Tuple, Set, Dict # default environment variables for subprocesses. from bauh.api.abstract.handler import ProcessWatcher @@ -298,3 +298,23 @@ def get_human_size_str(size) -> str: def run(cmd: List[str], success_code: int = 0) -> Tuple[bool, str]: p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) return p.returncode == success_code, p.stdout.decode() + + +def check_active_services(*names: str) -> Dict[str, bool]: + output = run_cmd('systemctl is-active {}'.format(' '.join(names)), print_error=False) + + if not output: + return {n: False for n in names} + else: + status = output.split('\n') + return {s: status[i].strip().lower() == 'active' for i, s in enumerate(names) if s} + + +def check_enabled_services(*names: str) -> Dict[str, bool]: + output = run_cmd('systemctl is-enabled {}'.format(' '.join(names)), print_error=False) + + if not output: + return {n: False for n in names} + else: + status = output.split('\n') + return {s: status[i].strip().lower() == 'enabled' for i, s in enumerate(names) if s} diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 1317a49c..15555b09 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -25,7 +25,7 @@ ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextComponent, TextInputType, \ FileChooserComponent from bauh.api.constants import TEMP_DIR -from bauh.commons import user, internet +from bauh.commons import user, internet, system from bauh.commons.category import CategoriesDownloader from bauh.commons.config import save_config from bauh.commons.html import bold @@ -203,7 +203,14 @@ def __init__(self, context: ApplicationContext, disk_cache_updater: ArchDiskCach icon_path=get_icon_path(), requires_root=True, refresh=False, - manager=self) + manager=self), + 'setup_snapd': CustomSoftwareAction(i18n_label_key='arch.custom_action.setup_snapd', + i18n_status_key='arch.custom_action.setup_snapd.status', + manager_method='setup_snapd', + icon_path=get_icon_path(), + requires_root=False, + refresh=False, + manager=self), } self.index_aur = None self.re_file_conflict = re.compile(r'[\w\d\-_.]+:') @@ -2573,6 +2580,9 @@ def get_custom_actions(self) -> List[CustomSoftwareAction]: if bool(arch_config['repositories']): actions.append(self.custom_actions['sys_up']) + if pacman.is_snapd_installed(): + actions.append(self.custom_actions['setup_snapd']) + return actions def fill_sizes(self, pkgs: List[ArchPackage]): @@ -2607,7 +2617,7 @@ def fill_sizes(self, pkgs: List[ArchPackage]): p.size = new_size - p.size def upgrade_system(self, root_password: str, watcher: ProcessWatcher) -> bool: - repo_map = pacman.map_repositories() + # repo_map = pacman.map_repositories() installed = self.read_installed(limit=-1, only_apps=False, pkg_types=None, internet_available=internet.is_available(), disk_loader=None).installed if not installed: @@ -2820,3 +2830,86 @@ def enable_pkgbuild_edition(self, pkg: ArchPackage, root_password: str, watcher: def disable_pkgbuild_edition(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatcher): if self._remove_from_editable_pkgbuilds(pkg.name): pkg.pkgbuild_editable = False + + def setup_snapd(self, root_password: str, watcher: ProcessWatcher) -> bool: + # checking services + missing_items = [] + for serv, active in system.check_enabled_services('snapd.service', 'snapd.socket').items(): + if not active: + missing_items.append(InputOption(label=self.i18n['snap.custom_action.setup_snapd.service_disabled'].format("'{}'".format(serv)), + value='enable:{}'.format(serv), + read_only=True)) + + for serv, active in system.check_active_services('snapd.service', 'snapd.socket').items(): + if not active: + missing_items.append(InputOption(label=self.i18n['snap.custom_action.setup_snapd.service_inactive'].format("'{}'".format(serv)), + value='start:{}'.format(serv), + read_only=True)) + + link = '/snap' + link_dest = '/var/lib/snapd/snap' + if not os.path.exists('/snap'): + missing_items.append(InputOption(label=self.i18n['snap.custom_action.setup_snapd.missing_link'].format("'{}'".format(link), "'{}'".format(link_dest)), + value='link:{}:{}'.format(link, link_dest), + read_only=True)) + + if missing_items: + actions = MultipleSelectComponent(label=self.i18n['snap.custom_action.setup_snapd.required_actions'], + options=missing_items, + default_options=set(missing_items), + max_per_line=1, + spaces=False) + if watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(), + body='', + components=[actions], + confirmation_label=self.i18n['proceed'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + + pwd, valid_pwd = watcher.request_root_password() + + if valid_pwd: + handler = ProcessHandler(watcher) + for a in missing_items: + action = a.value.split(':') + + if action[0] == 'enable': + msg = 'Enabling service {}'.format(action[1]) + watcher.print(msg) + self.logger.info(msg) + proc = SimpleProcess(['systemctl', 'enable', '--now', action[1]], root_password=pwd) + elif action[0] == 'start': + msg = 'Starting service {}'.format(action[1]) + watcher.print(msg) + self.logger.info(msg) + proc = SimpleProcess(['systemctl', 'start', action[1]], root_password=pwd) + elif action[0] == 'link': + msg = 'Creating symbolic link {} for {}'.format(action[1], action[2]) + watcher.print(msg) + self.logger.info(msg) + proc = SimpleProcess(['ln', '-s', action[2], action[1]], root_password=pwd) + else: + msg = "Wrong action '{}'".format(action) + watcher.print(msg) + self.logger.warning(msg) + proc = None + + if not proc: + return False + + success, output = handler.handle_simple(proc) + + if not success: + watcher.show_message(title=self.i18n['error'].capitalize(), + body=output, + type_=MessageType.ERROR) + return False + + watcher.show_message(title=self.i18n['snap.custom_action.setup_snapd.ready'], + body=self.i18n['snap.custom_action.setup_snapd.ready.body'], + type_=MessageType.INFO) + return True + else: + watcher.show_message(title=self.i18n['snap.custom_action.setup_snapd.ready'], + body=self.i18n['snap.custom_action.setup_snapd.ready.body'], + type_=MessageType.INFO) + return True diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 323722eb..8c5e46ec 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -1113,3 +1113,7 @@ def get_packages_to_sync_first() -> Set[str]: return {s.strip() for s in to_sync_first[0].split(' ') if s and s.strip()} return set() + + +def is_snapd_installed() -> bool: + return bool(run_cmd('pacman -Qq snapd', print_error=False)) diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index c799c0fe..af2a376d 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -68,6 +68,14 @@ arch.custom_action.refresh_mirrors.location.all.tip=If this option is checked, t arch.custom_action.refresh_mirrors.select_label=Check the desired locations arch.custom_action.refresh_mirrors.status.sorting=Sorting mirrors by speed arch.custom_action.refresh_mirrors.status.updating=Sorting mirrors by speed +arch.custom_action.setup_snapd=Check Snaps support +snap.custom_action.setup_snapd.missing_link=Create the link {} for {} +arch.custom_action.setup_snapd.status=Checking Snaps support +snap.custom_action.setup_snapd.ready=Ready! +snap.custom_action.setup_snapd.ready.body=The system is ready to work with Snaps! +snap.custom_action.setup_snapd.required_actions=Actions required for Snaps to work correctly +snap.custom_action.setup_snapd.service_disabled=Enable the service {} +snap.custom_action.setup_snapd.service_inactive=Start the service {} arch.custom_action.upgrade_system=Quick system upgrade arch.custom_action.upgrade_system.no_updates=There are no updates available arch.custom_action.upgrade_system.pkgs=The packages below will be upgraded diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 6fdada78..1c7f7d6a 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -68,6 +68,14 @@ arch.custom_action.refresh_mirrors.location.all.tip=If this option is checked, t arch.custom_action.refresh_mirrors.select_label=Check the desired locations arch.custom_action.refresh_mirrors.status.sorting=Sorting mirrors by speed arch.custom_action.refresh_mirrors.status.updating=Sorting mirrors by speed +arch.custom_action.setup_snapd=Check Snaps support +snap.custom_action.setup_snapd.missing_link=Create the link {} for {} +arch.custom_action.setup_snapd.status=Checking Snaps support +snap.custom_action.setup_snapd.ready=Ready! +snap.custom_action.setup_snapd.ready.body=The system is ready to work with Snaps! +snap.custom_action.setup_snapd.required_actions=Actions required for Snaps to work correctly +snap.custom_action.setup_snapd.service_disabled=Enable the service {} +snap.custom_action.setup_snapd.service_inactive=Start the service {} arch.custom_action.upgrade_system=Quick system upgrade arch.custom_action.upgrade_system.no_updates=There are no updates available arch.custom_action.upgrade_system.pkgs=The packages below will be upgraded diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index e96cc144..6568b0d3 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -69,6 +69,14 @@ arch.custom_action.refresh_mirrors.location.all.tip=If this option is checked, t arch.custom_action.refresh_mirrors.select_label=Check the desired locations arch.custom_action.refresh_mirrors.status.sorting=Sorting mirrors by speed arch.custom_action.refresh_mirrors.status.updating=Updating mirrors +arch.custom_action.setup_snapd=Check Snaps support +snap.custom_action.setup_snapd.missing_link=Create the link {} for {} +arch.custom_action.setup_snapd.status=Checking Snaps support +snap.custom_action.setup_snapd.ready=Ready! +snap.custom_action.setup_snapd.ready.body=The system is ready to work with Snaps! +snap.custom_action.setup_snapd.required_actions=Actions required for Snaps to work correctly +snap.custom_action.setup_snapd.service_disabled=Enable the service {} +snap.custom_action.setup_snapd.service_inactive=Start the service {} arch.custom_action.upgrade_system=Quick system upgrade arch.custom_action.upgrade_system.no_updates=There are no updates available arch.custom_action.upgrade_system.pkgs=The packages below will be upgraded diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index a3a0ff49..7b992848 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -68,6 +68,14 @@ arch.custom_action.refresh_mirrors.location.all.tip=Si esta opción está marcad arch.custom_action.refresh_mirrors.select_label=Marcar las ubicaciones deseadas arch.custom_action.refresh_mirrors.status.sorting=Ordenando los espejos por velocidad arch.custom_action.refresh_mirrors.status.updating=Actualizando espejos +arch.custom_action.setup_snapd=Verificar soporte de Snaps +snap.custom_action.setup_snapd.missing_link=Crear el link {} para {} +arch.custom_action.setup_snapd.status=Verificando soporte de Snaps +snap.custom_action.setup_snapd.ready=¡Listo! +snap.custom_action.setup_snapd.ready.body=¡El sistema está listo para trabajar con Snaps! +snap.custom_action.setup_snapd.required_actions=Acciones necesarias para que los Snaps funcionen correctamente +snap.custom_action.setup_snapd.service_disabled=Habilitar el servicio {} +snap.custom_action.setup_snapd.service_inactive=Iniciar el servicio {} arch.custom_action.upgrade_system=Actualización rápida de sistema arch.custom_action.upgrade_system.no_updates=No hay actualizaciones disponibles arch.custom_action.upgrade_system.pkgs=Los paquetes abajo serán actualizados diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 84dbe716..7378f0c2 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -68,6 +68,14 @@ arch.custom_action.refresh_mirrors.location.all.tip=If this option is checked, t arch.custom_action.refresh_mirrors.select_label=Check the desired locations arch.custom_action.refresh_mirrors.status.sorting=Sorting mirrors by speed arch.custom_action.refresh_mirrors.status.updating=Sorting mirrors by speed +arch.custom_action.setup_snapd=Check Snaps support +snap.custom_action.setup_snapd.missing_link=Create the link {} for {} +arch.custom_action.setup_snapd.status=Checking Snaps support +snap.custom_action.setup_snapd.ready=Ready! +snap.custom_action.setup_snapd.ready.body=The system is ready to work with Snaps! +snap.custom_action.setup_snapd.required_actions=Actions required for Snaps to work correctly +snap.custom_action.setup_snapd.service_disabled=Enable the service {} +snap.custom_action.setup_snapd.service_inactive=Start the service {} arch.custom_action.upgrade_system=Quick system upgrade arch.custom_action.upgrade_system.no_updates=There are no updates available arch.custom_action.upgrade_system.pkgs=The packages below will be upgraded diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 3ef9422f..6d63b838 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -68,6 +68,14 @@ arch.custom_action.refresh_mirrors.location.all.tip=Se essa opção estiver marc arch.custom_action.refresh_mirrors.select_label=Marque as localizações desejadas arch.custom_action.refresh_mirrors.status.sorting=Ordenando os espelhos por velocidade arch.custom_action.refresh_mirrors.status.updating=Atualizando espelhos +arch.custom_action.setup_snapd=Verificar suporte ao Snaps +snap.custom_action.setup_snapd.missing_link=Criar o link {} para {} +arch.custom_action.setup_snapd.status=Verificando suporte aos Snaps +snap.custom_action.setup_snapd.ready=Pronto! +snap.custom_action.setup_snapd.ready.body=O sistema está pronto para trabalhar com Snaps! +snap.custom_action.setup_snapd.required_actions=Ações necessárias para que os Snaps funcionem corretamente +snap.custom_action.setup_snapd.service_disabled=Habilitar o serviço {} +snap.custom_action.setup_snapd.service_inactive=Iniciar o serviço {} arch.custom_action.upgrade_system=Atualização rápida de sistema arch.custom_action.upgrade_system.no_updates=Não há atualizações disponíveis arch.custom_action.upgrade_system.pkgs=Os pacotes abaixo serão atualizados diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index f353aa61..a4dbd93c 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -68,6 +68,14 @@ arch.custom_action.refresh_mirrors.location.all.tip=Если этот парам arch.custom_action.refresh_mirrors.select_label=Проверка предпочитаемых расположений arch.custom_action.refresh_mirrors.status.sorting=Sorting mirrors by speed arch.custom_action.refresh_mirrors.status.updating=Сортировка зеркал по скорости +arch.custom_action.setup_snapd=Check Snaps support +snap.custom_action.setup_snapd.missing_link=Create the link {} for {} +arch.custom_action.setup_snapd.status=Checking Snaps support +snap.custom_action.setup_snapd.ready=Ready! +snap.custom_action.setup_snapd.ready.body=The system is ready to work with Snaps! +snap.custom_action.setup_snapd.required_actions=Actions required for Snaps to work correctly +snap.custom_action.setup_snapd.service_disabled=Enable the service {} +snap.custom_action.setup_snapd.service_inactive=Start the service {} arch.custom_action.upgrade_system=Обновление системы arch.custom_action.upgrade_system.no_updates=Обновления отсутствуют arch.custom_action.upgrade_system.pkgs=Следующие пакеты будут обновлены diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 5b702b3c..86e41eff 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -68,6 +68,14 @@ arch.custom_action.refresh_mirrors.location.all.tip=Bu seçenek işaretlenirse, arch.custom_action.refresh_mirrors.select_label=İstediğiniz konumları kontrol edin arch.custom_action.refresh_mirrors.status.sorting=Yansıları hıza göre sırala arch.custom_action.refresh_mirrors.status.updating=Yansılar güncelleniyor +arch.custom_action.setup_snapd=Check Snaps support +snap.custom_action.setup_snapd.missing_link=Create the link {} for {} +arch.custom_action.setup_snapd.status=Checking Snaps support +snap.custom_action.setup_snapd.ready=Ready! +snap.custom_action.setup_snapd.ready.body=The system is ready to work with Snaps! +snap.custom_action.setup_snapd.required_actions=Actions required for Snaps to work correctly +snap.custom_action.setup_snapd.service_disabled=Enable the service {} +snap.custom_action.setup_snapd.service_inactive=Start the service {} arch.custom_action.upgrade_system=Sistemi hızlı yükselt arch.custom_action.upgrade_system.no_updates=Güncelleme yok arch.custom_action.upgrade_system.pkgs=Aşağıdaki paketler yükseltilecek From 60836643aa6355b4b6269434248cbc34ed9cc10a Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 27 Aug 2020 10:10:23 -0300 Subject: [PATCH 62/99] [arch] fix -> AUR: some packages dependencies cannot be downloaded due to the wrong download URL --- CHANGELOG.md | 1 + bauh/gems/arch/aur.py | 5 +++-- bauh/gems/arch/updates.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 093f7333..3737e3e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server address is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) - not installing the correct package built when several are generated (e.g: linux-xanmod-lts) + - some packages dependencies cannot be downloaded due to the wrong download URL (missing the 'pkgbase' field to determine the proper url) - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) - Flatpak - downgrading crashing with version 1.8.X diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index f6678c8b..7e4cd493 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -210,12 +210,13 @@ def map_update_data(self, pkgname: str, latest_version: str, srcinfo: dict = Non provided.update(info.get('provides')) return {'c': info.get('conflicts'), 's': None, 'p': provided, 'r': 'aur', - 'v': info['pkgver'], 'd': self.extract_required_dependencies(info)} + 'v': info['pkgver'], 'd': self.extract_required_dependencies(info), + 'b': info.get('pkgbase', pkgname)} else: if latest_version: provided.add('{}={}'.format(pkgname, latest_version)) - return {'c': None, 's': None, 'p': provided, 'r': 'aur', 'v': latest_version, 'd': set()} + return {'c': None, 's': None, 'p': provided, 'r': 'aur', 'v': latest_version, 'd': set(), 'b': pkgname} def fill_update_data(self, output: Dict[str, dict], pkgname: str, latest_version: str, srcinfo: dict = None): data = self.map_update_data(pkgname=pkgname, latest_version=latest_version, srcinfo=srcinfo) diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index 66cebfce..b00bc55b 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -241,7 +241,7 @@ def _fill_to_install(self, context: UpdateRequirementsContext) -> bool: for idx, dep in enumerate(deps): data = deps_data[dep[0]] - pkg = ArchPackage(name=dep[0], version=data['v'], latest_version=data['v'], repository=dep[1], i18n=self.i18n) + pkg = ArchPackage(name=dep[0], version=data['v'], latest_version=data['v'], repository=dep[1], i18n=self.i18n, package_base=data.get('b', dep[0])) sorted_pkgs[idx] = pkg context.to_install[dep[0]] = pkg From 4d526e5488881a8cf2cbe6d4951774f7acbbdc66 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 27 Aug 2020 11:14:00 -0300 Subject: [PATCH 63/99] [arch] fix -> not handling conflicting files errors during the installation process --- CHANGELOG.md | 1 + bauh/gems/arch/controller.py | 41 ++++++++++++++++++++++-------- bauh/gems/arch/pacman.py | 5 +++- bauh/gems/arch/resources/locale/ca | 3 +++ bauh/gems/arch/resources/locale/de | 3 +++ bauh/gems/arch/resources/locale/en | 3 +++ bauh/gems/arch/resources/locale/es | 3 +++ bauh/gems/arch/resources/locale/it | 3 +++ bauh/gems/arch/resources/locale/pt | 3 +++ bauh/gems/arch/resources/locale/ru | 3 +++ bauh/gems/arch/resources/locale/tr | 3 +++ 11 files changed, 59 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3737e3e8..3ef7aad0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -71,6 +71,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - not displaying all packages that must be uninstalled - displaying "required size" for packages that must be uninstalled - some conflict resolution scenarios when upgrading several packages + - not handling conflicting files errors during the installation process - AUR - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 15555b09..167178dd 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -22,7 +22,7 @@ from bauh.api.abstract.model import PackageUpdate, PackageHistory, SoftwarePackage, PackageSuggestion, PackageStatus, \ SuggestionPriority, CustomSoftwareAction from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ - ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextComponent, TextInputType, \ + ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextInputType, \ FileChooserComponent from bauh.api.constants import TEMP_DIR from bauh.commons import user, internet, system @@ -767,7 +767,7 @@ def _is_database_locked(self, handler: ProcessHandler, root_password: str) -> bo return False - def _map_conflicting_file(self, output: str) -> List[TextComponent]: + def _map_conflicting_file(self, output: str) -> List[MultipleSelectComponent]: error_idx = None lines = output.split('\n') for idx, l in enumerate(lines): @@ -782,9 +782,9 @@ def _map_conflicting_file(self, output: str) -> List[TextComponent]: line = lines[idx].strip() if line and self.re_file_conflict.match(line): - files.append(TextComponent(' - {}'.format(line))) + files.append(InputOption(label=line, value=idx, read_only=True)) - return files + return [MultipleSelectComponent(options=files, default_options={*files}, label='')] def list_related(self, pkgs: Iterable[str], all_pkgs: Iterable[str], data: Dict[str, dict], related: Set[str], provided_map: Dict[str, Set[str]]) -> Set[str]: related.update(pkgs) @@ -896,12 +896,11 @@ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str] disk.write_several(pkgs=pkg_map, overwrite=True, maintainer=None) return True elif 'conflicting files' in upgrade_output: - files = self._map_conflicting_file(upgrade_output) if not handler.watcher.request_confirmation(title=self.i18n['warning'].capitalize(), body=self.i18n['arch.upgrade.error.conflicting_files'] + ':', deny_label=self.i18n['arch.upgrade.conflicting_files.proceed'], confirmation_label=self.i18n['arch.upgrade.conflicting_files.stop'], - components=files): + components=self._map_conflicting_file(upgrade_output)): return self._upgrade_repo_pkgs(to_upgrade=to_upgrade_remaining, handler=handler, @@ -2030,11 +2029,8 @@ def _install(self, context: TransactionContext) -> bool: installed_with_same_name = self.read_installed(disk_loader=context.disk_loader, internet_available=True, names={context.name}).installed context.watcher.change_substatus(self.i18n['arch.installing.package'].format(bold(context.name))) - installed, _ = context.handler.handle_simple(pacman.install_as_process(pkgpaths=to_install, - root_password=context.root_password, - file=context.has_install_file(), - pkgdir=context.project_dir), - output_handler=status_handler.handle if status_handler else None) + installed = self._handle_install_call(context=context, to_install=to_install, status_handler=status_handler) + if status_handler: status_handler.stop_working() status_handler.join() @@ -2092,6 +2088,29 @@ def _install(self, context: TransactionContext) -> bool: return installed + def _call_pacman_install(self, context: TransactionContext, to_install: List[str], overwrite_files: bool, status_handler: Optional[object] = None) -> Tuple[bool, str]: + return context.handler.handle_simple(pacman.install_as_process(pkgpaths=to_install, + root_password=context.root_password, + file=context.has_install_file(), + pkgdir=context.project_dir, + overwrite_conflicting_files=overwrite_files), + output_handler=status_handler.handle if status_handler else None) + + def _handle_install_call(self, context: TransactionContext, to_install: List[str], status_handler) -> bool: + installed, output = self._call_pacman_install(context=context, to_install=to_install, + overwrite_files=False, status_handler=status_handler) + + if not installed and 'conflicting files' in output: + if not context.handler.watcher.request_confirmation(title=self.i18n['warning'].capitalize(), + body=self.i18n['arch.install.error.conflicting_files'].format(bold(context.name)) + ':', + deny_label=self.i18n['arch.install.error.conflicting_files.proceed'], + confirmation_label=self.i18n['arch.install.error.conflicting_files.stop'], + components=self._map_conflicting_file(output)): + installed, output = self._call_pacman_install(context=context, to_install=to_install, + overwrite_files=True, status_handler=status_handler) + + return installed + def _update_progress(self, context: TransactionContext, val: int): if context.change_progress: context.watcher.change_progress(val) diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 8c5e46ec..bc247590 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -141,12 +141,15 @@ def map_installed(names: Iterable[str] = None) -> dict: # returns a dict with w return pkgs -def install_as_process(pkgpaths: Iterable[str], root_password: str, file: bool, pkgdir: str = '.') -> SimpleProcess: +def install_as_process(pkgpaths: Iterable[str], root_password: str, file: bool, pkgdir: str = '.', overwrite_conflicting_files: bool = False) -> SimpleProcess: if file: cmd = ['pacman', '-U', *pkgpaths, '--noconfirm'] # pkgpath = install file path else: cmd = ['pacman', '-S', *pkgpaths, '--noconfirm'] # pkgpath = pkgname + if overwrite_conflicting_files: + cmd.append('--overwrite=*') + return SimpleProcess(cmd=cmd, root_password=root_password, cwd=pkgdir, diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index af2a376d..8ac03378 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -166,6 +166,9 @@ arch.install.dep_not_found.body.l3=S’ha cancel·lat la instal·lació. arch.install.dep_not_found.title=No s’ha trobat la dependència arch.install.dependency.install=S’està instal·lant el paquet depenent {} arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. +arch.install.error.conflicting_files=The package {} wants to overwrite files from other installed packages +arch.install.error.conflicting_files.proceed=Allow +arch.install.error.conflicting_files.stop=Cancel installation arch.install.optdep.error=Could not install the optional packages: {} arch.install.optdeps.request.body={} s’ha instal·lat correctament. Hi ha paquets opcionals associats que potser voldreu instal·lar arch.install.optdeps.request.help=Marqueu els que voleu diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 1c7f7d6a..f9a0a0d5 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -166,6 +166,9 @@ arch.install.dep_not_found.body.l3=Installation abgebrochen. arch.install.dep_not_found.title=Abhängigkeit nicht gefunden arch.install.dependency.install=Abhängigket {} wird installiert arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. +arch.install.error.conflicting_files=The package {} wants to overwrite files from other installed packages +arch.install.error.conflicting_files.proceed=Allow +arch.install.error.conflicting_files.stop=Cancel installation arch.install.optdep.error=Could not install the optional packages: {} arch.install.optdeps.request.body={} wurde erfolgreich installiert! Es gibt optionale zugehörige Pakete welche du vielleicht auch installieren möchtest arch.install.optdeps.request.help=Wähle entsprechende aus diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 6568b0d3..64d76b21 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -167,6 +167,9 @@ arch.install.dep_not_found.body.l3=Installation cancelled. arch.install.dep_not_found.title=Dependency not found arch.install.dependency.install=Installing package dependency {} arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. +arch.install.error.conflicting_files=The package {} wants to overwrite files from other installed packages +arch.install.error.conflicting_files.proceed=Allow +arch.install.error.conflicting_files.stop=Cancel installation arch.install.optdep.error=Could not install the optional packages: {} arch.install.optdeps.request.body={} was succesfully installed ! There are some optional associated packages that you might want to install arch.install.optdeps.request.help=Check those you want diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 7b992848..99d6f2bb 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -166,6 +166,9 @@ arch.install.dep_not_found.body.l3=Instalación cancelada. arch.install.dep_not_found.title=Dependencia no encontrada arch.install.dependency.install=Instalando el paquete dependiente {} arch.install.dependency.install.error=No se pudo instalar los paquetes dependientes: {}. Instalación de {} abortada. +arch.install.error.conflicting_files=El paquete {} quiere sobrescribir archivos de otros paquetes instalados +arch.install.error.conflicting_files.proceed=Permitir +arch.install.error.conflicting_files.stop=Cancelar instalación arch.install.optdep.error=No se pudo instalar los paquetes opcionales: {} arch.install.optdeps.request.body=¡{} se instaló correctamente! Hay algunos paquetes opcionales asociados que es posible que desee instalar arch.install.optdeps.request.help=Marque los que desee diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 7378f0c2..b17e9707 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -166,6 +166,9 @@ arch.install.dep_not_found.body.l3=Installazione annullata. arch.install.dep_not_found.title=Dipendenza non trovata arch.install.dependency.install=Installazione della dipendenza pacchetto {} arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. +arch.install.error.conflicting_files=The package {} wants to overwrite files from other installed packages +arch.install.error.conflicting_files.proceed=Allow +arch.install.error.conflicting_files.stop=Cancel installation arch.install.optdep.error=Could not install the optional packages: {} arch.install.optdeps.request.body={} è stato installato con successo! Ci sono alcuni pacchetti associati opzionali che potresti voler installare arch.install.optdeps.request.help=Controlla quelli che vuoi diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 6d63b838..ea9b24e2 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -166,6 +166,9 @@ arch.install.dep_not_found.body.l3=Instalação cancelada. arch.install.dep_not_found.title=Dependência não encontrada arch.install.dependency.install=Instalando o pacote dependente {} arch.install.dependency.install.error=Não foi possível instalar os pacotes dependentes: {}. Instalação de {} abortada. +arch.install.error.conflicting_files=O pacote {} quer sobrepor alguns arquivos de outros pacotes instalados +arch.install.error.conflicting_files.proceed=Permitir +arch.install.error.conflicting_files.stop=Cancelar instalação arch.install.optdep.error=Não foi possível instalar os pacotes opcionais: {} arch.install.optdeps.request.body={} foi instalado com sucesso ! Existem alguns pacotes opcionais associados que você talvez queira instalar arch.install.optdeps.request.help=Marque os desejados diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index a4dbd93c..05d256e1 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -166,6 +166,9 @@ arch.install.dep_not_found.body.l3=Установка отменена. arch.install.dep_not_found.title=Зависимость не найдена arch.install.dependency.install=Установка зависимостей пакета {} arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. +arch.install.error.conflicting_files=The package {} wants to overwrite files from other installed packages +arch.install.error.conflicting_files.proceed=Allow +arch.install.error.conflicting_files.stop=Cancel installation arch.install.optdep.error=Could not install the optional packages: {} arch.install.optdeps.request.body={} успешно установлен ! Есть некоторые дополнительные связанные пакеты, которые вы можете установить arch.install.optdeps.request.help=Отметьте те, которые вы хотите установить diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 86e41eff..5b52a195 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -166,6 +166,9 @@ arch.install.dep_not_found.body.l3=Yükleme iptal edildi. arch.install.dep_not_found.title=Bağımlılık bulunamadı arch.install.dependency.install=Paket bağımlılığını yükleniyor {} arch.install.dependency.install.error=Bağımlı paketler yüklenemedi: {}. {} Kurulumu iptal edildi. +arch.install.error.conflicting_files=The package {} wants to overwrite files from other installed packages +arch.install.error.conflicting_files.proceed=Allow +arch.install.error.conflicting_files.stop=Cancel installation arch.install.optdep.error=Tercihe bağlı paketler yüklenemedi: {} arch.install.optdeps.request.body={} başarıyla yüklendi! Yüklemek isteyebileceğiniz bazı tercihe bağlı bağımlılıklar vardır arch.install.optdeps.request.help=İstediklerinizi kontrol edin From 16eb8445fbd711b3e9a7b7d01c58c77771193f14 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 28 Aug 2020 13:53:59 -0300 Subject: [PATCH 64/99] [arch] fix -> AUR: not properly extracting srcinfo data when several pkgnames are declared --- CHANGELOG.md | 3 +- bauh/gems/arch/aur.py | 77 ++++-- bauh/gems/arch/controller.py | 16 +- tests/gems/arch/resources/bauh_srcinfo | 27 +++ tests/gems/arch/resources/mangohud_srcinfo | 36 +++ tests/gems/arch/test_aur.py | 266 +++++++++++++++++++++ 6 files changed, 394 insertions(+), 31 deletions(-) create mode 100644 tests/gems/arch/resources/bauh_srcinfo create mode 100644 tests/gems/arch/resources/mangohud_srcinfo create mode 100644 tests/gems/arch/test_aur.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ef7aad0..1bcfa425 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server address is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) - not installing the correct package built when several are generated (e.g: linux-xanmod-lts) - - some packages dependencies cannot be downloaded due to the wrong download URL (missing the 'pkgbase' field to determine the proper url) + - some packages dependencies cannot be downloaded due to the wrong download URL (missing the 'pkgbase' field to determine the proper url) + - not properly extracting srcinfo data when several pkgnames are declared (leads to wrong dependencies requirements) - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) - Flatpak - downgrading crashing with version 1.8.X diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index 7e4cd493..adfb54c6 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -2,7 +2,7 @@ import os import re import urllib.parse -from typing import Set, List, Iterable, Dict +from typing import Set, List, Iterable, Dict, Optional import requests @@ -28,6 +28,8 @@ 'optdepends', 'optdepends_x86_64', 'optdepends_i686', + 'sha256sums', + 'sha256sums_x86_64', 'sha512sums', 'sha512sums_x86_64', 'source', @@ -44,30 +46,61 @@ 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, fields: Set[str] = None) -> dict: - info = {} +def map_srcinfo(string: str, pkgname: str, fields: Set[str] = None) -> dict: + subinfos, subinfo = [], {} - if fields: - field_re = re.compile(r'({})\s+=\s+(.+)\n'.format('|'.join(fields))) - else: - field_re = RE_SRCINFO_KEYS + key_fields = {'pkgname', 'pkgbase'} - for tupl in field_re.findall(string): - key = tupl[0].strip() - val = tupl[1].strip() + for field in RE_SRCINFO_KEYS.findall(string): + key = field[0].strip() + val = field[1].strip() - if key not in info: - info[key] = [val] if key in KNOWN_LIST_FIELDS else val - else: - if not isinstance(info[key], list): - info[key] = [info[key]] + if subinfo and key in key_fields: + subinfos.append(subinfo) + subinfo = {key: val} + elif not fields or key in fields: + if key not in subinfo: + subinfo[key] = {val} if key in KNOWN_LIST_FIELDS else val + else: + if not isinstance(subinfo[key], set): + subinfo[key] = {subinfo[key]} - info[key].append(val) + subinfo[key].add(val) - pkgname = info.get('pkgname') + if subinfo: + subinfos.append(subinfo) - if isinstance(pkgname, list): - info['pkgname'] = pkgname[0] + pkgnames = {s['pkgname'] for s in subinfos if 'pkgname' in s} + return merge_subinfos(subinfos=subinfos, + pkgname=None if (len(pkgnames) == 1 or pkgname not in pkgnames) else pkgname, + fields=fields) + + +def merge_subinfos(subinfos: List[dict], pkgname: Optional[str] = None, fields: Optional[Set[str]] = None) -> dict: + info = {} + for subinfo in subinfos: + if not pkgname or subinfo.get('pkgname') in {None, pkgname}: + for key, val in subinfo.items(): + if not fields or key in fields: + current_val = info.get(key) + + if current_val is None: + info[key] = val + else: + if not isinstance(current_val, set): + current_val = {current_val} + info[key] = current_val + + if isinstance(val, set): + current_val.update(val) + else: + current_val.add(val) + + for field in info.keys(): + val = info.get(field) + + if isinstance(val, set): + info[field] = [*val] return info @@ -90,7 +123,7 @@ def get_info(self, names: Iterable[str]) -> List[dict]: except: return [] - def get_src_info(self, name: str) -> dict: + def get_src_info(self, name: str, real_name: Optional[str] = None) -> dict: srcinfo = self.srcinfo_cache.get(name) if srcinfo: @@ -99,7 +132,7 @@ def get_src_info(self, name: str) -> dict: res = self.http_client.get(URL_SRC_INFO + urllib.parse.quote(name)) if res and res.text: - srcinfo = map_srcinfo(res.text) + srcinfo = map_srcinfo(string=res.text, pkgname=real_name if real_name else name) if srcinfo: self.srcinfo_cache[name] = srcinfo @@ -118,7 +151,7 @@ def get_src_info(self, name: str) -> dict: info_base = info.get('PackageBase') if info_name and info_base and info_name != info_base: self.logger.info('{p} is based on {b}. Retrieving {b} .SRCINFO'.format(p=info_name, b=info_base)) - srcinfo = self.get_src_info(info_base) + srcinfo = self.get_src_info(name=info_base, real_name=info_name) if srcinfo: self.srcinfo_cache[name] = srcinfo diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 167178dd..9339ebe3 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -611,7 +611,7 @@ def _downgrade_aur_pkg(self, context: TransactionContext): for idx in range(1, len(commit_list)): commit = commit_list[idx] with open(srcinfo_path) as f: - pkgsrc = aur.map_srcinfo(f.read(), srcfields) + pkgsrc = aur.map_srcinfo(string=f.read(), pkgname=context.name ,fields=srcfields) reset_proc = new_subprocess(['git', 'reset', '--hard', commit], cwd=clone_path) if not context.handler.handle(SystemProcess(reset_proc, check_error_output=False)): @@ -1350,7 +1350,7 @@ def _get_history_aur_pkg(self, pkg: ArchPackage) -> PackageHistory: for idx, commit in enumerate(commits): with open(srcinfo_path) as f: - pkgsrc = aur.map_srcinfo(f.read(), srcfields) + pkgsrc = aur.map_srcinfo(string=f.read(), pkgname=pkg.name, fields=srcfields) if status_idx < 0 and '{}-{}'.format(pkgsrc.get('pkgver'), pkgsrc.get('pkgrel')) == pkg.version: status_idx = idx @@ -1566,10 +1566,10 @@ def _map_repos(self, pkgnames: Iterable[str]) -> dict: return pkg_repos - def _pre_download_source(self, project_dir: str, watcher: ProcessWatcher) -> bool: + def _pre_download_source(self, pkgname: str, project_dir: str, watcher: ProcessWatcher) -> bool: if self.context.file_downloader.is_multithreaded(): with open('{}/.SRCINFO'.format(project_dir)) as f: - srcinfo = aur.map_srcinfo(f.read()) + srcinfo = aur.map_srcinfo(string=f.read(), pkgname=pkgname) pre_download_files = [] @@ -1639,7 +1639,7 @@ def _edit_pkgbuild_and_update_context(self, context: TransactionContext): watcher=context.watcher, pkgbuild_path='{}/PKGBUILD'.format(context.project_dir)): context.pkgbuild_edited = True - srcinfo = aur.map_srcinfo(makepkg.gen_srcinfo(context.project_dir)) + srcinfo = aur.map_srcinfo(string=makepkg.gen_srcinfo(context.project_dir), pkgname=context.name) if srcinfo: context.name = srcinfo['pkgname'] @@ -1656,7 +1656,7 @@ def _edit_pkgbuild_and_update_context(self, context: TransactionContext): def _build(self, context: TransactionContext) -> bool: self._edit_pkgbuild_and_update_context(context) - self._pre_download_source(context.project_dir, context.watcher) + self._pre_download_source(context.name, context.project_dir, context.watcher) self._update_progress(context, 50) if not self._handle_aur_package_deps_and_keys(context): @@ -1691,7 +1691,7 @@ def _build(self, context: TransactionContext) -> bool: file_to_install = gen_file[0] if len(gen_file) > 1: - srcinfo = aur.map_srcinfo(makepkg.gen_srcinfo(context.project_dir)) + srcinfo = aur.map_srcinfo(string=makepkg.gen_srcinfo(context.project_dir), 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 '' @@ -1762,7 +1762,7 @@ def _list_missing_deps(self, context: TransactionContext) -> List[Tuple[str, str if context.repository == 'aur': with open('{}/.SRCINFO'.format(context.project_dir)) as f: - srcinfo = aur.map_srcinfo(f.read()) + srcinfo = aur.map_srcinfo(string=f.read(), pkgname=context.name) pkgs_data = {context.name: self.aur_client.map_update_data(context.name, context.get_version(), srcinfo)} else: diff --git a/tests/gems/arch/resources/bauh_srcinfo b/tests/gems/arch/resources/bauh_srcinfo new file mode 100644 index 00000000..ba7e53ae --- /dev/null +++ b/tests/gems/arch/resources/bauh_srcinfo @@ -0,0 +1,27 @@ +pkgbase = bauh + pkgdesc = Graphical interface for managing your applications ( AppImage, Flatpak, Snap, Arch/AUR, Web ) + pkgver = 0.9.6 + pkgrel = 2 + url = https://github.com/vinifmor/bauh + arch = any + license = zlib/libpng + makedepends = git + makedepends = python + makedepends = python-pip + makedepends = python-setuptools + depends = python + depends = python-pyqt5 + depends = python-pyqt5-sip + depends = python-requests + depends = python-colorama + depends = python-pyaml + depends = qt5-svg + optdepends = flatpak: required for Flatpak support + optdepends = snapd: required for Snap support + optdepends = python-beautifulsoup4: for Native Web applications support + optdepends = python-lxml: for Native Web applications support + source = https://github.com/vinifmor/bauh/archive/0.9.6.tar.gz + sha512sums = cb1820b8a41dccec746d91d71b7f524c2e3caf6b30b0cd9666598b8ad49302654d9ce9bd1a0a2a9612afebc27ef78a2a94ac10e4e6c183742effe4feeabaa7b2 + +pkgname = bauh + diff --git a/tests/gems/arch/resources/mangohud_srcinfo b/tests/gems/arch/resources/mangohud_srcinfo new file mode 100644 index 00000000..bec0adc2 --- /dev/null +++ b/tests/gems/arch/resources/mangohud_srcinfo @@ -0,0 +1,36 @@ +pkgbase = mangohud + pkgver = 0.5.1 + pkgrel = 3 + url = https://github.com/flightlessmango/MangoHud + arch = x86_64 + license = MIT + makedepends = meson + makedepends = python-mako + makedepends = glslang + makedepends = libglvnd + makedepends = lib32-libglvnd + makedepends = vulkan-headers + makedepends = vulkan-icd-loader + makedepends = lib32-vulkan-icd-loader + makedepends = libxnvctrl + source = mangohud-0.5.1.tar.gz::https://github.com/flightlessmango/MangoHud/archive/v0.5.1.tar.gz + sha256sums = 3e91d4fc7369d46763894c13f3315133871dd02705072981770c3cf58e8081c6 + +pkgname = mangohud + pkgdesc = A Vulkan overlay layer for monitoring FPS, temperatures, CPU/GPU load and more + depends = gcc-libs + depends = mangohud-common + optdepends = bash: mangohud helper script + optdepends = libxnvctrl: support for older NVIDIA GPUs + +pkgname = lib32-mangohud + pkgdesc = A Vulkan overlay layer for monitoring FPS, temperatures, CPU/GPU load and more (32-bit) + depends = lib32-gcc-libs + depends = mangohud + depends = mangohud-common + optdepends = lib32-libxnvctrl: support for older NVIDIA GPUs + +pkgname = mangohud-common + pkgdesc = Common files for mangohud and lib32-mangohud + optdepends = bash: mangohud helper script + diff --git a/tests/gems/arch/test_aur.py b/tests/gems/arch/test_aur.py new file mode 100644 index 00000000..6f34cebe --- /dev/null +++ b/tests/gems/arch/test_aur.py @@ -0,0 +1,266 @@ +import os +from unittest import TestCase + +from bauh.gems.arch import aur + +FILE_DIR = os.path.dirname(os.path.abspath(__file__)) + + +class AURModuleTest(TestCase): + + def test_map_srcinfo__only_one_pkgname(self): + expected_fields = { + 'pkgbase': 'bauh', + 'pkgname': 'bauh', + 'pkgver': '0.9.6', + 'pkgrel': '2', + 'url': 'https://github.com/vinifmor/bauh', + 'arch': 'any', + 'license': 'zlib/libpng', + 'makedepends': ['git', 'python', 'python-pip', 'python-setuptools'], + 'depends': [ + 'python', 'python-colorama', 'python-pyaml', 'python-pyqt5', 'python-pyqt5-sip', 'python-requests', 'qt5-svg' + ], + 'optdepends': [ + 'flatpak: required for Flatpak support', + 'python-beautifulsoup4: for Native Web applications support', + 'python-lxml: for Native Web applications support', + 'snapd: required for Snap support' + ], + 'source': ['https://github.com/vinifmor/bauh/archive/0.9.6.tar.gz'], + 'sha512sums': ['cb1820b8a41dccec746d91d71b7f524c2e3caf6b30b0cd9666598b8ad49302654d9ce9bd1a0a2a9612afebc27ef78a2a94ac10e4e6c183742effe4feeabaa7b2'] + } + + with open(FILE_DIR + '/resources/bauh_srcinfo') as f: + srcinfo = f.read() + + res = aur.map_srcinfo(srcinfo, 'bauh') + + for key, val in expected_fields.items(): + self.assertIn(key, res, "key '{}' not in res".format(key)) + + if isinstance(val, list): + val.sort() + res[key].sort() + + self.assertEqual(val, res[key], "expected: {}. current: {}".format(val, res[key])) + + def test_map_srcinfo__one_name__only_specific_fields(self): + expected_fields = { + 'pkgver': '0.9.6', + 'pkgrel': '2' + } + + with open(FILE_DIR + '/resources/bauh_srcinfo') as f: + srcinfo = f.read() + + res = aur.map_srcinfo(srcinfo, 'bauh', fields={*expected_fields.keys()}) + + self.assertEqual(len(expected_fields), len(res), "Expected: {}. Current: {}".format(len(expected_fields), len(res))) + + for key, val in expected_fields.items(): + self.assertIn(key, res, "key '{}' not in res".format(key)) + + if isinstance(val, list): + val.sort() + res[key].sort() + + self.assertEqual(val, res[key], "expected: {}. current: {}".format(val, res[key])) + + def test_map_srcinfo__several_pkgnames__pkgname_specified_case_1(self): + expected_fields = { + 'pkgbase': 'mangohud', + 'pkgname': 'mangohud', + 'pkgver': '0.5.1', + 'pkgrel': '3', + 'pkgdesc': 'A Vulkan overlay layer for monitoring FPS, temperatures, CPU/GPU load and more', + 'source': ['mangohud-0.5.1.tar.gz::https://github.com/flightlessmango/MangoHud/archive/v0.5.1.tar.gz'], + 'sha256sums': ['3e91d4fc7369d46763894c13f3315133871dd02705072981770c3cf58e8081c6'], + 'license': 'MIT', + 'arch': 'x86_64', + 'url': 'https://github.com/flightlessmango/MangoHud', + 'makedepends': [ + 'glslang', 'libglvnd', 'lib32-libglvnd', 'meson', 'python-mako', 'vulkan-headers', 'vulkan-icd-loader', + 'lib32-vulkan-icd-loader', 'libxnvctrl' + ], + 'depends': ['gcc-libs', 'mangohud-common'], + 'optdepends': ['bash: mangohud helper script', 'libxnvctrl: support for older NVIDIA GPUs'] + } + + with open(FILE_DIR + '/resources/mangohud_srcinfo') as f: + srcinfo = f.read() + + res = aur.map_srcinfo(srcinfo, 'mangohud') + + self.assertEqual(len(expected_fields), len(res), "Expected: {}. Current: {}".format(len(expected_fields), len(res))) + + for key, val in expected_fields.items(): + self.assertIn(key, res, "key '{}' not in res".format(key)) + + if isinstance(val, list): + val.sort() + + if isinstance(res[key], list): + res[key].sort() + + self.assertEqual(val, res[key], "expected: {}. current: {}".format(val, res[key])) + + def test_map_srcinfo__several_pkgnames__pkgname_specified_case_2(self): + expected_fields = { + 'pkgbase': 'mangohud', + 'pkgname': 'mangohud-common', + 'pkgver': '0.5.1', + 'pkgrel': '3', + 'pkgdesc': 'Common files for mangohud and lib32-mangohud', + 'source': ['mangohud-0.5.1.tar.gz::https://github.com/flightlessmango/MangoHud/archive/v0.5.1.tar.gz'], + 'sha256sums': ['3e91d4fc7369d46763894c13f3315133871dd02705072981770c3cf58e8081c6'], + 'license': 'MIT', + 'url': 'https://github.com/flightlessmango/MangoHud', + 'arch': 'x86_64', + 'makedepends': [ + 'glslang', 'libglvnd', 'lib32-libglvnd', 'meson', 'python-mako', 'vulkan-headers', 'vulkan-icd-loader', + 'lib32-vulkan-icd-loader', 'libxnvctrl' + ], + 'optdepends': ['bash: mangohud helper script'] + } + + with open(FILE_DIR + '/resources/mangohud_srcinfo') as f: + srcinfo = f.read() + + res = aur.map_srcinfo(srcinfo, 'mangohud-common') + self.assertEqual(len(expected_fields), len(res), "Expected: {}. Current: {}".format(len(expected_fields), len(res))) + + for key, val in expected_fields.items(): + self.assertIn(key, res, "key '{}' not in res".format(key)) + + if isinstance(val, list): + val.sort() + + if isinstance(res[key], list): + res[key].sort() + + self.assertEqual(val, res[key], "expected: {}. current: {}".format(val, res[key])) + + def test_map_srcinfo__several_pkgnames__pkgname_specified_case_3(self): + expected_fields = { + 'pkgbase': 'mangohud', + 'pkgname': 'lib32-mangohud', + 'pkgver': '0.5.1', + 'pkgrel': '3', + 'pkgdesc': 'A Vulkan overlay layer for monitoring FPS, temperatures, CPU/GPU load and more (32-bit)', + 'source': ['mangohud-0.5.1.tar.gz::https://github.com/flightlessmango/MangoHud/archive/v0.5.1.tar.gz'], + 'sha256sums': ['3e91d4fc7369d46763894c13f3315133871dd02705072981770c3cf58e8081c6'], + 'license': 'MIT', + 'url': 'https://github.com/flightlessmango/MangoHud', + 'arch': 'x86_64', + 'makedepends': [ + 'glslang', 'libglvnd', 'lib32-libglvnd', 'meson', 'python-mako', 'vulkan-headers', 'vulkan-icd-loader', + 'lib32-vulkan-icd-loader', 'libxnvctrl' + ], + 'depends': ['mangohud', 'mangohud-common', 'lib32-gcc-libs'], + 'optdepends': ['lib32-libxnvctrl: support for older NVIDIA GPUs'] + } + + with open(FILE_DIR + '/resources/mangohud_srcinfo') as f: + srcinfo = f.read() + + res = aur.map_srcinfo(srcinfo, 'lib32-mangohud') + self.assertEqual(len(expected_fields), len(res), "Expected: {}. Current: {}".format(len(expected_fields), len(res))) + + for key, val in expected_fields.items(): + self.assertIn(key, res, "key '{}' not in res".format(key)) + + if isinstance(val, list): + val.sort() + + if isinstance(res[key], list): + res[key].sort() + + self.assertEqual(val, res[key], "expected: {}. current: {}".format(val, res[key])) + + def test_map_srcinfo__several_pkgnames__different_pkgname(self): + expected_fields = { + 'pkgbase': 'mangohud', + 'pkgname': ['lib32-mangohud', 'mangohud', 'mangohud-common'], + 'pkgver': '0.5.1', + 'pkgrel': '3', + 'pkgdesc': [ + 'A Vulkan overlay layer for monitoring FPS, temperatures, CPU/GPU load and more (32-bit)', + 'Common files for mangohud and lib32-mangohud', + 'A Vulkan overlay layer for monitoring FPS, temperatures, CPU/GPU load and more', + ], + 'source': ['mangohud-0.5.1.tar.gz::https://github.com/flightlessmango/MangoHud/archive/v0.5.1.tar.gz'], + 'sha256sums': ['3e91d4fc7369d46763894c13f3315133871dd02705072981770c3cf58e8081c6'], + 'license': 'MIT', + 'url': 'https://github.com/flightlessmango/MangoHud', + 'arch': 'x86_64', + 'makedepends': [ + 'glslang', 'libglvnd', 'lib32-libglvnd', 'meson', 'python-mako', 'vulkan-headers', 'vulkan-icd-loader', + 'lib32-vulkan-icd-loader', 'libxnvctrl' + ], + 'depends': ['mangohud', 'mangohud-common', 'lib32-gcc-libs', 'gcc-libs'], + 'optdepends': ['lib32-libxnvctrl: support for older NVIDIA GPUs', + 'bash: mangohud helper script', + 'libxnvctrl: support for older NVIDIA GPUs'] + } + + with open(FILE_DIR + '/resources/mangohud_srcinfo') as f: + srcinfo = f.read() + + res = aur.map_srcinfo(srcinfo, 'xpto') + self.assertEqual(len(expected_fields), len(res), "Expected: {}. Current: {}".format(len(expected_fields), len(res))) + + for key, val in expected_fields.items(): + self.assertIn(key, res, "key '{}' not in res".format(key)) + + if isinstance(val, list): + val.sort() + + if isinstance(res[key], list): + res[key].sort() + + self.assertEqual(val, res[key], "expected: {}. current: {}".format(val, res[key])) + + def test_map_srcinfo__several_names__pkgname_present__only_specific_fields(self): + expected_fields = { + 'pkgver': '0.5.1', + 'pkgrel': '3' + } + + with open(FILE_DIR + '/resources/mangohud_srcinfo') as f: + srcinfo = f.read() + + res = aur.map_srcinfo(srcinfo, 'mangohud-commons', fields={*expected_fields.keys()}) + + self.assertEqual(len(expected_fields), len(res), "Expected: {}. Current: {}".format(len(expected_fields), len(res))) + + for key, val in expected_fields.items(): + self.assertIn(key, res, "key '{}' not in res".format(key)) + + if isinstance(val, list): + val.sort() + res[key].sort() + + self.assertEqual(val, res[key], "expected: {}. current: {}".format(val, res[key])) + + def test_map_srcinfo__several_names__pkgname_not_present__only_specific_fields(self): + expected_fields = { + 'pkgname': ['mangohud', 'lib32-mangohud', 'mangohud-common'], + 'pkgver': '0.5.1' + } + + with open(FILE_DIR + '/resources/mangohud_srcinfo') as f: + srcinfo = f.read() + + res = aur.map_srcinfo(srcinfo, 'xpto', fields={*expected_fields.keys()}) + + self.assertEqual(len(expected_fields), len(res), "Expected: {}. Current: {}".format(len(expected_fields), len(res))) + + for key, val in expected_fields.items(): + self.assertIn(key, res, "key '{}' not in res".format(key)) + + if isinstance(val, list): + val.sort() + res[key].sort() + + self.assertEqual(val, res[key], "expected: {}. current: {}".format(val, res[key])) From fede4fb210ac906179e214199192d21a69146006 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 31 Aug 2020 10:19:17 -0300 Subject: [PATCH 65/99] [flatpak] fix -> not handling empty array API response when filling the app data asynchronously --- bauh/gems/flatpak/worker.py | 53 ++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/bauh/gems/flatpak/worker.py b/bauh/gems/flatpak/worker.py index 5f581146..fe1e0403 100644 --- a/bauh/gems/flatpak/worker.py +++ b/bauh/gems/flatpak/worker.py @@ -47,42 +47,45 @@ def run(self): if res and res.text: data = res.json() - if not self.app.version: - self.app.version = data.get('version') + if not data: + self.logger.warning("No data returned for id {} ({})".format(self.app.id, self.app.name)) + else: + if not self.app.version: + self.app.version = data.get('version') - if not self.app.name: - self.app.name = data.get('name') + if not self.app.name: + self.app.name = data.get('name') - self.app.description = data.get('description', data.get('summary', None)) - self.app.icon_url = data.get('iconMobileUrl', None) - self.app.latest_version = data.get('currentReleaseVersion', self.app.version) + self.app.description = data.get('description', data.get('summary', None)) + self.app.icon_url = data.get('iconMobileUrl', None) + self.app.latest_version = data.get('currentReleaseVersion', self.app.version) - if self.app.latest_version and (not self.app.version or not self.app.update): - self.app.version = self.app.latest_version + if self.app.latest_version and (not self.app.version or not self.app.update): + self.app.version = self.app.latest_version - if not self.app.installed and self.app.latest_version: - self.app.version = self.app.latest_version + if not self.app.installed and self.app.latest_version: + self.app.version = self.app.latest_version - if self.app.icon_url and self.app.icon_url.startswith('/'): - self.app.icon_url = FLATHUB_URL + self.app.icon_url + if self.app.icon_url and self.app.icon_url.startswith('/'): + self.app.icon_url = FLATHUB_URL + self.app.icon_url - if data.get('categories'): - cats = [] - for c in data['categories']: - cached = self.category_cache.get(c['name']) + if data.get('categories'): + cats = [] + for c in data['categories']: + cached = self.category_cache.get(c['name']) - if not cached: - cached = self.format_category(c['name']) - self.category_cache.add_non_existing(c['name'], cached) + if not cached: + cached = self.format_category(c['name']) + self.category_cache.add_non_existing(c['name'], cached) - cats.append(cached) + cats.append(cached) - self.app.categories = cats + self.app.categories = cats - loaded_data = self.app.get_data_to_cache() + loaded_data = self.app.get_data_to_cache() - self.api_cache.add(self.app.id, loaded_data) - self.persist = self.app.supports_disk_cache() + self.api_cache.add(self.app.id, loaded_data) + self.persist = self.app.supports_disk_cache() else: self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode())) except: From 1418383730fee4bf1d3b3cbe7ce2e7d3cd9115c5 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 31 Aug 2020 15:01:20 -0300 Subject: [PATCH 66/99] [arch] improvement -> AUR: new settings property 'aur_build_only_chosen' --- CHANGELOG.md | 6 +- README.md | 1 + bauh/commons/system.py | 10 +- bauh/gems/arch/aur.py | 4 +- bauh/gems/arch/config.py | 3 +- bauh/gems/arch/controller.py | 216 +++++++++++++++++++++++------ bauh/gems/arch/makepkg.py | 32 ++++- bauh/gems/arch/pacman.py | 12 +- bauh/gems/arch/resources/locale/ca | 6 + bauh/gems/arch/resources/locale/de | 6 + bauh/gems/arch/resources/locale/en | 6 + bauh/gems/arch/resources/locale/es | 6 + bauh/gems/arch/resources/locale/it | 6 + bauh/gems/arch/resources/locale/pt | 6 + bauh/gems/arch/resources/locale/ru | 6 + bauh/gems/arch/resources/locale/tr | 6 + 16 files changed, 271 insertions(+), 61 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bcfa425..4003fad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues - only removing packages after downloading the required ones - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. + - preventing a possible error when the optional deps of a given package cannot be found - AUR: - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) - new settings property **aur_build_dir** -> it allows to define a custom build dir. @@ -47,7 +48,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- new settings property **aur_remove_build_dir** -> it defines if a package's generated build directory should be removed after the operation is finished (installation, upgrading, ...). Default: true - - preventing a possible error when the optional deps of a given package cannot be found + - new settings property **aur_build_only_chosen**: some AUR packages have a common file definition declaring several packages to be built. When this property is 'true' only the package the user select to install will be built (unless its name is different from those declared in the PKGBUILD base). With a 'null' value a popup asking if the user wants to build all of them will be displayed. 'false' will build and install all packages. Default: true. +

+ +

- Flatpak - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) diff --git a/README.md b/README.md index 7077bead..a7ac525d 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,7 @@ automatch_providers: true # if a possible provider for a given package dependen edit_aur_pkgbuild: false # if the AUR PKGBUILD file should be displayed for edition before the make process. true (PKGBUILD will always be displayed for edition), false (PKGBUILD never will be displayed), null (a popup will ask if the user want to edit the PKGBUILD). Default: false. aur_build_dir: null # defines a custom build directory for AUR packages (a null value will point to /tmp/bauh/arch (non-root user) or /tmp/bauh_root/arch (root user)). Default: null. aur_remove_build_dir: true # it defines if a package's generated build directory should be removed after the operation is finished (installation, upgrading, ...). Options: true, false (default: true). +aur_build_only_chosen : true # some AUR packages have a common file definition declaring several packages to be built. When this property is 'true' only the package the user select to install will be built (unless its name is different from those declared in the PKGBUILD base). With a 'null' value a popup asking if the user wants to build all of them will be displayed. 'false' will build and install all packages. Default: true. ``` - Required dependencies: - **pacman** diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 719b6f25..08f4bc4a 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -164,8 +164,9 @@ def handle(self, process: SystemProcess, error_output: StringIO = None, output_h return process.subproc.returncode is None or process.subproc.returncode == 0 - def handle_simple(self, proc: SimpleProcess, output_handler=None) -> Tuple[bool, str]: - self._notify_watcher((proc.instance.args if isinstance(proc.instance.args, str) else ' '.join(proc.instance.args)) + '\n') + def handle_simple(self, proc: SimpleProcess, output_handler=None, notify_watcher: bool = True) -> Tuple[bool, str]: + if notify_watcher: + self._notify_watcher((proc.instance.args if isinstance(proc.instance.args, str) else ' '.join(proc.instance.args)) + '\n') output = StringIO() for o in proc.instance.stdout: @@ -179,8 +180,9 @@ def handle_simple(self, proc: SimpleProcess, output_handler=None) -> Tuple[bool, if line: if output_handler: output_handler(line) - - self._notify_watcher(line) + + if notify_watcher: + self._notify_watcher(line) proc.instance.wait() output.seek(0) diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index adfb54c6..947f6f76 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -46,7 +46,7 @@ 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: str, fields: Set[str] = None) -> dict: +def map_srcinfo(string: str, pkgname: Optional[str], fields: Set[str] = None) -> dict: subinfos, subinfo = [], {} key_fields = {'pkgname', 'pkgbase'} @@ -72,7 +72,7 @@ def map_srcinfo(string: str, pkgname: str, fields: Set[str] = None) -> dict: pkgnames = {s['pkgname'] for s in subinfos if 'pkgname' in s} return merge_subinfos(subinfos=subinfos, - pkgname=None if (len(pkgnames) == 1 or pkgname not in pkgnames) else pkgname, + pkgname=None if (not pkgname or len(pkgnames) == 1 or pkgname not in pkgnames) else pkgname, fields=fields) diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index d95ae12c..80f412d8 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -17,7 +17,8 @@ def read_config(update_file: bool = False) -> dict: 'automatch_providers': True, 'edit_aur_pkgbuild': False, 'aur_build_dir': None, - 'aur_remove_build_dir': True} + 'aur_remove_build_dir': True, + 'aur_build_only_chosen': True} return read(CONFIG_FILE, template, update_file=update_file) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 9339ebe3..83ec271f 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -43,6 +43,7 @@ from bauh.gems.arch.mapper import ArchDataMapper 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.updates import UpdatesSummarizer from bauh.gems.arch.worker import AURIndexUpdater, ArchDiskCacheUpdater, ArchCompilationOptimizer, SyncDatabases, \ RefreshMirrors @@ -56,6 +57,8 @@ SOURCE_FIELDS = ('source', 'source_x86_64') RE_PRE_DOWNLOAD_WL_PROTOCOLS = re.compile(r'^(.+::)?(https?|ftp)://.+') RE_PRE_DOWNLOAD_BL_EXT = re.compile(r'.+\.(git|gpg)$') +RE_PKGBUILD_PKGNAME = re.compile(r'pkgname\s*=.+') +RE_CONFLICT_DETECTED = re.compile(r'\n::\s*(.+)\s+are in conflict\s*.') class TransactionContext: @@ -63,12 +66,13 @@ class TransactionContext: def __init__(self, name: str = None, base: str = None, maintainer: str = None, watcher: ProcessWatcher = None, handler: ProcessHandler = None, dependency: bool = None, skip_opt_deps: bool = False, root_password: str = None, build_dir: str = None, project_dir: str = None, change_progress: bool = False, arch_config: dict = None, - install_file: str = None, repository: str = None, pkg: ArchPackage = None, + install_files: Set[str] = None, repository: str = None, pkg: ArchPackage = None, remote_repo_map: Dict[str, str] = None, provided_map: Dict[str, Set[str]] = None, remote_provided_map: Dict[str, Set[str]] = None, aur_idx: Set[str] = None, 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): + new_pkg: bool = False, custom_pkgbuild_path: str = None, + pkgs_to_build: Set[str] = None): self.name = name self.base = base self.maintainer = maintainer @@ -82,7 +86,7 @@ def __init__(self, name: str = None, base: str = None, maintainer: str = None, w self.change_progress = change_progress self.repository = repository self.config = arch_config - self.install_file = install_file + self.install_files = install_files self.pkg = pkg self.provided_map = provided_map self.remote_repo_map = remote_repo_map @@ -95,6 +99,8 @@ def __init__(self, name: str = None, base: str = None, maintainer: str = None, w self.disk_cache_updater = disk_cache_updater self.pkgbuild_edited = False self.new_pkg = new_pkg + self.custom_pkgbuild_path = custom_pkgbuild_path + self.pkgs_to_build = pkgs_to_build @classmethod def gen_context_from(cls, pkg: ArchPackage, arch_config: dict, root_password: str, handler: ProcessHandler) -> "TransactionContext": @@ -123,11 +129,14 @@ def gen_dep_context(self, name: str, repository: str): dep_context.removed = {} return dep_context - def has_install_file(self) -> bool: - return self.install_file is not None + def has_install_files(self) -> bool: + return bool(self.install_files) - def get_package_path(self) -> str: - return self.install_file if self.install_file else self.name + def get_packages_paths(self) -> Set[str]: + return self.install_files if self.install_files else {self.name} + + def get_package_names(self) -> Set[str]: + return self.pkgs_to_build if (self.pkgs_to_build and self.install_files) else {self.name} def get_version(self) -> str: return self.pkg.version if self.pkg else None @@ -693,7 +702,7 @@ def _downgrade_repo_pkg(self, context: TransactionContext): context.watcher.change_progress(50) - context.install_file = version_files[versions[0]] + context.install_files = version_files[versions[0]] # TODO verify if not self._handle_missing_deps(context=context): return False @@ -1654,11 +1663,26 @@ def _edit_pkgbuild_and_update_context(self, context: TransactionContext): 'description': 'pkgdesc'}.items(): 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) + if not os.path.exists(src_path): + srcinfo = makepkg.gen_srcinfo(context.project_dir, context.custom_pkgbuild_path) + + with open(src_path, 'w+') as f: + f.write(srcinfo) + else: + with open(src_path) as f: + srcinfo = f.read() + + return srcinfo + def _build(self, context: TransactionContext) -> bool: self._edit_pkgbuild_and_update_context(context) self._pre_download_source(context.name, context.project_dir, context.watcher) self._update_progress(context, 50) + context.custom_pkgbuild_path = self._gen_custom_pkgbuild_if_required(context) + if not self._handle_aur_package_deps_and_keys(context): return False @@ -1673,7 +1697,10 @@ def _build(self, context: TransactionContext) -> bool: cpu_optimized = True try: - pkgbuilt, output = makepkg.make(context.project_dir, optimize=optimize, handler=context.handler) + pkgbuilt, output = makepkg.make(pkgdir=context.project_dir, + optimize=optimize, + handler=context.handler, + custom_pkgbuild=context.custom_pkgbuild_path) finally: if cpu_optimized: self.logger.info("Setting cpus to powersave mode") @@ -1682,7 +1709,32 @@ def _build(self, context: TransactionContext) -> bool: self._update_progress(context, 65) if pkgbuilt: - gen_file = [fname for root, dirs, files in os.walk(context.build_dir) for fname in files if re.match(r'^{}-.+\.tar\.(xz|zst)'.format(context.name), fname)] + self.__fill_aur_output_files(context) + + if self._install(context=context): + self._save_pkgbuild(context) + + if context.dependency or context.skip_opt_deps: + return True + + context.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(context.name))) + + self._update_progress(context, 100) + if self._install_optdeps(context): + return True + + return False + + 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)} + + if output_files: + context.install_files = output_files + else: + gen_file = [fname for root, dirs, files in os.walk(context.build_dir) for fname in files if + re.match(r'^{}-.+\.tar\.(xz|zst)'.format(context.name), fname)] if not gen_file: context.watcher.print('Could not find the built package. Aborting...') @@ -1702,21 +1754,9 @@ def _build(self, context: TransactionContext) -> bool: if perfect_match: file_to_install = perfect_match[0] - context.install_file = '{}/{}'.format(context.project_dir, file_to_install) - - if self._install(context=context): - self._save_pkgbuild(context) - - if context.dependency or context.skip_opt_deps: - return True + context.install_files = {'{}/{}'.format(context.project_dir, file_to_install)} - context.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(context.name))) - - self._update_progress(context, 100) - if self._install_optdeps(context): - return True - - return False + context.watcher.change_substatus('') def _save_pkgbuild(self, context: TransactionContext): cache_path = ArchPackage.disk_cache_path(context.name) @@ -1761,13 +1801,27 @@ def _list_missing_deps(self, context: TransactionContext) -> List[Tuple[str, str ti = time.time() if context.repository == 'aur': - with open('{}/.SRCINFO'.format(context.project_dir)) as f: - srcinfo = aur.map_srcinfo(string=f.read(), pkgname=context.name) + srcinfo = aur.map_srcinfo(string=self._read_srcinfo(context), + pkgname=context.name if (not context.pkgs_to_build or len(context.pkgs_to_build) == 1) else None) + + if context.pkgs_to_build and len(context.pkgs_to_build) > 1: # removing self dependencies from srcinfo + for attr in ('depends', 'makedepends', 'optdepends'): + dep_list = srcinfo.get(attr) + + if dep_list and isinstance(dep_list, list): + to_remove = set() + for dep in dep_list: + dep_name = RE_DEP_OPERATORS.split(dep.split(':')[0])[0].strip() + + if dep_name and dep_name in context.pkgs_to_build: + to_remove.add(dep) + + for dep in to_remove: + dep_list.remove(dep) pkgs_data = {context.name: self.aur_client.map_update_data(context.name, context.get_version(), srcinfo)} else: - file = bool(context.install_file) - pkgs_data = pacman.map_updates_data({context.install_file if file else context.name}, files=file) + pkgs_data = pacman.map_updates_data(context.get_packages_paths(), files=bool(context.install_files)) deps_data, alread_checked_deps = {}, set() @@ -1807,7 +1861,8 @@ def _handle_aur_package_deps_and_keys(self, context: TransactionContext) -> bool check_res = makepkg.check(context.project_dir, optimize=bool(context.config['optimize']), missing_deps=False, - handler=context.handler) + handler=context.handler, + custom_pkgbuild=context.custom_pkgbuild_path) if check_res: if check_res.get('gpg_key'): @@ -1952,22 +2007,28 @@ def _download_packages(self, pkgnames: List[str], handler: ProcessHandler, root_ raise ArchDownloadException() def _install(self, context: TransactionContext) -> bool: - check_install_output = [] - pkgpath = context.get_package_path() + pkgpaths = context.get_packages_paths() context.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(context.name))) self.logger.info("Checking for possible conflicts with '{}'".format(context.name)) - for check_out in SimpleProcess(cmd=['pacman', '-U' if context.install_file else '-S', pkgpath], - root_password=context.root_password, - cwd=context.project_dir or '.').instance.stdout: - check_install_output.append(check_out.decode()) + _, output = context.handler.handle_simple(pacman.install_as_process(pkgpaths=pkgpaths, + root_password=context.root_password, + pkgdir=context.project_dir or '.', + file=bool(context.install_files), + simulate=True), + notify_watcher=False) + # for check_out in SimpleProcess(cmd=['pacman', '-U' if context.install_files else '-S', pkgpath], + # root_password=context.root_password, + # cwd=context.project_dir or '.').instance.stdout: + # check_install_output.append(check_out.decode()) self._update_progress(context, 70) - if check_install_output and 'conflict' in check_install_output[-1]: + if 'unresolvable package conflicts detected' in output: self.logger.info("Conflicts detected for '{}'".format(context.name)) - conflicting_apps = [w[0] for w in re.findall(r'((\w|\-|\.)+)\s(and|are)', check_install_output[-1])] + conflict_msgs = RE_CONFLICT_DETECTED.findall(output) + conflicting_apps = {n.strip() for m in conflict_msgs for n in m.split(' and ')} conflict_msg = ' {} '.format(self.i18n['and']).join([bold(c) for c in conflicting_apps]) if not context.watcher.request_confirmation(title=self.i18n['arch.install.conflict.popup.title'], body=self.i18n['arch.install.conflict.popup.body'].format(conflict_msg)): @@ -1975,7 +2036,8 @@ def _install(self, context: TransactionContext) -> bool: return False else: # uninstall conflicts self._update_progress(context, 75) - to_uninstall = [conflict for conflict in conflicting_apps if conflict != context.name] + names_to_install = context.get_package_names() + to_uninstall = [conflict for conflict in conflicting_apps if conflict not in names_to_install] self.logger.info("Preparing to uninstall conflicting packages: {}".format(to_uninstall)) @@ -2006,7 +2068,7 @@ def _install(self, context: TransactionContext) -> bool: if context.missing_deps: to_install.extend((d[0] for d in context.missing_deps)) - to_install.append(pkgpath) + to_install.extend(pkgpaths) downloaded = 0 if self._multithreaded_download_enabled(context.config): @@ -2027,7 +2089,7 @@ def _install(self, context: TransactionContext) -> bool: else: status_handler = None - installed_with_same_name = self.read_installed(disk_loader=context.disk_loader, internet_available=True, names={context.name}).installed + installed_with_same_name = self.read_installed(disk_loader=context.disk_loader, internet_available=True, names=context.get_package_names()).installed context.watcher.change_substatus(self.i18n['arch.installing.package'].format(bold(context.name))) installed = self._handle_install_call(context=context, to_install=to_install, status_handler=status_handler) @@ -2038,7 +2100,7 @@ def _install(self, context: TransactionContext) -> bool: self._update_progress(context, 95) if installed: - context.installed.add(context.name) + context.installed.update(context.get_package_names()) context.installed.update((p for p in to_install if not p.startswith('/'))) if installed_with_same_name: @@ -2091,7 +2153,7 @@ def _install(self, context: TransactionContext) -> bool: def _call_pacman_install(self, context: TransactionContext, to_install: List[str], overwrite_files: bool, status_handler: Optional[object] = None) -> Tuple[bool, str]: return context.handler.handle_simple(pacman.install_as_process(pkgpaths=to_install, root_password=context.root_password, - file=context.has_install_file(), + file=context.has_install_files(), pkgdir=context.project_dir, overwrite_conflicting_files=overwrite_files), output_handler=status_handler.handle if status_handler else None) @@ -2520,6 +2582,17 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: only_int=True, max_width=max_width, value=local_config['mirrors_sort_limit'] if isinstance(local_config['mirrors_sort_limit'], int) else ''), + new_select(id_='aur_build_only_chosen', + label=self.i18n['arch.config.aur_build_only_chosen'], + tip=self.i18n['arch.config.aur_build_only_chosen.tip'], + opts=[(self.i18n['yes'].capitalize(), True, None), + (self.i18n['no'].capitalize(), False, None), + (self.i18n['ask'].capitalize(), None, None), + ], + value=local_config['aur_build_only_chosen'], + max_width=max_width, + type_=SelectViewType.RADIO, + capitalize_label=False), new_select(label=self.i18n['arch.config.edit_aur_pkgbuild'], tip=self.i18n['arch.config.edit_aur_pkgbuild.tip'], id_='edit_aur_pkgbuild', @@ -2565,6 +2638,7 @@ def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: config['edit_aur_pkgbuild'] = form_install.get_component('edit_aur_pkgbuild').get_selected() config['aur_remove_build_dir'] = form_install.get_component('aur_remove_build_dir').get_selected() config['aur_build_dir'] = form_install.get_component('aur_build_dir').file_path + config['aur_build_only_chosen'] = form_install.get_component('aur_build_only_chosen').get_selected() if not config['aur_build_dir']: config['aur_build_dir'] = None @@ -2932,3 +3006,61 @@ def setup_snapd(self, root_password: str, watcher: ProcessWatcher) -> bool: body=self.i18n['snap.custom_action.setup_snapd.ready.body'], type_=MessageType.INFO) return True + + def _gen_custom_pkgbuild_if_required(self, context: TransactionContext) -> Optional[str]: + build_only_chosen = context.config.get('aur_build_only_chosen') + + pkgs_to_build = aur.map_srcinfo(string=self._read_srcinfo(context), pkgname=None, fields={'pkgname'}).get('pkgname') + + if isinstance(pkgs_to_build, str): + pkgs_to_build = {pkgs_to_build} + else: + pkgs_to_build = {*pkgs_to_build} + + if build_only_chosen is False: + context.pkgs_to_build = pkgs_to_build + return + + # checking if more than one package is mapped for this pkgbuild + + if not pkgs_to_build or not isinstance(pkgs_to_build, set) or len(pkgs_to_build) == 1 or context.name not in pkgs_to_build: + context.pkgs_to_build = pkgs_to_build + return + + if build_only_chosen is None: + if not context.dependency: + pkgnames = [InputOption(label=n, value=n, read_only=False) for n in pkgs_to_build if n != context.name] + select = MultipleSelectComponent(label='', + options=pkgnames, + default_options={*pkgnames}, + max_per_line=1) + + if not context.watcher.request_confirmation(title=self.i18n['warning'].capitalize(), + body=self.i18n['arch.aur.sync.several_names.popup.body'].format(bold(context.name)) + ':', + components=[select], + confirmation_label=self.i18n['arch.aur.sync.several_names.popup.bt_only_chosen'].format(context.name), + 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) + with open(pkgbuild_path) as f: + current_pkgbuild = f.read() + + if context.pkgs_to_build: + names = '({})'.format(' '.join(("'{}'".format(p) for p in context.pkgs_to_build))) + else: + 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' + + with open(custom_pkgbuild_path, 'w+') as f: + f.write(new_pkgbuild) + + new_srcinfo = makepkg.gen_srcinfo(context.project_dir, custom_pkgbuild_path) + + with open('{}/.SRCINFO'.format(context.project_dir), 'w+') as f: + f.write(new_srcinfo) + + return custom_pkgbuild_path diff --git a/bauh/gems/arch/makepkg.py b/bauh/gems/arch/makepkg.py index a89964fe..60b26640 100644 --- a/bauh/gems/arch/makepkg.py +++ b/bauh/gems/arch/makepkg.py @@ -1,6 +1,6 @@ import os import re -from typing import Tuple +from typing import Tuple, Optional, Set from bauh.commons.system import SimpleProcess, ProcessHandler, run_cmd from bauh.gems.arch import CUSTOM_MAKEPKG_FILE @@ -9,18 +9,23 @@ RE_UNKNOWN_GPG_KEY = re.compile(r'\(unknown public key (\w+)\)') -def gen_srcinfo(build_dir: str) -> str: - return run_cmd('makepkg --printsrcinfo', cwd=build_dir) +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 check(pkgdir: str, optimize: bool, missing_deps: bool, handler: ProcessHandler) -> dict: +def check(pkgdir: str, optimize: bool, missing_deps: bool, handler: ProcessHandler, custom_pkgbuild: Optional[str] = None) -> dict: res = {} - cmd = ['makepkg', '-ALcf', '--check', '--noarchive', '--nobuild', '--noprepare'] + cmd = ['makepkg', '-ALcfm', '--check', '--noarchive', '--nobuild', '--noprepare'] if not missing_deps: cmd.append('--nodeps') + if custom_pkgbuild: + cmd.append('-p') + cmd.append(custom_pkgbuild) + if optimize: if os.path.exists(CUSTOM_MAKEPKG_FILE): handler.watcher.print('Using custom makepkg.conf -> {}'.format(CUSTOM_MAKEPKG_FILE)) @@ -44,9 +49,13 @@ def check(pkgdir: str, optimize: bool, missing_deps: bool, handler: ProcessHandl return res -def make(pkgdir: str, optimize: bool, handler: ProcessHandler) -> Tuple[bool, str]: +def make(pkgdir: str, optimize: bool, handler: ProcessHandler, custom_pkgbuild: Optional[str] = None) -> Tuple[bool, str]: cmd = ['makepkg', '-ALcsmf', '--skipchecksums'] + if custom_pkgbuild: + cmd.append('-p') + cmd.append(custom_pkgbuild) + if optimize: if os.path.exists(CUSTOM_MAKEPKG_FILE): handler.watcher.print('Using custom makepkg.conf -> {}'.format(CUSTOM_MAKEPKG_FILE)) @@ -66,3 +75,14 @@ def update_srcinfo(project_dir: str) -> bool: return True return False + + +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 output: + return {p.strip() for p in output.split('\n') if p} + + return set() diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index bc247590..e2e9d0b5 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -141,11 +141,13 @@ def map_installed(names: Iterable[str] = None) -> dict: # returns a dict with w return pkgs -def install_as_process(pkgpaths: Iterable[str], root_password: str, file: bool, pkgdir: str = '.', overwrite_conflicting_files: bool = False) -> SimpleProcess: - if file: - cmd = ['pacman', '-U', *pkgpaths, '--noconfirm'] # pkgpath = install file path - else: - cmd = ['pacman', '-S', *pkgpaths, '--noconfirm'] # pkgpath = pkgname +def install_as_process(pkgpaths: Iterable[str], root_password: str, file: bool, pkgdir: str = '.', + overwrite_conflicting_files: bool = False, simulate: bool = False) -> SimpleProcess: + cmd = ['pacman', '-U'] if file else ['pacman', '-S'] + cmd.extend(pkgpaths) + + if not simulate: + cmd.append('--noconfirm') if overwrite_conflicting_files: cmd.append('--overwrite=*') diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 8ac03378..0fd756c4 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -21,6 +21,10 @@ arch.aur.install.validity_check.body=Alguns dels fitxers font necessaris per a l arch.aur.install.validity_check.proceed=Voleu continuar de totes maneres? ( no es recomana ) arch.aur.install.validity_check.title=Problemes d’integritat {} arch.aur.install.verifying_pgp=S’estan comprovant les claus PGP +arch.aur.build.list_output=Checking built files +arch.aur.sync.several_names.popup.body=The definition file (PKGBUILD) of {} configures the build of other packages +arch.aur.sync.several_names.popup.bt_only_chosen=Build only {} +arch.aur.sync.several_names.popup.bt_selected=Build selected too arch.building.package=S’està compilant el paquet {} arch.checking.conflicts=S’està comprovant si hi ha conflictes amb {} arch.checking.deps=S’estan comprovant les dependències de {} @@ -32,6 +36,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_build_only_chosen=Build only chosen (AUR) +arch.config.aur_build_only_chosen.tip=Some AUR packages have a common PKGBUILD shared with other packages and that defines build instructions for each one. This property enabled will ensure that only the chosen package will be built. arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Elimina les versions antigues diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index f9a0a0d5..342e8775 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -21,6 +21,10 @@ arch.aur.install.validity_check.body=Einige der Source-Dateien für die Installa arch.aur.install.validity_check.proceed=Wollen Sie trotzdem fortfahren? ( nicht empfohlen ) arch.aur.install.validity_check.title=Integritätsprobleme {} arch.aur.install.verifying_pgp=PGP Schlüssel überprüfen +arch.aur.build.list_output=Checking built files +arch.aur.sync.several_names.popup.body=The definition file (PKGBUILD) of {} configures the build of other packages +arch.aur.sync.several_names.popup.bt_only_chosen=Build only {} +arch.aur.sync.several_names.popup.bt_selected=Build selected too arch.building.package=Paket {} erstellen arch.checking.conflicts=Konflikte mit {} überprüfen arch.checking.deps={} Abhängigkeiten überprüfen @@ -32,6 +36,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_build_only_chosen=Build only chosen (AUR) +arch.config.aur_build_only_chosen.tip=Some AUR packages have a common PKGBUILD shared with other packages and that defines build instructions for each one. This property enabled will ensure that only the chosen package will be built. arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 64d76b21..74add205 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -22,6 +22,10 @@ arch.aur.install.validity_check.body=Some of the source-files needed for {} inst arch.aur.install.validity_check.proceed=Do you want to continue anyway ? ( not recommended ) arch.aur.install.validity_check.title=Integrity issues {} arch.aur.install.verifying_pgp=Verifying PGP keys +arch.aur.build.list_output=Checking built files +arch.aur.sync.several_names.popup.body=The definition file (PKGBUILD) of {} configures the build of other packages +arch.aur.sync.several_names.popup.bt_only_chosen=Build only {} +arch.aur.sync.several_names.popup.bt_selected=Build selected too arch.building.package=Building package {} arch.checking.conflicts=Checking any conflicts with {} arch.checking.deps=Checking {} dependencies @@ -33,6 +37,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_build_only_chosen=Build only chosen (AUR) +arch.config.aur_build_only_chosen.tip=Some AUR packages have a common PKGBUILD shared with other packages and that defines build instructions for each one. This property enabled will ensure that only the chosen package will be built. arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 99d6f2bb..87feee95 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -21,6 +21,10 @@ arch.aur.install.validity_check.body=Algunos de los archivos fuente necesarios p arch.aur.install.validity_check.proceed=¿Desea continuar de todos modos? ( no recomendado ) arch.aur.install.validity_check.title=Problemas de integridad {} arch.aur.install.verifying_pgp=Verificando claves PGP +arch.aur.build.list_output=Checking built files +arch.aur.sync.several_names.popup.body=El archivo de definición (PKGBUILD) de {} configura la compilación de otros paquetes +arch.aur.sync.several_names.popup.bt_only_chosen=Compilar sólo {} +arch.aur.sync.several_names.popup.bt_selected=Compilar seleccionados también arch.building.package=Construyendo el paquete {} arch.checking.conflicts=Verificando se hay conflictos con {} arch.checking.deps=Verificando las dependencias de {} @@ -32,6 +36,8 @@ arch.config.automatch_providers=Autodefinir proveedores de dependencia arch.config.automatch_providers.tip=Elige automáticamente qué proveedor se usará para una dependencia de paquete cuando ambos nombres son iguales. arch.config.aur_build_dir=Directorio de compilación (AUR) arch.config.aur_build_dir.tip=Define un directorio personalizado donde se construirán los paquetes AUR. Defecto: {}. +arch.config.aur_build_only_chosen=Compilar solo elegido (AUR) +arch.config.aur_build_only_chosen.tip=Algunos paquetes AUR tienen un PKGBUILD común compartido con otros paquetes y que define las instrucciones de construcción para cada uno. Esta propiedad habilitada garantizará que solo se compile el paquete elegido. arch.config.aur_remove_build_dir=Eliminar directorio de compilación (AUR) arch.config.aur_remove_build_dir.tip=Si el directorio de compilación generado para un paquete debe ser eliminado una vez finalizada la operación. arch.config.clean_cache=Eliminar versiones antiguas diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index b17e9707..bfe1eff2 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -21,6 +21,10 @@ arch.aur.install.validity_check.body=Alcuni dei file di origine necessari per l' arch.aur.install.validity_check.proceed=Vuoi continuare comunque? ( non consigliato ) arch.aur.install.validity_check.title=Problemi di integrità {} arch.aur.install.verifying_pgp=Verifica chiavi PGP +arch.aur.build.list_output=Checking built files +arch.aur.sync.several_names.popup.body=The definition file (PKGBUILD) of {} configures the build of other packages +arch.aur.sync.several_names.popup.bt_only_chosen=Build only {} +arch.aur.sync.several_names.popup.bt_selected=Build selected too arch.building.package=Pacchetto costruito {} arch.checking.conflicts=Verifica di eventuali conflitti con {} arch.checking.deps=Verifica di {} dipendenze @@ -32,6 +36,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_build_only_chosen=Build only chosen (AUR) +arch.config.aur_build_only_chosen.tip=Some AUR packages have a common PKGBUILD shared with other packages and that defines build instructions for each one. This property enabled will ensure that only the chosen package will be built. arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Rimuovi le vecchie versioni diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index ea9b24e2..fe526426 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -22,6 +22,10 @@ arch.aur.install.validity_check.body=Alguns dos arquivos-fonte necessários para arch.aur.install.validity_check.proceed=Você deseja continuar mesmo assim ? ( não recomendado ) arch.aur.install.validity_check.title=Problemas de integridade {} arch.aur.install.verifying_pgp=Verificando chaves PGP +arch.aur.build.list_output=Verificando arquivos construídos +arch.aur.sync.several_names.popup.body=O arquivo de definição (PKGBUILD) de {} configura a construção de outros pacotes +arch.aur.sync.several_names.popup.bt_only_chosen=Construir somente {} +arch.aur.sync.several_names.popup.bt_selected=Construir selecionados também arch.building.package=Construindo o pacote {} arch.checking.conflicts=Verificando se há conflitos com {} arch.checking.deps=Verificando as dependências de {} @@ -33,6 +37,8 @@ arch.config.automatch_providers=Auto-definir provedores de dependências arch.config.automatch_providers.tip=Escolhe automaticamente qual provedor será utilizado para determinada dependência de um pacote caso os nomes de ambos sejam iguais. arch.config.aur_build_dir=Diretório de construção (AUR) arch.config.aur_build_dir.tip=Define um diretório personalizado onde pacotes do AUR serão construídos. Padrão: {}. +arch.config.aur_build_only_chosen=Construir somente escolhido (AUR) +arch.config.aur_build_only_chosen.tip=Alguns pacotes do AUR têm um PKGBUILD comum a outros pacotes e que define a construção para todos. Essa propriedade ativada garantirá que somente o pacote escolhido será construído. arch.config.aur_remove_build_dir=Remover diretório de construção (AUR) arch.config.aur_remove_build_dir.tip=Se o diretório gerado para a construção de um pacote do AUR deve ser removido após a operação ser finalizada. arch.config.clean_cache=Remover versões antigas diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 05d256e1..02329c12 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -21,6 +21,10 @@ arch.aur.install.validity_check.body=Некоторые исходные фай arch.aur.install.validity_check.proceed=Вы всё равно хотите продолжить ? ( не рекомендуется ) arch.aur.install.validity_check.title=Проблемы целостности arch.aur.install.verifying_pgp=Проверка ключей PGP +arch.aur.build.list_output=Checking built files +arch.aur.sync.several_names.popup.body=The definition file (PKGBUILD) of {} configures the build of other packages +arch.aur.sync.several_names.popup.bt_only_chosen=Build only {} +arch.aur.sync.several_names.popup.bt_selected=Build selected too arch.building.package=Сборка пакета {} arch.checking.conflicts=Проверка конфликтов с {} arch.checking.deps=Проверка зависимостей {} @@ -32,6 +36,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_build_only_chosen=Build only chosen (AUR) +arch.config.aur_build_only_chosen.tip=Some AUR packages have a common PKGBUILD shared with other packages and that defines build instructions for each one. This property enabled will ensure that only the chosen package will be built. arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 5b52a195..74efb19d 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -21,6 +21,10 @@ arch.aur.install.validity_check.body={} kurulumu için gereken kaynak dosyalarda arch.aur.install.validity_check.proceed=Yine de devam etmek istiyor musunuz? ( önerilmez ) arch.aur.install.validity_check.title=Bütünlük sorunları {} arch.aur.install.verifying_pgp=PGP anahtarları doğrulanıyor +arch.aur.build.list_output=Checking built files +arch.aur.sync.several_names.popup.body=The definition file (PKGBUILD) of {} configures the build of other packages +arch.aur.sync.several_names.popup.bt_only_chosen=Build only {} +arch.aur.sync.several_names.popup.bt_selected=Build selected too arch.building.package=Paket inşa ediliyor {} arch.checking.conflicts={} ile çakışmalar kontrol ediliyor arch.checking.deps={} bağımlılıkları kontrol ediliyor @@ -32,6 +36,8 @@ arch.config.automatch_providers=Auto-define dependency providers arch.config.automatch_providers.tip=It automatically chooses which provider will be used for a package dependency when both names are equal. arch.config.aur_build_dir=Build directory (AUR) arch.config.aur_build_dir.tip=It define a custom directory where the AUR packages will be built. Default: {}. +arch.config.aur_build_only_chosen=Build only chosen (AUR) +arch.config.aur_build_only_chosen.tip=Some AUR packages have a common PKGBUILD shared with other packages and that defines build instructions for each one. This property enabled will ensure that only the chosen package will be built. arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Önbelleği temizle From 9fabe9ed38fb1040e89b7f28fed9e3415935a73b Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 31 Aug 2020 15:04:39 -0300 Subject: [PATCH 67/99] Updating CHANGELOG.md --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4003fad3..60ce0294 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,12 +36,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- - upgrade: + - upgrade: - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues - only removing packages after downloading the required ones - - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. - - preventing a possible error when the optional deps of a given package cannot be found - - AUR: + - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. + - preventing a possible error when the optional deps of a given package cannot be found + - AUR: - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) - new settings property **aur_build_dir** -> it allows to define a custom build dir.

From d7225dab3b35f6baf4d6b3baaf9b16323f8db809 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 31 Aug 2020 15:15:41 -0300 Subject: [PATCH 68/99] Updating CHANGELOG.md --- CHANGELOG.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60ce0294..008f4cbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,26 +32,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - trying to auto-fill the 'Name' and 'Version' fields - Arch - initializing task "Organizing data from installed packages" is taking about 80% less time (now is called "Indexing packages data") [#131](https://github.com/vinifmor/bauh/issues/131) - - upgrade summary: displaying the reason a given package must be installed -

- -

- - upgrade: + - upgrade - upgrading firstly the keyring packages declared in **SyncFirst** (**/etc/pacman.conf**) to avoid pacman downloading issues - only removing packages after downloading the required ones - - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. - - preventing a possible error when the optional deps of a given package cannot be found - - AUR: + - summary: displaying the reason a given package must be installed +

+ +

+ + - AUR - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) - new settings property **aur_build_dir** -> it allows to define a custom build dir.

+ - new settings property **aur_remove_build_dir** -> it defines if a package's generated build directory should be removed after the operation is finished (installation, upgrading, ...). Default: true - new settings property **aur_build_only_chosen**: some AUR packages have a common file definition declaring several packages to be built. When this property is 'true' only the package the user select to install will be built (unless its name is different from those declared in the PKGBUILD base). With a 'null' value a popup asking if the user wants to build all of them will be displayed. 'false' will build and install all packages. Default: true.

+ + - "Multi-threaded download (repositories)" is not the default behavior anymore (current pacman download approach is faster). If your settings has this property set as 'Yes', just change it to 'No'. + - preventing a possible error when the optional deps of a given package cannot be found - Flatpak - creating the exports path **~/.local/share/flatpak/exports/share** (if it does not exist) and adding it to install/upgrade/downgrade/remove commands path to prevent warning messages. [#128](https://github.com/vinifmor/bauh/issues/128) From feb061efa40f7c52205d0c38cbe9134ce2c4d7ea Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Tue, 1 Sep 2020 19:36:05 -0300 Subject: [PATCH 69/99] [arch] improvement -> upgrade: checking specific version requirements and marking packages as 'cannot upgrade' when these requirements are not met --- CHANGELOG.md | 14 +- README.md | 1 + bauh/api/abstract/controller.py | 8 +- bauh/gems/arch/config.py | 3 +- bauh/gems/arch/controller.py | 6 + bauh/gems/arch/model.py | 5 + bauh/gems/arch/pacman.py | 4 +- bauh/gems/arch/resources/locale/ca | 3 + bauh/gems/arch/resources/locale/de | 3 + bauh/gems/arch/resources/locale/en | 3 + bauh/gems/arch/resources/locale/es | 3 + bauh/gems/arch/resources/locale/it | 3 + bauh/gems/arch/resources/locale/pt | 3 + bauh/gems/arch/resources/locale/ru | 3 + bauh/gems/arch/resources/locale/tr | 3 + bauh/gems/arch/updates.py | 230 ++++++++++++++++++++++++++--- bauh/view/qt/components.py | 1 + bauh/view/qt/thread.py | 33 +++-- 18 files changed, 286 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 008f4cbf..9c8681f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,9 +37,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - only removing packages after downloading the required ones - summary: displaying the reason a given package must be installed

- +

- + + - checking specific version requirements and marking packages as "cannot upgrade" when these requirements are not met (e.g: package A depends on version 1.0 of B. If A and B were selected to upgrade, and B would be upgrade to 2.0, then B would be excluded from the transaction. This new checking behavior can be disabled through the property (**check_dependency_breakage**): +

+ +

+ - AUR - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) - new settings property **aur_build_dir** -> it allows to define a custom build dir. @@ -77,6 +82,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - upgrade summary - not displaying all packages that must be uninstalled - displaying "required size" for packages that must be uninstalled + - not displaying packages that cannot upgrade due to specific version requirements (e.g: package A requires version 1.0 of package B, however package B will be upgrade to version 2.0) +

+ +

+ - some conflict resolution scenarios when upgrading several packages - not handling conflicting files errors during the installation process - AUR diff --git a/README.md b/README.md index a7ac525d..cfa0039c 100644 --- a/README.md +++ b/README.md @@ -197,6 +197,7 @@ edit_aur_pkgbuild: false # if the AUR PKGBUILD file should be displayed for edi aur_build_dir: null # defines a custom build directory for AUR packages (a null value will point to /tmp/bauh/arch (non-root user) or /tmp/bauh_root/arch (root user)). Default: null. aur_remove_build_dir: true # it defines if a package's generated build directory should be removed after the operation is finished (installation, upgrading, ...). Options: true, false (default: true). aur_build_only_chosen : true # some AUR packages have a common file definition declaring several packages to be built. When this property is 'true' only the package the user select to install will be built (unless its name is different from those declared in the PKGBUILD base). With a 'null' value a popup asking if the user wants to build all of them will be displayed. 'false' will build and install all packages. Default: true. +check_dependency_breakage: true # If, during the verification of the update requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. If A and B were selected to upgrade, and B would be upgrade to 2.0, then B would be excluded from the transaction. Default: true. ``` - Required dependencies: - **pacman** diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 8ce3f477..cf496949 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -30,18 +30,24 @@ def __init__(self, installed: Optional[List[SoftwarePackage]], new: Optional[Lis class UpgradeRequirement: - def __init__(self, pkg: SoftwarePackage, reason: str = None, required_size: int = None, extra_size: int = None): + def __init__(self, pkg: SoftwarePackage, reason: str = None, required_size: int = None, extra_size: int = None, sorting_priority: int = 0): """ :param pkg: :param reason: :param required_size: size in BYTES required to upgrade the package :param extra_size: the extra size IN BYTES the upgrade will allocate in relation to the already allocated + :param sorting_priority: an int representing the sorting priority (higher numbers = higher priority) """ self.pkg = pkg self.reason = reason self.required_size = required_size self.extra_size = extra_size + self.sorting_priority = sorting_priority + + @staticmethod + def sort_by_priority(req: "UpgradeRequirement") -> Tuple[int, str]: + return -req.sorting_priority, req.pkg.name class UpgradeRequirements: diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index 80f412d8..402c0ce2 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -18,7 +18,8 @@ def read_config(update_file: bool = False) -> dict: 'edit_aur_pkgbuild': False, 'aur_build_dir': None, 'aur_remove_build_dir': True, - 'aur_build_only_chosen': True} + 'aur_build_only_chosen': True, + 'check_dependency_breakage': True} return read(CONFIG_FILE, template, update_file=update_file) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 83ec271f..b6511bc2 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -2554,6 +2554,11 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: tooltip_key='arch.config.automatch_providers.tip', value=bool(local_config['automatch_providers']), max_width=max_width), + self._gen_bool_selector(id_='check_dependency_breakage', + label_key='arch.config.check_dependency_breakage', + tooltip_key='arch.config.check_dependency_breakage.tip', + value=bool(local_config['check_dependency_breakage']), + max_width=max_width), self._gen_bool_selector(id_='mthread_download', label_key='arch.config.pacman_mthread_download', tooltip_key='arch.config.pacman_mthread_download.tip', @@ -2639,6 +2644,7 @@ def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: config['aur_remove_build_dir'] = form_install.get_component('aur_remove_build_dir').get_selected() config['aur_build_dir'] = form_install.get_component('aur_build_dir').file_path config['aur_build_only_chosen'] = form_install.get_component('aur_build_only_chosen').get_selected() + config['check_dependency_breakage'] = form_install.get_component('check_dependency_breakage').get_selected() if not config['aur_build_dir']: config['aur_build_dir'] = None diff --git a/bauh/gems/arch/model.py b/bauh/gems/arch/model.py index d72c256d..a78f4287 100644 --- a/bauh/gems/arch/model.py +++ b/bauh/gems/arch/model.py @@ -180,3 +180,8 @@ def get_custom_supported_actions(self) -> List[CustomSoftwareAction]: else: return ACTIONS_AUR_ENABLE_PKGBUILD_EDITION + def __hash__(self): + if self.view_name is not None: + return hash((self.view_name, self.repository)) + else: + return hash((self.name, self.repository)) diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index e2e9d0b5..fab93ed9 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -914,8 +914,8 @@ def get_cache_dir() -> str: return '/var/cache/pacman/pkg' -def map_required_by(names: Iterable[str] = None) -> Dict[str, Set[str]]: - output = run_cmd('pacman -Qi {}'.format(' '.join(names) if names else '')) +def map_required_by(names: Iterable[str] = None, remote: bool = False) -> Dict[str, Set[str]]: + output = run_cmd('pacman -{} {}'.format('Sii' if remote else 'Qi', ' '.join(names) if names else ''), print_error=False) if output: res = {} diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 0fd756c4..4350d047 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -42,6 +42,8 @@ arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Elimina les versions antigues arch.config.clean_cache.tip=Si cal eliminar les versions antigues d'un paquet emmagatzemat al disc durant la desinstal·lació +arch.config.check_dependency_breakage=Check dependency version breakage +arch.config.check_dependency_breakage.tip=If, during the verification of upgrade requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Mirrors sort limit @@ -196,6 +198,7 @@ arch.substatus.integrity=Checking packages integrity arch.substatus.keyring=Checking keyring arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks +arch.sync.dep_breakage.reason={} requires {} arch.sync_databases.substatus=Synchronizing package databases arch.sync_databases.substatus.error=It was not possible to synchronize the package database arch.task.disk_cache=Indexing packages data diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 342e8775..9e9ce49b 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -42,6 +42,8 @@ arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall +arch.config.check_dependency_breakage=Check dependency version breakage +arch.config.check_dependency_breakage.tip=If, during the verification of upgrade requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Mirrors sort limit @@ -196,6 +198,7 @@ arch.substatus.integrity=Checking packages integrity arch.substatus.keyring=Checking keyring arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks +arch.sync.dep_breakage.reason={} requires {} arch.sync_databases.substatus=Synchronizing package databases arch.sync_databases.substatus.error=It was not possible to synchronize the package database arch.task.disk_cache=Indexing packages data diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 74add205..13bb8423 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -43,6 +43,8 @@ arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall +arch.config.check_dependency_breakage=Check dependency version breakage +arch.config.check_dependency_breakage.tip=If, during the verification of upgrade requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Mirrors sort limit @@ -197,6 +199,7 @@ arch.substatus.integrity=Checking packages integrity arch.substatus.keyring=Checking keyring arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks +arch.sync.dep_breakage.reason={} requires {} arch.sync_databases.substatus=Synchronizing package databases arch.sync_databases.substatus.error=It was not possible to synchronize the package database arch.task.disk_cache=Indexing packages data diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 87feee95..bcf416a5 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -42,6 +42,8 @@ arch.config.aur_remove_build_dir=Eliminar directorio de compilación (AUR) arch.config.aur_remove_build_dir.tip=Si el directorio de compilación generado para un paquete debe ser eliminado una vez finalizada la operación. arch.config.clean_cache=Eliminar versiones antiguas arch.config.clean_cache.tip=Si las versiones antiguas de un paquete almacenado en el disco deben ser eliminadas durante la desinstalación +arch.config.check_dependency_breakage=Verificar rotura de versión de dependencia +arch.config.check_dependency_breakage.tip=Si, durante la verificación de los requisitos de actualización, también se deben verificar versiones específicas de las dependencias. Ejemplo: el paquete A depende de la versión 1.0 de B. arch.config.edit_aur_pkgbuild=Editar PKGBUILD (AUR) arch.config.edit_aur_pkgbuild.tip=Si el archivo PKGBUILD de un paquete AUR debe ser exhibido para edición antes de su instalación/actualización/degradación arch.config.mirrors_sort_limit=Límite de ordenación de espejos @@ -196,6 +198,7 @@ arch.substatus.integrity=Verificando la integridad de los paquetes arch.substatus.keyring=Verificando keyring arch.substatus.loading_files=Cargando archivos de los paquetes arch.substatus.pre_hooks=Ejecutando ganchos pre-transacción +arch.sync.dep_breakage.reason={} necesita de {} arch.sync_databases.substatus=Sincronizando bases de paquetes arch.sync_databases.substatus.error=No fue posible sincronizar la base de paquetes arch.task.disk_cache=Indexando datos de paquetes diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index bfe1eff2..ddad0842 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -42,6 +42,8 @@ arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Rimuovi le vecchie versioni arch.config.clean_cache.tip=Se le vecchie versioni di un pacchetto memorizzate sul disco devono essere rimosse durante la disinstallazione +arch.config.check_dependency_breakage=Check dependency version breakage +arch.config.check_dependency_breakage.tip=If, during the verification of upgrade requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Mirrors sort limit @@ -196,6 +198,7 @@ arch.substatus.integrity=Checking packages integrity arch.substatus.keyring=Checking keyring arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks +arch.sync.dep_breakage.reason={} requires {} arch.sync_databases.substatus=Synchronizing package databases arch.sync_databases.substatus.error=It was not possible to synchronize the package database arch.task.disk_cache=Indexing packages data diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index fe526426..21d92811 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -43,6 +43,8 @@ arch.config.aur_remove_build_dir=Remover diretório de construção (AUR) arch.config.aur_remove_build_dir.tip=Se o diretório gerado para a construção de um pacote do AUR deve ser removido após a operação ser finalizada. arch.config.clean_cache=Remover versões antigas arch.config.clean_cache.tip=Se versões antigas de um pacote armazenadas em disco devem ser removidas durante a desinstalação +arch.config.check_dependency_breakage=Verificar quebra de dependência de versão +arch.config.check_dependency_breakage.tip=Se durante a verificação dos requisitos de atualização deve-se verificar também versões específicas de dependências. Exemplo: pacote A depende da versão 1.0 de B. arch.config.edit_aur_pkgbuild=Editar PKGBUILD (AUR) arch.config.edit_aur_pkgbuild.tip=Se o arquivo PKGBUILD de um pacote do AUR deve ser exibido para edição antes da instalação/atualização/reversão arch.config.mirrors_sort_limit=Limite de ordenação de espelhos @@ -196,6 +198,7 @@ arch.substatus.integrity=Verificando a integridade dos pacotes arch.substatus.keyring=Verificando o keyring arch.substatus.loading_files=Carregando os arquivos dos pacotes arch.substatus.pre_hooks=Executando ganchos pré-transação +arch.sync.dep_breakage.reason={} precisa de {} arch.sync_databases.substatus=Sincronizando bases de pacotes arch.sync_databases.substatus.error=Não foi possível sincronizar as bases de pacotes arch.task.disk_cache=Indexando dados de pacotes diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 02329c12..51fe3fa1 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -42,6 +42,8 @@ arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Remove old versions arch.config.clean_cache.tip=Whether old versions of a package stored on disk should be removed during uninstall +arch.config.check_dependency_breakage=Check dependency version breakage +arch.config.check_dependency_breakage.tip=If, during the verification of upgrade requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Ограничение сортировки зеркал @@ -196,6 +198,7 @@ arch.substatus.integrity=Checking packages integrity arch.substatus.keyring=Checking keyring arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks +arch.sync.dep_breakage.reason={} requires {} arch.sync_databases.substatus=Синхронизация баз данных пакетов arch.sync_databases.substatus.error=Синхронизировать базу данных пакета не удалось arch.task.disk_cache=Indexing packages data diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 74efb19d..0ae6eb28 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -42,6 +42,8 @@ arch.config.aur_remove_build_dir=Remove build directory (AUR) arch.config.aur_remove_build_dir.tip=If a package's generated build directory should be removed after the operations is finished. arch.config.clean_cache=Önbelleği temizle arch.config.clean_cache.tip=Disk üzerinde kurulu bir paketin eski sürümlerinin kaldırma sırasında kaldırılıp kaldırılmayacağı +arch.config.check_dependency_breakage=Check dependency version breakage +arch.config.check_dependency_breakage.tip=If, during the verification of upgrade requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. arch.config.edit_aur_pkgbuild=Edit PKGBUILD (AUR) arch.config.edit_aur_pkgbuild.tip=If the PKGBUILD file of an AUR package should be displayed for editing before its installation/upgrade/downgrade arch.config.mirrors_sort_limit=Yansı sıralama sınırı @@ -196,6 +198,7 @@ arch.substatus.integrity=Checking packages integrity arch.substatus.keyring=Checking keyring arch.substatus.loading_files=Loading package files arch.substatus.pre_hooks=Running pre-transaction hooks +arch.sync.dep_breakage.reason={} requires {} arch.sync_databases.substatus=Paket veritabanı eşitleniyor arch.sync_databases.substatus.error=Paket veritabanı eşitlenemedi arch.task.disk_cache=Indexing packages data diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index b00bc55b..be96550a 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -1,7 +1,8 @@ import logging import time +from distutils.version import LooseVersion from threading import Thread -from typing import Dict, Set, List, Tuple, Iterable +from typing import Dict, Set, List, Tuple, Iterable, Optional from bauh.api.abstract.controller import UpgradeRequirements, UpgradeRequirement from bauh.api.abstract.handler import ProcessWatcher @@ -261,38 +262,53 @@ def _fill_to_install(self, context: UpdateRequirementsContext) -> bool: context.pkgs_data.update(all_to_install_data) self._fill_conflicts(context, context.to_remove.keys()) + if context.to_install: + self.__fill_provided_map(context=context, pkgs=context.to_install, fill_installed=False) + tf = time.time() self.logger.info("It took {0:.2f} seconds to retrieve required upgrade packages".format(tf - ti)) return True - def __fill_provided_map(self, context: UpdateRequirementsContext): - ti = time.time() - self.logger.info("Filling provided names") - context.installed_names = pacman.list_installed_names() - installed_to_ignore = set() + def __fill_provided_map(self, context: UpdateRequirementsContext, pkgs: Dict[str, ArchPackage], fill_installed: bool = True): + if pkgs: + ti = time.time() + self.logger.info("Filling provided names") - for pkgname in context.to_update: - pacman.fill_provided_map(pkgname, pkgname, context.provided_map) - installed_to_ignore.add(pkgname) + if not context.installed_names: + context.installed_names = pacman.list_installed_names() - pdata = context.pkgs_data.get(pkgname) - if pdata and pdata['p']: - pacman.fill_provided_map('{}={}'.format(pkgname, pdata['v']), pkgname, context.provided_map) - for p in pdata['p']: - pacman.fill_provided_map(p, pkgname, context.provided_map) - split_provided = p.split('=') + installed_to_ignore = set() - if len(split_provided) > 1 and split_provided[0] != p: - pacman.fill_provided_map(split_provided[0], pkgname, context.provided_map) + for pkgname in pkgs: + pacman.fill_provided_map(pkgname, pkgname, context.provided_map) - if installed_to_ignore: # filling the provided names of the installed - installed_to_query = context.installed_names.difference(installed_to_ignore) + if fill_installed: + installed_to_ignore.add(pkgname) - if installed_to_query: - context.provided_map.update(pacman.map_provided(remote=False, pkgs=installed_to_query)) + pdata = context.pkgs_data.get(pkgname) + if pdata and pdata['p']: + pacman.fill_provided_map('{}={}'.format(pkgname, pdata['v']), pkgname, context.provided_map) - tf = time.time() - self.logger.info("Filling provided names took {0:.2f} seconds".format(tf - ti)) + ver_split = pdata['v'].split('-') + + if len(ver_split) > 1: + pacman.fill_provided_map('{}={}'.format(pkgname, '-'.join(ver_split[0:-1])), pkgname, context.provided_map) + + for p in pdata['p']: + pacman.fill_provided_map(p, pkgname, context.provided_map) + split_provided = p.split('=') + + if len(split_provided) > 1 and split_provided[0] != p: + pacman.fill_provided_map(split_provided[0], pkgname, context.provided_map) + + if installed_to_ignore: # filling the provided names of the installed + installed_to_query = context.installed_names.difference(installed_to_ignore) + + if installed_to_query: + context.provided_map.update(pacman.map_provided(remote=False, pkgs=installed_to_query)) + + tf = time.time() + self.logger.info("Filling provided names took {0:.2f} seconds".format(tf - ti)) def __fill_aur_index(self, context: UpdateRequirementsContext): if context.arch_config['aur']: @@ -381,7 +397,7 @@ def summarize(self, pkgs: List[ArchPackage], root_password: str, arch_config: di if aur_data: context.pkgs_data.update(aur_data) - self.__fill_provided_map(context) + self.__fill_provided_map(context=context, pkgs=context.to_update) if context.pkgs_data: self._fill_conflicts(context) @@ -394,6 +410,9 @@ def summarize(self, pkgs: List[ArchPackage], root_password: str, arch_config: di self.logger.error("Package '{}' not found".format(e.name)) return + if context.pkgs_data: + self._fill_dependency_breakage(context) + self.__update_context_based_on_to_remove(context) if context.to_update: @@ -558,3 +577,166 @@ def _add_to_remove(self, pkgs_to_sync: Set[str], names: Dict[str, Set[str]], con self._add_to_remove(pkgs_to_sync, {dep: {n} for dep in all_deps}, context, blacklist) else: self.logger.warning("Package '{}' could not be removed from the transaction context because its data was not loaded") + + def _fill_dependency_breakage(self, context: UpdateRequirementsContext): + if bool(context.arch_config['check_dependency_breakage']) and (context.to_update or context.to_install): + ti = time.time() + self.logger.info("Begin: checking dependency breakage") + + required_by = pacman.map_required_by(context.to_update.keys()) if context.to_update else {} + + if context.to_install: + required_by.update(pacman.map_required_by(context.to_install.keys(), remote=True)) + + reqs_not_in_transaction = set() + reqs_in_transaction = set() + + transaction_pkgs = {*context.to_update.keys(), *context.to_install.keys()} + + for reqs in required_by.values(): + for r in reqs: + if r in transaction_pkgs: + reqs_in_transaction.add(r) + elif r in context.installed_names: + reqs_not_in_transaction.add(r) + + if not reqs_not_in_transaction and not reqs_in_transaction: + return + + provided_versions = {} + + for p in context.provided_map: + pkg_split = p.split('=') + + if len(pkg_split) > 1: + versions = provided_versions.get(pkg_split[0]) + + if versions is None: + versions = set() + provided_versions[pkg_split[0]] = versions + + versions.add(pkg_split[1]) + + if not provided_versions: + return + + cannot_upgrade = set() + + for pkg, deps in pacman.map_required_dependencies(*reqs_not_in_transaction).items(): + self._add_dependency_breakage(pkgname=pkg, + pkgdeps=deps, + provided_versions=provided_versions, + cannot_upgrade=cannot_upgrade, + context=context) + + for pkg in reqs_in_transaction: + data = context.pkgs_data[pkg] + + if data and data['d']: + self._add_dependency_breakage(pkgname=pkg, + pkgdeps=data['d'], + provided_versions=provided_versions, + cannot_upgrade=cannot_upgrade, + context=context) + + if cannot_upgrade: + cannot_upgrade.update(self._add_dependents_as_cannot_upgrade(context=context, + names=cannot_upgrade, + pkgs_available={*context.to_update.values(), *context.to_install.values()})) + + for p in cannot_upgrade: + if p in context.to_update: + del context.to_update[p] + + if p in context.repo_to_update: + del context.repo_to_update[p] + + if p in context.aur_to_update: + del context.aur_to_update[p] + + if p in context.pkgs_data: + del context.pkgs_data[p] + + if p in context.to_install: + del context.to_install[p] + + if p in context.repo_to_install: + del context.repo_to_install[p] + + if p in context.aur_to_install: + del context.aur_to_install[p] + + tf = time.time() + self.logger.info("End: checking dependency breakage. Time: {0:.2f} seconds".format(tf - ti)) + + def _add_dependents_as_cannot_upgrade(self, context: UpdateRequirementsContext, names: Iterable[str], pkgs_available: Set[ArchPackage], already_removed: Optional[Set[str]] = None, iteration_level: int = 0) -> Set[str]: + removed = set() if already_removed is None else already_removed + removed.update(names) + + available = {p for p in pkgs_available if p.name not in removed} + to_remove = set() + + if available: + for pkg in available: + if pkg.name not in removed: + data = context.pkgs_data.get(pkg.name) + + if data and data['d']: + for dep in data['d']: + dep_providers = context.provided_map.get(dep) + + if dep_providers: + for p in dep_providers: + if p in names: + to_remove.add(pkg.name) + + if pkg.name not in context.cannot_upgrade: + reason = "{} {}".format(self.i18n['arch.info.depends on'].capitalize(), p) + context.cannot_upgrade[pkg.name] = UpgradeRequirement(pkg=pkg, + reason=reason, + sorting_priority=iteration_level - 1) + + break + + if to_remove: + removed.update(to_remove) + self._add_dependents_as_cannot_upgrade(context=context, names=to_remove, pkgs_available=available, + already_removed=to_remove, iteration_level=iteration_level-1) + + return to_remove + + def _add_dependency_breakage(self, pkgname: str, pkgdeps: Optional[Set[str]], provided_versions: Dict[str, Set[str]], cannot_upgrade: Set[str], context: UpdateRequirementsContext): + if pkgdeps: + for dep in pkgdeps: + dep_split = RE_DEP_OPERATORS.split(dep) + + if len(dep_split) > 1 and dep_split[1]: + real_providers = context.provided_map.get(dep_split[0]) + + if real_providers: + versions = provided_versions.get(dep_split[0]) + + if versions: + op = ''.join(RE_DEP_OPERATORS.findall(dep)) + + if op == '=': + op = '==' + + version_match = False + + for v in versions: + provided_version, required_version = LooseVersion(v), LooseVersion(dep_split[1]) + + if eval('provided_version {} required_version'.format(op)): + version_match = True + break + + if not version_match: + for pname in real_providers: + if pname not in cannot_upgrade: + provider = context.to_update.get(pname) + if provider: + cannot_upgrade.add(pname) + reason = self.i18n['arch.sync.dep_breakage.reason'].format(pkgname, dep) + context.cannot_upgrade[pname] = UpgradeRequirement(pkg=provider, + reason=reason) diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index ab219268..41ed76e0 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -619,6 +619,7 @@ def __init__(self, model: MultipleSelectComponent, parent: QWidget = None): help_icon = QLabel() help_icon.setPixmap(pixmap_help) help_icon.setToolTip(op.tooltip) + help_icon.setCursor(QCursor(Qt.PointingHandCursor)) widget.layout().addWidget(help_icon) self._layout.addWidget(widget, line, col) diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index 818d74b8..cec0122d 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -193,7 +193,7 @@ def __init__(self, manager: SoftwareManager, i18n: I18n, pkgs: List[PackageView] self.manager = manager self.i18n = i18n - def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None, required_size: bool = True) -> InputOption: + def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_tooltip: str = None, required_size: bool = True, display_sizes: bool = True) -> InputOption: if req.pkg.installed: icon_path = req.pkg.get_disk_icon_path() @@ -205,15 +205,19 @@ def _req_as_option(self, req: UpgradeRequirement, tooltip: bool = True, custom_t else: icon_path = req.pkg.get_type_icon_path() - size_str = '{}: {}'.format(self.i18n['size'].capitalize(), - '?' if req.extra_size is None else get_human_size_str(req.extra_size)) - if required_size and req.extra_size != req.required_size: - size_str += ' ( {}: {} )'.format(self.i18n['action.update.pkg.required_size'].capitalize(), - '?' if req.required_size is None else get_human_size_str(req.required_size)) + size_str = None + if display_sizes: + size_str = '{}: {}'.format(self.i18n['size'].capitalize(), + '?' if req.extra_size is None else get_human_size_str(req.extra_size)) + if required_size and req.extra_size != req.required_size: + size_str += ' ( {}: {} )'.format(self.i18n['action.update.pkg.required_size'].capitalize(), + '?' if req.required_size is None else get_human_size_str(req.required_size)) - label = '{}{} - {}'.format(req.pkg.name, - ' ( {} )'.format(req.pkg.latest_version) if req.pkg.latest_version else '', - size_str) + label = '{}{}'.format(req.pkg.name, + ' ( {} )'.format(req.pkg.latest_version) if req.pkg.latest_version else '') + + if size_str: + label += ' - {}'.format(size_str) return InputOption(label=label, value=None, @@ -232,11 +236,14 @@ def _sum_pkgs_size(self, reqs: List[UpgradeRequirement]) -> Tuple[int, int]: return required, extra - def _gen_cannot_update_form(self, reqs: List[UpgradeRequirement]) -> FormComponent: - opts = [self._req_as_option(r, False, r.reason) for r in reqs] + def _gen_cannot_upgrade_form(self, reqs: List[UpgradeRequirement]) -> FormComponent: + reqs.sort(key=UpgradeRequirement.sort_by_priority) + opts = [self._req_as_option(req=r, tooltip=False, custom_tooltip=r.reason, display_sizes=False) for r in reqs] comps = [MultipleSelectComponent(label='', options=opts, default_options=set(opts))] - return FormComponent(label=self.i18n['action.update.cannot_update_label'], components=comps) + return FormComponent(label='{} ( {}: {} )'.format(self.i18n['action.update.cannot_update_label'], + self.i18n['amount'].capitalize(), len(opts)), + components=comps) def _gen_to_install_form(self, reqs: List[UpgradeRequirement]) -> Tuple[FormComponent, Tuple[int, int]]: opts = [self._req_as_option(r, custom_tooltip=r.reason) for r in reqs] @@ -396,7 +403,7 @@ def run(self): comps, required_size, extra_size = [], 0, 0 if requirements.cannot_upgrade: - comps.append(self._gen_cannot_update_form(requirements.cannot_upgrade)) + comps.append(self._gen_cannot_upgrade_form(requirements.cannot_upgrade)) if requirements.to_install: req_form, reqs_size = self._gen_to_install_form(requirements.to_install) From b4b5dd3f6feab9ccdaf1b37bf417179de035d6a6 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 2 Sep 2020 09:36:07 -0300 Subject: [PATCH 70/99] [arch] fix -> upgrade: crashing during the dependency breakage checking if AUR packages dependents cannot be found --- bauh/gems/arch/pacman.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index fab93ed9..1d3491b5 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -918,9 +918,9 @@ def map_required_by(names: Iterable[str] = None, remote: bool = False) -> Dict[s output = run_cmd('pacman -{} {}'.format('Sii' if remote else 'Qi', ' '.join(names) if names else ''), print_error=False) if output: - res = {} latest_name, required = None, None - + res = {} + for l in output.split('\n'): if l: if l[0] != ' ': @@ -943,8 +943,11 @@ def map_required_by(names: Iterable[str] = None, remote: bool = False) -> Dict[s elif latest_name and required is not None: required.update(required.update((d for d in l.strip().split(' ') if d))) - return res + elif names: + return {n: set() for n in names} + else: + return {} def map_conflicts_with(names: Iterable[str], remote: bool) -> Dict[str, Set[str]]: From e4661e837a6f12c2cd1004a179958266cbe8d2f3 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 2 Sep 2020 10:18:59 -0300 Subject: [PATCH 71/99] [arch] fix -> AUR: not detecting some package updates --- bauh/gems/arch/mapper.py | 70 +--------- tests/gems/arch/test_arch_data_mapper.py | 167 +---------------------- 2 files changed, 7 insertions(+), 230 deletions(-) diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index e4311a2b..a4d01b91 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -1,25 +1,14 @@ import os -import re from datetime import datetime +from pkg_resources import parse_version + from bauh.api.abstract.model import PackageStatus from bauh.api.http import HttpClient from bauh.gems.arch.model import ArchPackage from bauh.view.util.translation import I18n URL_PKG_DOWNLOAD = 'https://aur.archlinux.org/{}' -RE_LETTERS = re.compile(r'\.([a-zA-Z]+)-\d+$') -RE_VERSION_SPLIT = re.compile(r'[a-zA-Z]+|\d+|[\.\-_@#]+') - -BAUH_PACKAGES = {'bauh', 'bauh-staging'} -RE_SFX = ('r', 're', 'release') -GA_SFX = ('ga', 'ge') -RC_SFX = ('rc',) -BETA_SFX = ('b', 'beta') -AL_SFX = ('alpha', 'alfa') -DEV_SFX = ('dev', 'devel', 'development') - -V_SUFFIX_MAP = {s: {'c': sfxs[0], 'p': idx} for idx, sfxs in enumerate([RE_SFX, GA_SFX, RC_SFX, BETA_SFX, AL_SFX, DEV_SFX]) for s in sfxs} class ArchDataMapper: @@ -52,62 +41,13 @@ def fill_api_data(self, pkg: ArchPackage, package: dict, fill_version: bool = Tr 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, check_suffix=pkg.name in BAUH_PACKAGES) + pkg.update = self.check_update(pkg.version, pkg.latest_version) @staticmethod - def check_update(version: str, latest_version: str, check_suffix: bool = False) -> bool: + def check_update(version: str, latest_version: str) -> bool: if version and latest_version: + return parse_version(version) < parse_version(latest_version) - if check_suffix: - current_sfx = RE_LETTERS.findall(version) - latest_sf = RE_LETTERS.findall(latest_version) - - if latest_sf and current_sfx: - current_sfx = current_sfx[0] - latest_sf = latest_sf[0] - - current_sfx_data = V_SUFFIX_MAP.get(current_sfx.lower()) - latest_sfx_data = V_SUFFIX_MAP.get(latest_sf.lower()) - - if current_sfx_data and latest_sfx_data: - nversion = version.split(current_sfx)[0] - nlatest = latest_version.split(latest_sf)[0] - - if nversion == nlatest: - if current_sfx_data['c'] != latest_sfx_data['c']: - return latest_sfx_data['p'] < current_sfx_data['p'] - else: - return ''.join(latest_version.split(latest_sf)) > ''.join(version.split(current_sfx)) - - return nlatest > nversion - - latest_split = RE_VERSION_SPLIT.findall(latest_version) - current_split = RE_VERSION_SPLIT.findall(version) - - for idx in range(len(latest_split)): - if idx < len(current_split): - latest_part = latest_split[idx] - current_part = current_split[idx] - - if latest_part != current_part: - - try: - dif = int(latest_part) - int(current_part) - - if dif > 0: - return True - elif dif < 0: - return False - else: - continue - - except ValueError: - if latest_part.isdigit(): - return True - elif current_part.isdigit(): - return False - else: - return latest_part > current_part return False def fill_package_build(self, pkg: ArchPackage): diff --git a/tests/gems/arch/test_arch_data_mapper.py b/tests/gems/arch/test_arch_data_mapper.py index fdc17d5a..ad300458 100644 --- a/tests/gems/arch/test_arch_data_mapper.py +++ b/tests/gems/arch/test_arch_data_mapper.py @@ -5,7 +5,7 @@ class ArchDataMapperTest(TestCase): - def test_check_update_no_suffix(self): + 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')) @@ -25,169 +25,6 @@ def test_check_update_no_suffix(self): 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_no_suffix_3_x_2_digits(self): - self.assertTrue(ArchDataMapper.check_update('1.0.0-1', '1.1-1')) - self.assertFalse(ArchDataMapper.check_update('1.2.0-1', '1.1-1')) - - def test_check_update_no_suffix_3_x_1_digits(self): - self.assertTrue(ArchDataMapper.check_update('1.0.0-1', '2-1')) - self.assertFalse(ArchDataMapper.check_update('2-1', '1.1-1')) - - def test_check_update_release(self): - # RE - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.R-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-2', '1.0.0.R-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.R-5', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.1.R-1', '1.0.1.R-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.2.R-1', '1.0.1.R-7', check_suffix=True)) - - self.assertTrue(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.R-2', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.RELEASE-2', '1.0.0.R-5', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RELEASE-5', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.1.R-1', '1.0.0.RE-1', check_suffix=True)) - - # GA - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Ge-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.Ge-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GE-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GE-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GE-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.GA-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.GA-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.GA-1', check_suffix=True)) - - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.GA-1', check_suffix=True)) - - # RCS - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.RC-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.RC-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.RC-1', check_suffix=True)) - - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.RC-1', check_suffix=True)) - - # BETA - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.B-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.B-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.B-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.BETA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.BETA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.BETA-2', check_suffix=True)) - - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.BETA-1', check_suffix=True)) - - # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.Alpha-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALPHA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALPHA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALPHA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.ALFA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.ALFA-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.ALFA-2', check_suffix=True)) - - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.Alfa-1', check_suffix=True)) - - # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DeV-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEVEL-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.R-1', '1.0.0.DEV-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.DEV-2', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.RELEASE-1', '1.0.0.DEV-2', check_suffix=True)) - - self.assertTrue(ArchDataMapper.check_update('1.0.0.R-5', '1.0.1.DEV-1', check_suffix=True)) - - def test_check_update_ga(self): - # GA - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.GE-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.GA-2', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-2', '1.0.1.GA-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-3', '1.0.6.GE-10', check_suffix=True)) - - # RC - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.RC-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.RC-1', check_suffix=True)) - - self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.RC-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.10.GA-10', '1.1.0.RC-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.1.0.GE-1', '1.0.1.RC-1', check_suffix=True)) - - # BETA - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.BETA-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.BETA-1', check_suffix=True)) - - # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.0.ALPHA-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.1.ALPHA-1', check_suffix=True)) - - # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.GA-1', '1.0.0.DEV-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.GE-1', '1.0.1.DEV-1', check_suffix=True)) - - def test_check_update_rc(self): - # RC - self.assertFalse(ArchDataMapper.check_update('1.0.0.RC-1', '1.0.0.RC-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.RC-1', '1.0.0.RC-2')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.RC-2', '1.0.1.RC-1')) - self.assertFalse(ArchDataMapper.check_update('1.1.0.RC-3', '1.0.6.RC-10')) - - # BETA - self.assertFalse(ArchDataMapper.check_update('1.0.0.RC-1', '1.0.0.BETA-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.RC-1', '1.0.1.BETA-1')) - - # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.RC-1', '1.0.0.ALPHA-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.RC-1', '1.0.1.ALPHA-1')) - - # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.RC-1', '1.0.0.DEV-1')) - self.assertTrue(ArchDataMapper.check_update('1.0.0.RC-1', '1.0.1.DEV-1')) - - def test_check_update_beta(self): - # BETA - self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.BETA-2', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.BETA-2', '1.0.1.B-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.1.0.B-3', '1.0.6.BETA-10', check_suffix=True)) - - # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.ALPHA-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.ALPHA-1', check_suffix=True)) - - # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.BETA-1', '1.0.0.DEV-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.B-1', '1.0.1.DEV-1', check_suffix=True)) - - def test_check_update_alpha(self): - # ALPHA - self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALPHA-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.0.ALFA-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.ALPHA-2', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-2', '1.0.1.ALFA-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.1.0.ALFA-3', '1.0.6.ALFA-10', check_suffix=True)) - - # DEV - self.assertFalse(ArchDataMapper.check_update('1.0.0.ALFA-1', '1.0.0.DEV-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.ALPHA-1', '1.0.1.DEV-1', check_suffix=True)) - - def test_check_update_dev(self): - self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEV-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.0.0.DEVEL-1', '1.0.0.DEVELOPMENT-1', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.DEV-1', '1.0.0.DEVEL-2', check_suffix=True)) - self.assertTrue(ArchDataMapper.check_update('1.0.0.DEVEL-2', '1.0.1.DEV-1', check_suffix=True)) - self.assertFalse(ArchDataMapper.check_update('1.1.0.DEV-3', '1.0.6.DEVELOPMENT-10', check_suffix=True)) - - def test_check_update_unknown_suffix(self): - self.assertTrue(ArchDataMapper.check_update('1.0.0.BALL-1', '1.0.0.TAR-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.TAR-1', '1.0.0.BALL-1')) - - def test_check_update_known_and_unknown_suffix(self): - self.assertTrue(ArchDataMapper.check_update('1.0.0.RE-1', '1.0.0.TAR-1')) - self.assertFalse(ArchDataMapper.check_update('1.0.0.TAR-1', '1.0.0.RE-1')) From 4d260df3f7ff5603d624eb7a54c04514ae7978cb Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 2 Sep 2020 10:22:05 -0300 Subject: [PATCH 72/99] [arch] improvement -> AUR: preventing update check errors --- bauh/gems/arch/mapper.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index a4d01b91..bc081ec0 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -1,6 +1,8 @@ import os +import traceback from datetime import datetime +from colorama import Fore from pkg_resources import parse_version from bauh.api.abstract.model import PackageStatus @@ -46,7 +48,12 @@ def fill_api_data(self, pkg: ArchPackage, package: dict, fill_version: bool = Tr @staticmethod def check_update(version: str, latest_version: str) -> bool: if version and latest_version: - return parse_version(version) < parse_version(latest_version) + try: + return parse_version(version) < parse_version(latest_version) + except: + print('{}Version: {}. Latest version: {}{}'.format(Fore.RED, version, latest_version, Fore.RESET)) + traceback.print_exc() + return False return False From ae27b6af3b32cd8f07ced8d59dacd9f649bf77ee Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 2 Sep 2020 10:35:55 -0300 Subject: [PATCH 73/99] [improvement] changing the way versions are compared in Arch and AppImage gems --- bauh/gems/appimage/controller.py | 4 +-- bauh/gems/arch/dependencies.py | 60 ++++++++++++++++++-------------- bauh/gems/arch/updates.py | 16 ++++++--- 3 files changed, 47 insertions(+), 33 deletions(-) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index a1da2aeb..686db47f 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -7,13 +7,13 @@ import subprocess import traceback from datetime import datetime -from distutils.version import LooseVersion from math import floor from pathlib import Path from threading import Lock from typing import Set, Type, List, Tuple, Optional from colorama import Fore +from pkg_resources import parse_version from bauh.api.abstract.context import ApplicationContext from bauh.api.abstract.controller import SoftwareManager, SearchResult, UpgradeRequirements, UpgradeRequirement, \ @@ -265,7 +265,7 @@ def read_installed(self, disk_loader: DiskCacheLoader, limit: int = -1, only_app app.update = False else: try: - app.update = LooseVersion(tup[2]) > LooseVersion(app.version) if tup[2] else False + app.update = parse_version(tup[2]) > parse_version(app.version) if tup[2] else False except: app.update = False traceback.print_exc() diff --git a/bauh/gems/arch/dependencies.py b/bauh/gems/arch/dependencies.py index 99f21c8a..9f4759e7 100644 --- a/bauh/gems/arch/dependencies.py +++ b/bauh/gems/arch/dependencies.py @@ -1,8 +1,10 @@ import re -from distutils.version import LooseVersion +import traceback from threading import Thread from typing import Set, List, Tuple, Dict, Iterable +from pkg_resources import parse_version + from bauh.api.abstract.handler import ProcessWatcher from bauh.gems.arch import pacman, message, sorting, confirmation from bauh.gems.arch.aur import AURClient @@ -203,23 +205,26 @@ def _fill_missing_dep(self, dep_name: str, dep_exp: str, aur_index: Iterable[str matched_providers = set() split_informed_dep = self.re_dep_operator.split(dep_exp) - version_informed = LooseVersion(split_informed_dep[2]) - exp_op = split_informed_dep[1] if split_informed_dep[1] != '=' else '==' + try: + version_informed = parse_version(split_informed_dep[2]) + exp_op = split_informed_dep[1] if split_informed_dep[1] != '=' else '==' - for p in providers: - provided = deps_data[p]['p'] + for p in providers: + provided = deps_data[p]['p'] - for provided_exp in provided: - split_dep = self.re_dep_operator.split(provided_exp) + for provided_exp in provided: + split_dep = self.re_dep_operator.split(provided_exp) - if len(split_dep) == 3 and split_dep[0] == dep_name: - provided_version = LooseVersion(split_dep[2]) + if len(split_dep) == 3 and split_dep[0] == dep_name: + provided_version = parse_version(split_dep[2]) - if eval('provided_version {} version_informed'.format(exp_op)): - matched_providers.add(p) - break + if eval('provided_version {} version_informed'.format(exp_op)): + matched_providers.add(p) + break - providers = matched_providers + providers = matched_providers + except: + traceback.print_exc() if providers: if len(providers) > 1: @@ -298,19 +303,22 @@ def map_missing_deps(self, pkgs_data: Dict[str, dict], provided_map: Dict[str, S if '-' not in version_informed: version_found = version_found.split('-')[0] - version_found = LooseVersion(version_found) - version_informed = LooseVersion(version_informed) - - op = dep_split[1] if dep_split[1] != '=' else '==' - if not eval('version_found {} version_informed'.format(op)): - self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, - missing_deps=missing_deps, - remote_provided_map=remote_provided_map, - remote_repo_map=remote_repo_map, - repo_deps=repo_missing, aur_deps=aur_missing, - watcher=watcher, - deps_data=deps_data, - automatch_providers=automatch_providers) + try: + version_found = parse_version(version_found) + version_informed = parse_version(version_informed) + + op = dep_split[1] if dep_split[1] != '=' else '==' + if not eval('version_found {} version_informed'.format(op)): + self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, + missing_deps=missing_deps, + remote_provided_map=remote_provided_map, + remote_repo_map=remote_repo_map, + repo_deps=repo_missing, aur_deps=aur_missing, + watcher=watcher, + deps_data=deps_data, + automatch_providers=automatch_providers) + except: + traceback.print_exc() else: self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, missing_deps=missing_deps, diff --git a/bauh/gems/arch/updates.py b/bauh/gems/arch/updates.py index be96550a..a0c6239d 100644 --- a/bauh/gems/arch/updates.py +++ b/bauh/gems/arch/updates.py @@ -1,9 +1,11 @@ import logging import time -from distutils.version import LooseVersion +import traceback from threading import Thread from typing import Dict, Set, List, Tuple, Iterable, Optional +from pkg_resources import parse_version + from bauh.api.abstract.controller import UpgradeRequirements, UpgradeRequirement from bauh.api.abstract.handler import ProcessWatcher from bauh.gems.arch import pacman, sorting @@ -725,11 +727,15 @@ def _add_dependency_breakage(self, pkgname: str, pkgdeps: Optional[Set[str]], pr version_match = False for v in versions: - provided_version, required_version = LooseVersion(v), LooseVersion(dep_split[1]) + try: + provided_version, required_version = parse_version(v), parse_version(dep_split[1]) - if eval('provided_version {} required_version'.format(op)): - version_match = True - break + if eval('provided_version {} required_version'.format(op)): + version_match = True + break + except: + self.logger.error("Error when comparing versions {} (provided) and {} (required)".format(v, dep_split[1])) + traceback.print_exc() if not version_match: for pname in real_providers: From a0d960aa4b351c28d8aded4774a70a29e4508076 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 2 Sep 2020 10:46:35 -0300 Subject: [PATCH 74/99] Updating CHANGELOG --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c8681f7..cd1cd927 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,14 +89,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - some conflict resolution scenarios when upgrading several packages - not handling conflicting files errors during the installation process + - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) - AUR - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) - importing PGP keys (Generic error). Now the key server is specified: `gpg --keyserver SERVER --recv-key KEYID` (the server address is retrieved from [bauh-files](https://github.com/vinifmor/bauh-files/blob/master/arch/gpgservers.txt)) - not installing the correct package built when several are generated (e.g: linux-xanmod-lts) - some packages dependencies cannot be downloaded due to the wrong download URL (missing the 'pkgbase' field to determine the proper url) - - not properly extracting srcinfo data when several pkgnames are declared (leads to wrong dependencies requirements) - - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) + - not properly extracting srcinfo data when several pkgnames are declared (leads to wrong dependencies requirements) + - not detecting some package updates + - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X From fb2c062226ca75f591397b2a68c14e58d48c2995 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 2 Sep 2020 11:10:50 -0300 Subject: [PATCH 75/99] [arch] fix -> AUR: not checking updates correctly when there are epoch numbers present --- bauh/gems/arch/mapper.py | 16 +++++++++++++++- tests/gems/arch/test_arch_data_mapper.py | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/bauh/gems/arch/mapper.py b/bauh/gems/arch/mapper.py index bc081ec0..00974511 100644 --- a/bauh/gems/arch/mapper.py +++ b/bauh/gems/arch/mapper.py @@ -49,7 +49,21 @@ def fill_api_data(self, pkg: ArchPackage, package: dict, fill_version: bool = Tr def check_update(version: str, latest_version: str) -> bool: if version and latest_version: try: - return parse_version(version) < parse_version(latest_version) + ver_epoch, latest_epoch = version.split(':'), latest_version.split(':') + + if len(ver_epoch) > 1 and len(latest_epoch) > 1: + parsed_ver_epoch, parsed_latest_epoch = parse_version(ver_epoch[0]), parse_version(latest_epoch[0]) + + if parsed_ver_epoch == parsed_latest_epoch: + return parse_version(''.join(ver_epoch[1:])) < parse_version(''.join(latest_epoch[1:])) + else: + return parsed_ver_epoch < parsed_latest_epoch + elif len(ver_epoch) > 1 and len(latest_epoch) == 1: + return False + elif len(ver_epoch) == 1 and len(latest_epoch) > 1: + return True + else: + return parse_version(version) < parse_version(latest_version) except: print('{}Version: {}. Latest version: {}{}'.format(Fore.RED, version, latest_version, Fore.RESET)) traceback.print_exc() diff --git a/tests/gems/arch/test_arch_data_mapper.py b/tests/gems/arch/test_arch_data_mapper.py index ad300458..0f704d88 100644 --- a/tests/gems/arch/test_arch_data_mapper.py +++ b/tests/gems/arch/test_arch_data_mapper.py @@ -28,3 +28,18 @@ def test_check_update(self): 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')) From 2b652532be9b76e35e8dffb5bd102b4581aef456 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 2 Sep 2020 12:11:26 -0300 Subject: [PATCH 76/99] [arch] improvement -> upgrade: allowing the user to bypass dependency breakage scenarios --- CHANGELOG.md | 3 +- bauh/gems/arch/controller.py | 51 +++++++++++++++++++++++++++--- bauh/gems/arch/pacman.py | 5 ++- bauh/gems/arch/resources/locale/ca | 4 +++ bauh/gems/arch/resources/locale/de | 4 +++ bauh/gems/arch/resources/locale/en | 4 +++ bauh/gems/arch/resources/locale/es | 4 +++ bauh/gems/arch/resources/locale/it | 4 +++ bauh/gems/arch/resources/locale/pt | 4 +++ bauh/gems/arch/resources/locale/ru | 4 +++ bauh/gems/arch/resources/locale/tr | 4 +++ 11 files changed, 84 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd1cd927..e22b5ed6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,12 +39,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- - checking specific version requirements and marking packages as "cannot upgrade" when these requirements are not met (e.g: package A depends on version 1.0 of B. If A and B were selected to upgrade, and B would be upgrade to 2.0, then B would be excluded from the transaction. This new checking behavior can be disabled through the property (**check_dependency_breakage**):

+ - allowing the user to bypass dependency breakage scenarios (a popup will be displayed) + - AUR - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) - new settings property **aur_build_dir** -> it allows to define a custom build dir. diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index b6511bc2..f60f6fc8 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -23,7 +23,7 @@ SuggestionPriority, CustomSoftwareAction from bauh.api.abstract.view import MessageType, FormComponent, InputOption, SingleSelectComponent, SelectViewType, \ ViewComponent, PanelComponent, MultipleSelectComponent, TextInputComponent, TextInputType, \ - FileChooserComponent + FileChooserComponent, TextComponent from bauh.api.constants import TEMP_DIR from bauh.commons import user, internet, system from bauh.commons.category import CategoriesDownloader @@ -59,6 +59,7 @@ RE_PRE_DOWNLOAD_BL_EXT = re.compile(r'.+\.(git|gpg)$') RE_PKGBUILD_PKGNAME = re.compile(r'pkgname\s*=.+') RE_CONFLICT_DETECTED = re.compile(r'\n::\s*(.+)\s+are in conflict\s*.') +RE_DEPENDENCY_BREAKAGE = re.compile(r'\n?::\s+installing\s+(.+\s\(.+\))\sbreaks\sdependency\s\'(.+)\'\srequired\sby\s(.+)\s*', flags=re.IGNORECASE) class TransactionContext: @@ -795,6 +796,21 @@ def _map_conflicting_file(self, output: str) -> List[MultipleSelectComponent]: return [MultipleSelectComponent(options=files, default_options={*files}, label='')] + def _map_dependencies_breakage(self, output: str) -> List[ViewComponent]: + errors = RE_DEPENDENCY_BREAKAGE.findall(output) + + if errors: + opts = [] + + for idx, err in enumerate(errors): + opts.append(InputOption(label=self.i18n['arch.upgrade.error.dep_breakage.item'].format(*err), value=idx, read_only=True)) + + return [MultipleSelectComponent(label='', + options=opts, + default_options={*opts})] + else: + return [TextComponent(output)] + def list_related(self, pkgs: Iterable[str], all_pkgs: Iterable[str], data: Dict[str, dict], related: Set[str], provided_map: Dict[str, Set[str]]) -> Set[str]: related.update(pkgs) @@ -832,7 +848,7 @@ def list_related(self, pkgs: Iterable[str], all_pkgs: Iterable[str], data: Dict[ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str]], handler: ProcessHandler, root_password: str, multithread_download: bool, pkgs_data: Dict[str, dict], overwrite_files: bool = False, status_handler: TransactionStatusHandler = None, sizes: Dict[str, int] = None, download: bool = True, - check_syncfirst: bool = True) -> bool: + check_syncfirst: bool = True, skip_dependency_checks: bool = False) -> bool: to_sync_first = None if check_syncfirst: @@ -883,8 +899,9 @@ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str] self.logger.info("Upgrading {} repository packages: {}".format(len(to_upgrade), ', '.join(to_upgrade))) success, upgrade_output = handler.handle_simple(pacman.upgrade_several(pkgnames=to_upgrade_remaining, root_password=root_password, - overwrite_conflicting_files=overwrite_files), - output_handler=output_handler.handle,) + overwrite_conflicting_files=overwrite_files, + skip_dependency_checks=skip_dependency_checks), + output_handler=output_handler.handle) handler.watcher.change_substatus('') if success: @@ -921,7 +938,31 @@ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str] check_syncfirst=False, pkgs_data=pkgs_data, to_remove=None, - sizes=sizes) + sizes=sizes, + skip_dependency_checks=skip_dependency_checks) + else: + output_handler.stop_working() + output_handler.join() + handler.watcher.print("Aborted by the user") + return False + elif ' breaks dependency ' in upgrade_output: + if not handler.watcher.request_confirmation(title=self.i18n['warning'].capitalize(), + body=self.i18n['arch.upgrade.error.dep_breakage'] + ':', + deny_label=self.i18n['arch.upgrade.error.dep_breakage.proceed'], + confirmation_label=self.i18n['arch.upgrade.error.dep_breakage.stop'], + components=self._map_dependencies_breakage(upgrade_output)): + return self._upgrade_repo_pkgs(to_upgrade=to_upgrade_remaining, + handler=handler, + root_password=root_password, + overwrite_files=overwrite_files, + status_handler=output_handler, + multithread_download=multithread_download, + download=False, + check_syncfirst=False, + pkgs_data=pkgs_data, + to_remove=None, + sizes=sizes, + skip_dependency_checks=True) else: output_handler.stop_working() output_handler.join() diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 1d3491b5..5c042d2d 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -707,12 +707,15 @@ def map_updates_data(pkgs: Iterable[str], files: bool = False) -> dict: return res -def upgrade_several(pkgnames: Iterable[str], root_password: str, overwrite_conflicting_files: bool = False) -> SimpleProcess: +def upgrade_several(pkgnames: Iterable[str], root_password: str, overwrite_conflicting_files: bool = False, skip_dependency_checks: bool = False) -> SimpleProcess: cmd = ['pacman', '-S', *pkgnames, '--noconfirm'] if overwrite_conflicting_files: cmd.append('--overwrite=*') + if skip_dependency_checks: + cmd.append('-d') + return SimpleProcess(cmd=cmd, root_password=root_password, error_phrases={'error: failed to prepare transaction', 'error: failed to commit transaction', 'error: target not found'}, diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 4350d047..a5521e78 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -229,6 +229,10 @@ arch.update_summary.to_update.conflicts_dep=Conflicts with the dependency {} of arch.update_summary.to_update.dep_conflicts=Dependency {} conflicts with {} arch.upgrade.caching_pkgs_data=Caching updates data arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to overwrite files of another installed packages +arch.upgrade.error.dep_breakage=Some dependency breakage issues have been detected +arch.upgrade.error.dep_breakage.item=The new version of {} breaks the dependency {} required by the installed version of {} +arch.upgrade.error.dep_breakage.proceed=Proceed anyway +arch.upgrade.error.dep_breakage.stop=Cancel upgrade arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 9e9ce49b..a0f8a813 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -229,6 +229,10 @@ arch.update_summary.to_update.conflicts_dep=Conflicts with the dependency {} of arch.update_summary.to_update.dep_conflicts=Dependency {} conflicts with {} arch.upgrade.caching_pkgs_data=Caching updates data arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to overwrite files of another installed packages +arch.upgrade.error.dep_breakage=Some dependency breakage issues have been detected +arch.upgrade.error.dep_breakage.item=The new version of {} breaks the dependency {} required by the installed version of {} +arch.upgrade.error.dep_breakage.proceed=Proceed anyway +arch.upgrade.error.dep_breakage.stop=Cancel upgrade arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 13bb8423..c58dc999 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -230,6 +230,10 @@ arch.update_summary.to_update.conflicts_dep=Conflicts with the dependency {} of arch.update_summary.to_update.dep_conflicts=Dependency {} conflicts with {} arch.upgrade.caching_pkgs_data=Caching updates data arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to overwrite files of another installed packages +arch.upgrade.error.dep_breakage=Some dependency breakage issues have been detected +arch.upgrade.error.dep_breakage.item=The new version of {} breaks the dependency {} required by the installed version of {} +arch.upgrade.error.dep_breakage.proceed=Proceed anyway +arch.upgrade.error.dep_breakage.stop=Cancel upgrade arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index bcf416a5..98c81add 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -229,6 +229,10 @@ arch.update_summary.to_update.conflicts_dep=Conflicta con la dependencia {} de { arch.update_summary.to_update.dep_conflicts=La dependencia {} conflicta con {} arch.upgrade.caching_pkgs_data=Caching upgrades data arch.upgrade.error.conflicting_files=Algunos de los paquetes que se están actualizando desean sobrescribir archivos de otros paquetes instalados +arch.upgrade.error.dep_breakage=Se han detectado algunos problemas de ruptura de dependencias +arch.upgrade.error.dep_breakage.item=La nueva versión de {} rompe la dependencia {} requerida por la versión instalada de {} +arch.upgrade.error.dep_breakage.proceed=Continuar de todos modos +arch.upgrade.error.dep_breakage.stop=Cancelar actualización arch.upgrade.conflicting_files.proceed=Permitir y continuar arch.upgrade.conflicting_files.stop=Cancelar la actualización arch.upgrade.fail=Falló la actualización del paquete {} diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index ddad0842..488db7aa 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -229,6 +229,10 @@ arch.update_summary.to_update.conflicts_dep=Conflicts with the dependency {} of arch.update_summary.to_update.dep_conflicts=Dependency {} conflicts with {} arch.upgrade.caching_pkgs_data=Caching updates data arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to overwrite files of another installed packages +arch.upgrade.error.dep_breakage=Some dependency breakage issues have been detected +arch.upgrade.error.dep_breakage.item=The new version of {} breaks the dependency {} required by the installed version of {} +arch.upgrade.error.dep_breakage.proceed=Proceed anyway +arch.upgrade.error.dep_breakage.stop=Cancel upgrade arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 21d92811..bf148f0c 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -229,6 +229,10 @@ arch.update_summary.to_update.conflicts_dep=Conflita com a dependência {} de {} arch.update_summary.to_update.dep_conflicts=A dependência {} conflita com {} arch.upgrade.caching_pkgs_data=Armazenando dados das atualizações arch.upgrade.error.conflicting_files=Alguns dos pacotes que estão sendo atualizados querem sobrepor arquivos de outros pacotes instalados +arch.upgrade.error.dep_breakage=Foram detectados os seguintes probemas de quebra de dependência +arch.upgrade.error.dep_breakage.item=A nova versão de {} quebra a dependência {} requerida pela versão instalada de {} +arch.upgrade.error.dep_breakage.proceed=Continuar mesmo assim +arch.upgrade.error.dep_breakage.stop=Cancelar atualização arch.upgrade.conflicting_files.proceed=Permitir e continuar arch.upgrade.conflicting_files.stop=Cancelar atualização arch.upgrade.fail=Atualização do pacote {} falhou diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 51fe3fa1..9a848975 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -229,6 +229,10 @@ arch.update_summary.to_update.conflicts_dep=Конфликты с зависим arch.update_summary.to_update.dep_conflicts=Зависимость {} конфликтует с {} arch.upgrade.caching_pkgs_data=Caching updates data arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to overwrite files of another installed packages +arch.upgrade.error.dep_breakage=Some dependency breakage issues have been detected +arch.upgrade.error.dep_breakage.item=The new version of {} breaks the dependency {} required by the installed version of {} +arch.upgrade.error.dep_breakage.proceed=Proceed anyway +arch.upgrade.error.dep_breakage.stop=Cancel upgrade arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail=Package {} upgrade failed diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 0ae6eb28..8e867b99 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -229,6 +229,10 @@ arch.update_summary.to_update.conflicts_dep={} / {} bağımlılığıyla ilgili arch.update_summary.to_update.dep_conflicts=Bağımlılık {} ile {} çakışıyor arch.upgrade.caching_pkgs_data=Yükseltme verileri önbelleğe alınıyor arch.upgrade.error.conflicting_files=Some of the packages being upgraded want to overwrite files of another installed packages +arch.upgrade.error.dep_breakage=Some dependency breakage issues have been detected +arch.upgrade.error.dep_breakage.item=The new version of {} breaks the dependency {} required by the installed version of {} +arch.upgrade.error.dep_breakage.proceed=Proceed anyway +arch.upgrade.error.dep_breakage.stop=Cancel upgrade arch.upgrade.conflicting_files.proceed=Allow and continue arch.upgrade.conflicting_files.stop=Cancel upgrading arch.upgrade.fail={} paketi yükseltilemedi From 129ede9c6c07d0eb51311cbfa1c38a055067b5b1 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 3 Sep 2020 18:06:43 -0300 Subject: [PATCH 77/99] [arch] fixes -> not displaying and uninstalling dependent packages during conflict resolutions | not retrieving all packages that would break if a given package is uninstalled --- CHANGELOG.md | 2 + bauh/api/abstract/controller.py | 2 +- bauh/gems/arch/controller.py | 130 ++++++++++++++--------------- bauh/gems/arch/resources/locale/ca | 12 +-- bauh/gems/arch/resources/locale/de | 12 +-- bauh/gems/arch/resources/locale/en | 12 +-- bauh/gems/arch/resources/locale/es | 12 +-- bauh/gems/arch/resources/locale/it | 12 +-- bauh/gems/arch/resources/locale/pt | 12 +-- bauh/gems/arch/resources/locale/ru | 12 +-- bauh/gems/arch/resources/locale/tr | 12 +-- bauh/view/qt/components.py | 2 + 12 files changed, 118 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e22b5ed6..90d440ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - some conflict resolution scenarios when upgrading several packages - not handling conflicting files errors during the installation process - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) + - not displaying and uninstalling dependent packages during conflict resolutions + - not retrieving all packages that would break if a given package is uninstalled - AUR - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index cf496949..67a61d29 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -72,7 +72,7 @@ class TransactionResult: The result of a given operation """ - def __init__(self, success: bool, installed: List[SoftwarePackage], removed: List[SoftwarePackage]): + def __init__(self, success: bool, installed: Optional[List[SoftwarePackage]], removed: Optional[List[SoftwarePackage]]): self.success = success self.installed = installed self.removed = removed diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index f60f6fc8..ba0a0d05 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -102,6 +102,7 @@ def __init__(self, name: str = None, base: str = None, maintainer: str = None, w self.new_pkg = new_pkg self.custom_pkgbuild_path = custom_pkgbuild_path self.pkgs_to_build = pkgs_to_build + self.previous_change_progress = change_progress @classmethod def gen_context_from(cls, pkg: ArchPackage, arch_config: dict, root_password: str, handler: ProcessHandler) -> "TransactionContext": @@ -169,6 +170,17 @@ def get_remote_repo_map(self) -> Dict[str, str]: return self.remote_repo_map + def disable_progress_if_changing(self): + if self.change_progress: + self.previous_change_progress = True + self.change_progress = False + + def restabilish_progress(self): + if self.previous_change_progress is not None: + self.change_progress = self.previous_change_progress + + self.previous_change_progress = self.change_progress + class ArchManager(SoftwareManager): @@ -1093,37 +1105,34 @@ def _uninstall_pkgs(self, pkgs: Iterable[str], root_password: str, handler: Proc return all_uninstalled - def _request_uninstall_confirmation(self, pkgs: Iterable[str], context: TransactionContext) -> bool: - reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=True) for p in pkgs] - reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=3) + def _request_uninstall_confirmation(self, to_uninstall: Iterable[str], required: Iterable[str], watcher: ProcessWatcher) -> bool: + reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=True) for p in required] + reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=1 if len(reqs) < 4 else 3) - msg = '

{}

{}

'.format(self.i18n['arch.uninstall.required_by'].format(bold(context.name), bold(str(len(reqs)))), - self.i18n['arch.uninstall.required_by.advice']) + msg = '

{}

{}

'.format(self.i18n['arch.uninstall.required_by'].format(bold(str(len(required))), ', '.join(bold(n)for n in to_uninstall)) + '.', + self.i18n['arch.uninstall.required_by.advice'] + '.') - if not context.watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(), - body=msg, - components=[reqs_select], - confirmation_label=self.i18n['proceed'].capitalize(), - deny_label=self.i18n['cancel'].capitalize(), - window_cancel=False): - context.watcher.print("Aborted") + if not watcher.request_confirmation(title=self.i18n['warning'].capitalize(), + body=msg, + components=[reqs_select], + confirmation_label=self.i18n['proceed'].capitalize(), + deny_label=self.i18n['cancel'].capitalize(), + window_cancel=False): + watcher.print("Aborted") return False return True - def _request_unncessary_uninstall_confirmation(self, pkgs: Iterable[str], context: TransactionContext) -> List[str]: - reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=False) for p in pkgs] + def _request_unncessary_uninstall_confirmation(self, uninstalled: Iterable[str], unnecessary: Iterable[str], watcher: ProcessWatcher) -> Optional[List[str]]: + reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=False) for p in unnecessary] reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=3) - msg = '

{}

{}:

'.format(self.i18n['arch.uninstall.unnecessary.l1'].format(bold(context.name)), - self.i18n['arch.uninstall.unnecessary.l2']) - - if not context.watcher.request_confirmation(title=self.i18n['confirmation'].capitalize(), - body=msg, - components=[reqs_select], - confirmation_label=self.i18n['arch.uninstall.unnecessary.proceed'].capitalize(), - deny_label=self.i18n['arch.uninstall.unnecessary.cancel'].capitalize(), - window_cancel=False): + if not watcher.request_confirmation(title=self.i18n['arch.uninstall.unnecessary.l1'].capitalize(), + body='

{}

'.format(self.i18n['arch.uninstall.unnecessary.l2']), + components=[reqs_select], + confirmation_label=self.i18n['arch.uninstall.unnecessary.proceed'].capitalize(), + deny_label=self.i18n['arch.uninstall.unnecessary.cancel'].capitalize(), + window_cancel=False): return return reqs_select.get_selected_values() @@ -1143,15 +1152,15 @@ def _request_all_unncessary_uninstall_confirmation(self, pkgs: Iterable[str], co return True - def _uninstall(self, context: TransactionContext, remove_unneeded: bool = False, disk_loader: DiskCacheLoader = None): + def _uninstall(self, context: TransactionContext, names: Set[str], remove_unneeded: bool = False, disk_loader: DiskCacheLoader = None): self._update_progress(context, 10) net_available = internet.is_available() if disk_loader else True - required_by = self.deps_analyser.map_all_required_by({context.name}, set()) + required_by = self.deps_analyser.map_all_required_by(names, set()) if required_by: - target_provided = pacman.map_provided(pkgs={context.name}).keys() + target_provided = pacman.map_provided(pkgs={*names, *required_by}).keys() if target_provided: required_by_deps = pacman.map_all_deps(required_by, only_installed=True) @@ -1163,7 +1172,7 @@ def _uninstall(self, context: TransactionContext, remove_unneeded: bool = False, target_required_by = 0 for dep in deps: dep_split = pacman.RE_DEP_OPERATORS.split(dep) - if dep_split[0] in target_provided: + if dep_split[0] in target_provided or dep_split[0] in required_by: dep_providers = all_provided.get(dep_split[0]) if dep_providers: @@ -1175,12 +1184,14 @@ def _uninstall(self, context: TransactionContext, remove_unneeded: bool = False, self._update_progress(context, 50) to_uninstall = set() - to_uninstall.add(context.name) + to_uninstall.update(names) if required_by: to_uninstall.update(required_by) - if not self._request_uninstall_confirmation(required_by, context): + if not self._request_uninstall_confirmation(to_uninstall=names, + required=required_by, + watcher=context.watcher): return False if remove_unneeded: @@ -1188,12 +1199,12 @@ def _uninstall(self, context: TransactionContext, remove_unneeded: bool = False, else: all_deps_map = None - if disk_loader and len(to_uninstall) > 1: # loading package instances in case the uninstall succeeds + if disk_loader and to_uninstall: # loading package instances in case the uninstall succeeds instances = self.read_installed(disk_loader=disk_loader, - names={n for n in to_uninstall if n != context.name}, + names={n for n in to_uninstall}, internet_available=net_available).installed - if len(instances) + 1 < len(to_uninstall): + if len(instances) != len(to_uninstall): self.logger.warning("Not all packages to be uninstalled could be read") else: instances = None @@ -1202,8 +1213,6 @@ def _uninstall(self, context: TransactionContext, remove_unneeded: bool = False, if uninstalled: if disk_loader: # loading package instances in case the uninstall succeeds - context.removed[context.pkg.name] = context.pkg - if instances: for p in instances: context.removed[p.name] = p @@ -1234,7 +1243,9 @@ def _uninstall(self, context: TransactionContext, remove_unneeded: bool = False, if no_longer_needed: self.logger.info("{} packages no longer needed found".format(len(no_longer_needed))) - unnecessary_to_uninstall = self._request_unncessary_uninstall_confirmation(no_longer_needed, context) + unnecessary_to_uninstall = self._request_unncessary_uninstall_confirmation(uninstalled=to_uninstall, + unnecessary=no_longer_needed, + watcher=context.watcher) if unnecessary_to_uninstall: unnecessary_to_uninstall_deps = pacman.list_unnecessary_deps(unnecessary_to_uninstall, all_provided) @@ -1289,15 +1300,14 @@ def uninstall(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatche return TransactionResult.fail() removed = {} - success = self._uninstall(TransactionContext(name=pkg.name, - pkg=pkg, - change_progress=True, + success = self._uninstall(TransactionContext(change_progress=True, arch_config=read_config(), watcher=watcher, root_password=root_password, handler=handler, removed=removed), remove_unneeded=True, + 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 []) @@ -1515,17 +1525,14 @@ def _request_conflict_resolution(self, pkg: str, conflicting_pkg: str, context: return False else: context.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflicting_pkg))) - conflict_context = context.clone_base() - conflict_context.change_progress = False - conflict_context.name = conflicting_pkg + context.disable_progress_if_changing() - if not self._uninstall(conflict_context): - context.watcher.show_message(title=self.i18n['error'], - body=self.i18n['arch.uninstalling.conflict.fail'].format(bold(conflicting_pkg)), - type_=MessageType.ERROR) - return False + if context.removed is None: + context.removed = {} - return True + res = self._uninstall(context=context, names={conflicting_pkg}, disk_loader=context.disk_loader) + context.restabilish_progress() + return res def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]]) -> Iterable[str]: """ @@ -2059,10 +2066,6 @@ def _install(self, context: TransactionContext) -> bool: file=bool(context.install_files), simulate=True), notify_watcher=False) - # for check_out in SimpleProcess(cmd=['pacman', '-U' if context.install_files else '-S', pkgpath], - # root_password=context.root_password, - # cwd=context.project_dir or '.').instance.stdout: - # check_install_output.append(check_out.decode()) self._update_progress(context, 70) @@ -2078,27 +2081,24 @@ def _install(self, context: TransactionContext) -> bool: else: # uninstall conflicts self._update_progress(context, 75) names_to_install = context.get_package_names() - to_uninstall = [conflict for conflict in conflicting_apps if conflict not in names_to_install] + to_uninstall = {conflict for conflict in conflicting_apps if conflict not in names_to_install} - self.logger.info("Preparing to uninstall conflicting packages: {}".format(to_uninstall)) + if to_uninstall: + self.logger.info("Preparing to uninstall conflicting packages: {}".format(to_uninstall)) + context.watcher.change_substatus(self.i18n['arch.uninstalling.conflict']) - pkgs_to_uninstall = self.read_installed(disk_loader=context.disk_loader, names=to_uninstall, internet_available=True).installed + if context.removed is None: + context.removed = {} - if not pkgs_to_uninstall: - self.logger.warning("Could not load packages to uninstall") - - for conflict in to_uninstall: - context.watcher.change_substatus(self.i18n['arch.uninstalling.conflict'].format(bold(conflict))) - - if not self._uninstall_pkgs(pkgs={conflict}, root_password=context.root_password, handler=context.handler): + context.disable_progress_if_changing() + if not self._uninstall(names=to_uninstall, context=context, remove_unneeded=False, disk_loader=context.disk_loader): context.watcher.show_message(title=self.i18n['error'], - body=self.i18n['arch.uninstalling.conflict.fail'].format(bold(conflict)), + body=self.i18n['arch.uninstalling.conflict.fail'].format(', '.join((bold(p) for p in to_uninstall))), type_=MessageType.ERROR) return False else: - uninstalled = [p for p in pkgs_to_uninstall if p.name == conflict] - if uninstalled: - context.removed[conflict] = uninstalled[0] + context.restabilish_progress() + else: self.logger.info("No conflict detected for '{}'".format(context.name)) diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index a5521e78..811413e5 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -213,15 +213,15 @@ arch.task.sync_sb.status=Actualitzen {} arch.uncompressing.package=S’està descomprimint el paquet arch.uninstall.clean_cached.error=No s'ha pogut eliminar {} versions antigues que es troba al disc arch.uninstall.clean_cached.substatus=Eliminació de versions antigues del disc -arch.uninstall.required_by=No es pot desinstal·lar {} perquè és necessari per al funcionament dels paquets següents. -arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed. +arch.uninstall.required_by=The {} packages listed below depend on {} to work properly +arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep -arch.uninstall.unnecessary.l1={} was successfully uninstalled. The packages below that it depended on are no longer needed. -arch.uninstall.unnecessary.l2=Check those you want to uninstall +arch.uninstall.unnecessary.l1=Packages successfully uninstalled! +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall arch.uninstall.unnecessary.proceed=Uninstall -arch.uninstalling.conflict=S’està suprimint el paquet conflictiu {} -arch.uninstalling.conflict.fail=No s’ha pogut desinstal·lar el paquet conflictiu {} +arch.uninstalling.conflict=Uninstalling conflicting packages +arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} arch.update.disabled.tooltip=This package can oly be upgrade through the action "Quick system upgrade" arch.update_summary.conflict_between=Conflict between {} and {} arch.update_summary.to_install.dep_conflict=Conflict between the dependencies {} and {} diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index a0f8a813..76d49c5f 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -213,15 +213,15 @@ arch.task.sync_sb.status=Updating {} arch.uncompressing.package=Paket entpacken arch.uninstall.clean_cached.error=It was not possible to remove old {} versions found on disk arch.uninstall.clean_cached.substatus=Removing old versions from disk -arch.uninstall.required_by={} konnte nicht deinstalliert werden, da es für die folgenden Pakete benötigt wird. -arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed. +arch.uninstall.required_by=The {} packages listed below depend on {} to work properly +arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep -arch.uninstall.unnecessary.l1={} was successfully uninstalled. The packages below that it depended on are no longer needed. -arch.uninstall.unnecessary.l2=Check those you want to uninstall +arch.uninstall.unnecessary.l1=Packages successfully uninstalled! +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall arch.uninstall.unnecessary.proceed=Uninstall -arch.uninstalling.conflict={} deinstallieren -arch.uninstalling.conflict.fail=Deinstallation von {} fehlgeschlagen +arch.uninstalling.conflict=Uninstalling conflicting packages +arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} arch.update.disabled.tooltip=This package can oly be upgrade through the action "Quick system upgrade" arch.update_summary.conflict_between=Conflict between {} and {} arch.update_summary.to_install.dep_conflict=Conflict between the dependencies {} and {} diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index c58dc999..c31b46d5 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -214,15 +214,15 @@ arch.task.sync_sb.status=Updating {} arch.uncompressing.package=Uncompressing the package arch.uninstall.clean_cached.error=It was not possible to remove old {} versions found on disk arch.uninstall.clean_cached.substatus=Removing old versions from disk -arch.uninstall.required_by={} cannot be uninstalled because it is required for the packages listed below to work. -arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed. +arch.uninstall.required_by=The {} packages listed below depend on {} to work properly +arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep -arch.uninstall.unnecessary.l1={} was successfully uninstalled. The packages below that it depended on are no longer needed. -arch.uninstall.unnecessary.l2=Check those you want to uninstall +arch.uninstall.unnecessary.l1=Packages successfully uninstalled! +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall arch.uninstall.unnecessary.proceed=Uninstall -arch.uninstalling.conflict=Uninstalling conflicting package {} -arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting package {} +arch.uninstalling.conflict=Uninstalling conflicting packages +arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} arch.update.disabled.tooltip=This package can oly be upgrade through the action "Quick system upgrade" arch.update_summary.conflict_between=Conflict between {} and {} arch.update_summary.to_install.dep_conflict=Conflict between the dependencies {} and {} diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 98c81add..64f5865e 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -213,15 +213,15 @@ arch.task.sync_sb.status=Actualizando {} arch.uncompressing.package=Descomprimindo el paquete arch.uninstall.clean_cached.error=No fue posible eliminar versiones antiguas de {} encontradas en disco arch.uninstall.clean_cached.substatus=Eliminando versiones antiguas del disco -arch.uninstall.required_by=No se puede desinstalar {} porque es necesario para que los paquetes enumerados abajo funcionen. -arch.uninstall.required_by.advice=Es necesario desinstalarlos también para continuar. +arch.uninstall.required_by=Los {} paquetes enumerados abajo dependen de {} para funcionar correctamente +arch.uninstall.required_by.advice=Es necesario desinstalarlos también para continuar arch.uninstall.unnecessary.all=Los {} seguintes paquetes serán desinstalados arch.uninstall.unnecessary.cancel=Mantener -arch.uninstall.unnecessary.l1={} se desinstaló correctamente. Los paquetes abajo de los que dependía ya no son necesarios -arch.uninstall.unnecessary.l2=Seleccione aquellos que desea desinstalar +arch.uninstall.unnecessary.l1=¡Paquetes desinstalados con éxito! +arch.uninstall.unnecessary.l2=Parece que los paquetes abajo ya no son necesarios. Seleccione los que desea desinstalar arch.uninstall.unnecessary.proceed=Desinstalar -arch.uninstalling.conflict=Eliminando el paquete conflictivo {} -arch.uninstalling.conflict.fail=No fue posible desinstalar el paquete conflictivo {} +arch.uninstalling.conflict=Eliminando paquetes conflictivos +arch.uninstalling.conflict.fail=No fue posible desinstalar los paquetes conflictivos: {} arch.update.disabled.tooltip=Solo es posible actualizar este paquete a través de la acción "Actualización rápida de sistema" arch.update_summary.conflict_between=Conflicto entre {} y {} arch.update_summary.to_install.dep_conflict=Conflicto entre las dependencias {} y {} diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 488db7aa..3011a2a1 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -213,15 +213,15 @@ arch.task.sync_sb.status=Aggiornando {} arch.uncompressing.package=Non comprimere il pacchetto arch.uninstall.clean_cached.error=Non è stato possibile rimuovere le vecchie {} versioni trovate sul disco arch.uninstall.clean_cached.substatus=Rimozione di versioni precedenti dal disco -arch.uninstall.required_by={} non può essere disinstallato perché è necessario che i seguenti pacchetti funzionino. -arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed. +arch.uninstall.required_by=The {} packages listed below depend on {} to work properly +arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep -arch.uninstall.unnecessary.l1={} was successfully uninstalled. The packages below that it depended on are no longer needed. -arch.uninstall.unnecessary.l2=Check those you want to uninstall: +arch.uninstall.unnecessary.l1=Packages successfully uninstalled! +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall arch.uninstall.unnecessary.proceed=Uninstall -arch.uninstalling.conflict=Disinstallazione del pacchetto in conflitto {} -arch.uninstalling.conflict.fail=Non è stato possibile disinstallare il pacchetto in conflitto {} +arch.uninstalling.conflict=Uninstalling conflicting packages +arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} arch.update.disabled.tooltip=This package can oly be upgrade through the action "Quick system upgrade" arch.update_summary.conflict_between=Conflict between {} and {} arch.update_summary.to_install.dep_conflict=Conflict between the dependencies {} and {} diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index bf148f0c..77706226 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -213,15 +213,15 @@ arch.task.sync_sb.status=Atualizando {} arch.uncompressing.package=Descompactando o pacote arch.uninstall.clean_cached.error=Não foi possível remover versões antigas de {} encontradas em disco arch.uninstall.clean_cached.substatus=Removendo versões antigas do disco -arch.uninstall.required_by={} não pode ser desinstalado porque ele é necessário para o funcionamento dos pacotes listados abaixo. -arch.uninstall.required_by.advice=Para prosseguir será necessário desinstá-los também. +arch.uninstall.required_by=Os {} pacotes listados abaixo dependem de {} para funcionar corretamente +arch.uninstall.required_by.advice=Para prosseguir será necessário desinstá-los também arch.uninstall.unnecessary.all=Os seguintes {} pacotes serão desinstalados arch.uninstall.unnecessary.cancel=Manter -arch.uninstall.unnecessary.l1={} foi desinstalado com sucesso ! Os pacotes abaixo que ele dependia já não são mais necessários. -arch.uninstall.unnecessary.l2=Selecione os desejados para desinstalação +arch.uninstall.unnecessary.l1=Pacotes desinstalados com sucesso! +arch.uninstall.unnecessary.l2=Os pacotes abaixo parecem não ser mais necessários. Selecione os desejados para desinstalação arch.uninstall.unnecessary.proceed=Desinstalar -arch.uninstalling.conflict=Desinstalando o pacote conflitante {} -arch.uninstalling.conflict.fail=Não foi possível desinstalar o pacote conflitante {} +arch.uninstalling.conflict=Desinstalando pacotes conflitante +arch.uninstalling.conflict.fail=Não foi possível desinstalar os pacotes conflitantes: {} arch.update.disabled.tooltip=Este pacote só pode ser atualizado através da ação "Atualização rápida de sistema" arch.update_summary.conflict_between=Conflito entre {} e {} arch.update_summary.to_install.dep_conflict=Conflito entre as dependências {} e {} diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 9a848975..c43bc763 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -213,15 +213,15 @@ arch.task.sync_sb.status=Updating {} arch.uncompressing.package=Распаковка пакета arch.uninstall.clean_cached.error=It was not possible to remove old {} versions found on disk arch.uninstall.clean_cached.substatus=Removing old versions from disk -arch.uninstall.required_by={} не может быть удален, так как это необходимо для работы следующих пакетов -arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed. +arch.uninstall.required_by=The {} packages listed below depend on {} to work properly +arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep -arch.uninstall.unnecessary.l1={} was successfully uninstalled. The packages below that it depended on are no longer needed. -arch.uninstall.unnecessary.l2=Check those you want to uninstall +arch.uninstall.unnecessary.l1=Packages successfully uninstalled! +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall arch.uninstall.unnecessary.proceed=Uninstall -arch.uninstalling.conflict=Удаление конфликтующего пакета {} -arch.uninstalling.conflict.fail=Не удалось удалить конфликтующий пакет {} +arch.uninstalling.conflict=Uninstalling conflicting packages +arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} arch.update.disabled.tooltip=Этот пакет может быть обновлен только через действие "обновить систему" arch.update_summary.conflict_between=Конфликт между {} и {} arch.update_summary.to_install.dep_conflict=Конфликт между зависимостями {} и {} diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 8e867b99..baeab0f8 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -213,15 +213,15 @@ arch.task.sync_sb.status={} güncelleniyor arch.uncompressing.package=Paket arşivden çıkarılıyor arch.uninstall.clean_cached.error=Diskte bulunan eski {} sürümleri kaldırılamadı arch.uninstall.clean_cached.substatus=Eski sürümler diskten kaldırılıyor -arch.uninstall.required_by={} kaldırılamıyor çünkü aşağıda listelenen paketlerin çalışması için gerekli. -arch.uninstall.required_by.advice=Devam etmek için bunları da kaldırmak gerekir. +arch.uninstall.required_by=The {} packages listed below depend on {} to work properly +arch.uninstall.required_by.advice=Devam etmek için bunları da kaldırmak gerekir arch.uninstall.unnecessary.all=Aşağıdaki {} paketler kaldırılacak arch.uninstall.unnecessary.cancel=Tut -arch.uninstall.unnecessary.l1={} başarıyla kaldırıldı. Aşağıda bağlı olduğu paketlere artık gerek kalmadı. -arch.uninstall.unnecessary.l2=Kaldırmak istediklerinizi kontrol edin +arch.uninstall.unnecessary.l1=Packages successfully uninstalled! +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall arch.uninstall.unnecessary.proceed=Kaldır -arch.uninstalling.conflict=Çakışan paketin kaldırılması {} -arch.uninstalling.conflict.fail=Çakışan {} paketini kaldırmak mümkün olmadı +arch.uninstalling.conflict=Uninstalling conflicting packages +arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} arch.update.disabled.tooltip=Bu paket yalnızca "Hızlı sistem yükseltme" işlemi ile yükseltilebilir arch.update_summary.conflict_between={} Ve {} arasında çakışma arch.update_summary.to_install.dep_conflict={} ve {} bağımlılıkları arasındaki çakışma diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 41ed76e0..e247b12a 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -302,6 +302,8 @@ def __init__(self, model: InputOption, model_parent: MultipleSelectComponent, ca if model.read_only: self.setAttribute(Qt.WA_TransparentForMouseEvents) self.setFocusPolicy(Qt.NoFocus) + else: + self.setCursor(QCursor(Qt.PointingHandCursor)) def _set_checked(self, state): checked = state == 2 From faa8cc4e0d2a45323f85f65f98b34c96bddfdf35 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sat, 5 Sep 2020 23:29:55 -0300 Subject: [PATCH 78/99] [arch] fix -> displaying wrong progress during the upgrade process when there are packages to install and upgrade --- CHANGELOG.md | 1 + bauh/gems/arch/controller.py | 33 +++++++++++++++++++++++++-------- bauh/gems/arch/output.py | 33 ++++++++++++++++++--------------- 3 files changed, 44 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90d440ba..562f0454 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) - not displaying and uninstalling dependent packages during conflict resolutions - not retrieving all packages that would break if a given package is uninstalled + - displaying wrong progress during the upgrade process when there are packages to install and upgrade - AUR - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index ba0a0d05..c3309f43 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -861,10 +861,12 @@ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str] multithread_download: bool, pkgs_data: Dict[str, dict], overwrite_files: bool = False, status_handler: TransactionStatusHandler = None, sizes: Dict[str, int] = None, download: bool = True, check_syncfirst: bool = True, skip_dependency_checks: bool = False) -> bool: + self.logger.info("Total packages to upgrade: {}".format(len(to_upgrade))) to_sync_first = None if check_syncfirst: to_sync_first = [p for p in pacman.get_packages_to_sync_first() if p.endswith('-keyring') and p in to_upgrade] + self.logger.info("Packages detected to upgrade firstly: {}".format(len(to_sync_first))) if to_sync_first: self.logger.info("Upgrading keyrings marked as 'SyncFirst'") @@ -880,9 +882,10 @@ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str] return False to_upgrade_remaining = [p for p in to_upgrade if p not in to_sync_first] if to_sync_first else to_upgrade + self.logger.info("Packages remaining to upgrade: {}".format(len(to_upgrade_remaining))) # pre-downloading all packages before removing any - if download: + if download and to_upgrade_remaining: try: downloaded = self._download_packages(pkgnames=to_upgrade_remaining, handler=handler, @@ -901,14 +904,17 @@ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str] if to_remove and not self._remove_transaction_packages(to_remove, handler, root_password): return False + if not to_upgrade_remaining: + return True + try: if status_handler: output_handler = status_handler else: - output_handler = TransactionStatusHandler(handler.watcher, self.i18n, len(to_upgrade), self.logger, downloading=len(to_upgrade_remaining)) + output_handler = TransactionStatusHandler(handler.watcher, self.i18n, {*to_upgrade_remaining}, self.logger, downloading=len(to_upgrade_remaining)) output_handler.start() - self.logger.info("Upgrading {} repository packages: {}".format(len(to_upgrade), ', '.join(to_upgrade))) + self.logger.info("Upgrading {} repository packages: {}".format(len(to_upgrade_remaining), ', '.join(to_upgrade_remaining))) success, upgrade_output = handler.handle_simple(pacman.upgrade_several(pkgnames=to_upgrade_remaining, root_password=root_password, overwrite_conflicting_files=overwrite_files, @@ -995,7 +1001,7 @@ def _upgrade_repo_pkgs(self, to_upgrade: List[str], to_remove: Optional[Set[str] def _remove_transaction_packages(self, to_remove: Set[str], handler: ProcessHandler, root_password: str) -> bool: output_handler = TransactionStatusHandler(watcher=handler.watcher, i18n=self.i18n, - pkgs_to_sync=0, + names=set(), logger=self.logger, pkgs_to_remove=len(to_remove)) output_handler.start() @@ -1090,10 +1096,21 @@ def upgrade(self, requirements: UpgradeRequirements, root_password: str, watcher return True def _uninstall_pkgs(self, pkgs: Iterable[str], root_password: str, handler: ProcessHandler) -> bool: + status_handler = TransactionStatusHandler(watcher=handler.watcher, + i18n=self.i18n, + names={*pkgs}, + logger=self.logger, + pkgs_to_remove=len(pkgs)) + + status_handler.start() all_uninstalled, _ = handler.handle_simple(SimpleProcess(cmd=['pacman', '-R', *pkgs, '--noconfirm'], root_password=root_password, error_phrases={'error: failed to prepare transaction', - 'error: failed to commit transaction'})) + 'error: failed to commit transaction'}, + shell=True), + output_handler=status_handler.handle) + status_handler.stop_working() + status_handler.join() installed = pacman.list_installed_names() @@ -1584,7 +1601,7 @@ def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]] except ArchDownloadException: return False - status_handler = TransactionStatusHandler(watcher=context.watcher, i18n=self.i18n, pkgs_to_sync=len(repo_dep_names), + status_handler = TransactionStatusHandler(watcher=context.watcher, i18n=self.i18n, names=repo_dep_names, logger=self.logger, percentage=len(repo_deps) > 1, downloading=downloaded) status_handler.start() installed, _ = context.handler.handle_simple(pacman.install_as_process(pkgpaths=repo_dep_names, @@ -2041,7 +2058,7 @@ def _download_packages(self, pkgnames: List[str], handler: ProcessHandler, root_ root_password=root_password) else: self.logger.info("Downloading {} repository package(s)".format(len(pkgnames))) - output_handler = TransactionStatusHandler(handler.watcher, self.i18n, len(pkgnames), self.logger) + output_handler = TransactionStatusHandler(handler.watcher, self.i18n, pkgnames, self.logger) output_handler.start() try: success, _ = handler.handle_simple(pacman.download(root_password, *pkgnames), output_handler=output_handler.handle) @@ -2123,7 +2140,7 @@ def _install(self, context: TransactionContext) -> bool: return False if not context.dependency: - status_handler = TransactionStatusHandler(context.watcher, self.i18n, len(to_install), self.logger, + status_handler = TransactionStatusHandler(context.watcher, self.i18n, to_install, self.logger, percentage=len(to_install) > 1, downloading=downloaded) if not context.dependency else None status_handler.start() diff --git a/bauh/gems/arch/output.py b/bauh/gems/arch/output.py index ecaa7057..39ff0c71 100644 --- a/bauh/gems/arch/output.py +++ b/bauh/gems/arch/output.py @@ -1,6 +1,7 @@ import logging import time from threading import Thread +from typing import Set from bauh.api.abstract.handler import ProcessWatcher from bauh.commons.html import bold @@ -9,12 +10,13 @@ class TransactionStatusHandler(Thread): - def __init__(self, watcher: ProcessWatcher, i18n: I18n, pkgs_to_sync: int, logger: logging.Logger, + def __init__(self, watcher: ProcessWatcher, i18n: I18n, names: Set[str], logger: logging.Logger, percentage: bool = True, downloading: int = 0, pkgs_to_remove: int = 0): super(TransactionStatusHandler, self).__init__(daemon=True) self.watcher = watcher self.i18n = i18n - self.pkgs_to_sync = pkgs_to_sync + self.pkgs_to_sync = len(names) + self.names = names self.pkgs_to_remove = pkgs_to_remove self.downloading = downloading self.upgrading = 0 @@ -43,24 +45,25 @@ def get_performed(self) -> int: def _handle(self, output: str) -> bool: if output: - if output.startswith('removing'): + output_split = output.split(' ') + + if output_split[0].lower() == 'removing' and (not self.names or output_split[1].split('.')[0] in self.names): if self.pkgs_to_remove > 0: self.removing += 1 - self.watcher.change_substatus( - '[{}/{}] {} {}'.format(self.removing, self.pkgs_to_remove, - self.i18n['uninstalling'].capitalize(), output.split(' ')[1].strip())) + self.watcher.change_substatus('[{}/{}] {} {}'.format(self.removing, self.pkgs_to_remove, + self.i18n['uninstalling'].capitalize(), output.split(' ')[1].strip())) else: - self.watcher.change_substatus('{} {}'.format(self.i18n['uninstalling'].capitalize(), output.split(' ')[1].strip())) + self.watcher.change_substatus('{} {}'.format(self.i18n['uninstalling'].capitalize(), output_split[1].strip())) - elif output.startswith('downloading'): + elif output_split[0].lower() == 'downloading' and (not self.names or (n for n in self.names if n in output_split[1])): if self.downloading < self.pkgs_to_sync: perc = self.gen_percentage() self.downloading += 1 self.watcher.change_substatus('{}[{}/{}] {} {} {}'.format(perc, self.downloading, self.pkgs_to_sync, bold('[pacman]'), - self.i18n['downloading'].capitalize(), output.split(' ')[1].strip())) - elif output.startswith('upgrading'): + self.i18n['downloading'].capitalize(), output_split[1].strip())) + elif output_split[0].lower() == 'upgrading' and (not self.names or output_split[1].split('.')[0] in self.names): if self.get_performed() < self.pkgs_to_sync: perc = self.gen_percentage() self.upgrading += 1 @@ -68,9 +71,9 @@ def _handle(self, output: str) -> bool: performed = self.upgrading + self.installing if performed <= self.pkgs_to_sync: - self.watcher.change_substatus('{}[{}/{}] {} {}'.format(perc, self.upgrading, self.pkgs_to_sync, - self.i18n['manage_window.status.upgrading'].capitalize(), output.split(' ')[1].strip())) - elif output.startswith('installing'): + self.watcher.change_substatus('{}[{}/{}] {} {}'.format(perc, performed, self.pkgs_to_sync, + self.i18n['manage_window.status.upgrading'].capitalize(), output_split[1].strip())) + elif output_split[0].lower() == 'installing' and (not self.names or output_split[1].split('.')[0] in self.names): if self.get_performed() < self.pkgs_to_sync: perc = self.gen_percentage() self.installing += 1 @@ -78,9 +81,9 @@ def _handle(self, output: str) -> bool: performed = self.upgrading + self.installing if performed <= self.pkgs_to_sync: - self.watcher.change_substatus('{}[{}/{}] {} {}'.format(perc, self.installing, self.pkgs_to_sync, + self.watcher.change_substatus('{}[{}/{}] {} {}'.format(perc, performed, self.pkgs_to_sync, self.i18n['manage_window.status.installing'].capitalize(), - output.split(' ')[1].strip())) + output_split[1].strip())) else: substatus_found = False lower_output = output.lower() From bf6361a6d1b95235b4209fe31fa854da96d86fc5 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sat, 5 Sep 2020 23:52:26 -0300 Subject: [PATCH 79/99] [arch] improvement -> 'keep' as the default button for 'no longer needed deps' dialog | [ui] adding missing 'pointing hand' cursors --- bauh/gems/arch/controller.py | 12 +++++------- bauh/gems/arch/resources/locale/ca | 2 +- bauh/gems/arch/resources/locale/de | 2 +- bauh/gems/arch/resources/locale/en | 2 +- bauh/gems/arch/resources/locale/es | 2 +- bauh/gems/arch/resources/locale/it | 2 +- bauh/gems/arch/resources/locale/pt | 2 +- bauh/gems/arch/resources/locale/ru | 2 +- bauh/gems/arch/resources/locale/tr | 2 +- bauh/view/qt/confirmation.py | 3 +++ bauh/view/qt/dialog.py | 6 ++++-- 11 files changed, 20 insertions(+), 17 deletions(-) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index c3309f43..93f7876b 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1142,17 +1142,15 @@ def _request_uninstall_confirmation(self, to_uninstall: Iterable[str], required: def _request_unncessary_uninstall_confirmation(self, uninstalled: Iterable[str], unnecessary: Iterable[str], watcher: ProcessWatcher) -> Optional[List[str]]: reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=False) for p in unnecessary] - reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=3) + reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=3 if len(reqs) > 9 else 1) if not watcher.request_confirmation(title=self.i18n['arch.uninstall.unnecessary.l1'].capitalize(), - body='

{}

'.format(self.i18n['arch.uninstall.unnecessary.l2']), + body='

{}

'.format(self.i18n['arch.uninstall.unnecessary.l2'] + ':'), components=[reqs_select], - confirmation_label=self.i18n['arch.uninstall.unnecessary.proceed'].capitalize(), - deny_label=self.i18n['arch.uninstall.unnecessary.cancel'].capitalize(), + deny_label=self.i18n['arch.uninstall.unnecessary.proceed'].capitalize(), + confirmation_label=self.i18n['arch.uninstall.unnecessary.cancel'].capitalize(), window_cancel=False): - return - - return reqs_select.get_selected_values() + return reqs_select.get_selected_values() def _request_all_unncessary_uninstall_confirmation(self, pkgs: Iterable[str], context: TransactionContext): reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=True) for p in pkgs] diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 811413e5..042f40cf 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -218,7 +218,7 @@ arch.uninstall.required_by.advice=It is necessary to uninstall them as well to p arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep arch.uninstall.unnecessary.l1=Packages successfully uninstalled! -arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary arch.uninstall.unnecessary.proceed=Uninstall arch.uninstalling.conflict=Uninstalling conflicting packages arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 76d49c5f..5a05b4c7 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -218,7 +218,7 @@ arch.uninstall.required_by.advice=It is necessary to uninstall them as well to p arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep arch.uninstall.unnecessary.l1=Packages successfully uninstalled! -arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary arch.uninstall.unnecessary.proceed=Uninstall arch.uninstalling.conflict=Uninstalling conflicting packages arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index c31b46d5..9855d84b 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -219,7 +219,7 @@ arch.uninstall.required_by.advice=It is necessary to uninstall them as well to p arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep arch.uninstall.unnecessary.l1=Packages successfully uninstalled! -arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary arch.uninstall.unnecessary.proceed=Uninstall arch.uninstalling.conflict=Uninstalling conflicting packages arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 64f5865e..4a16fafa 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -218,7 +218,7 @@ arch.uninstall.required_by.advice=Es necesario desinstalarlos también para cont arch.uninstall.unnecessary.all=Los {} seguintes paquetes serán desinstalados arch.uninstall.unnecessary.cancel=Mantener arch.uninstall.unnecessary.l1=¡Paquetes desinstalados con éxito! -arch.uninstall.unnecessary.l2=Parece que los paquetes abajo ya no son necesarios. Seleccione los que desea desinstalar +arch.uninstall.unnecessary.l2=Parece que los paquetes abajo ya no son necesarios arch.uninstall.unnecessary.proceed=Desinstalar arch.uninstalling.conflict=Eliminando paquetes conflictivos arch.uninstalling.conflict.fail=No fue posible desinstalar los paquetes conflictivos: {} diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 3011a2a1..09477ed5 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -218,7 +218,7 @@ arch.uninstall.required_by.advice=It is necessary to uninstall them as well to p arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep arch.uninstall.unnecessary.l1=Packages successfully uninstalled! -arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary arch.uninstall.unnecessary.proceed=Uninstall arch.uninstalling.conflict=Uninstalling conflicting packages arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 77706226..5396b5a3 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -218,7 +218,7 @@ arch.uninstall.required_by.advice=Para prosseguir será necessário desinstá-lo arch.uninstall.unnecessary.all=Os seguintes {} pacotes serão desinstalados arch.uninstall.unnecessary.cancel=Manter arch.uninstall.unnecessary.l1=Pacotes desinstalados com sucesso! -arch.uninstall.unnecessary.l2=Os pacotes abaixo parecem não ser mais necessários. Selecione os desejados para desinstalação +arch.uninstall.unnecessary.l2=Os pacotes abaixo parecem não ser mais necessários arch.uninstall.unnecessary.proceed=Desinstalar arch.uninstalling.conflict=Desinstalando pacotes conflitante arch.uninstalling.conflict.fail=Não foi possível desinstalar os pacotes conflitantes: {} diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index c43bc763..b964821c 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -218,7 +218,7 @@ arch.uninstall.required_by.advice=It is necessary to uninstall them as well to p arch.uninstall.unnecessary.all=The following {} packages will be uninstalled arch.uninstall.unnecessary.cancel=Keep arch.uninstall.unnecessary.l1=Packages successfully uninstalled! -arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary arch.uninstall.unnecessary.proceed=Uninstall arch.uninstalling.conflict=Uninstalling conflicting packages arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index baeab0f8..1c2550c0 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -218,7 +218,7 @@ arch.uninstall.required_by.advice=Devam etmek için bunları da kaldırmak gerek arch.uninstall.unnecessary.all=Aşağıdaki {} paketler kaldırılacak arch.uninstall.unnecessary.cancel=Tut arch.uninstall.unnecessary.l1=Packages successfully uninstalled! -arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary. Select those you want to uninstall +arch.uninstall.unnecessary.l2=The packages below seem to be no longer necessary arch.uninstall.unnecessary.proceed=Kaldır arch.uninstalling.conflict=Uninstalling conflicting packages arch.uninstalling.conflict.fail=It was not possible to uninstall the conflicting packages: {} diff --git a/bauh/view/qt/confirmation.py b/bauh/view/qt/confirmation.py index 85bc8410..d7ccc4d4 100644 --- a/bauh/view/qt/confirmation.py +++ b/bauh/view/qt/confirmation.py @@ -1,6 +1,7 @@ from typing import List from PyQt5.QtCore import QSize, Qt +from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import QMessageBox, QVBoxLayout, QLabel, QWidget, QScrollArea, QFrame from bauh.api.abstract.view import ViewComponent @@ -25,11 +26,13 @@ def __init__(self, title: str, body: str, i18n: I18n, screen_size: QSize, compo self.bt_yes = None if confirmation_button: self.bt_yes = self.addButton(i18n['popup.button.yes'] if not confirmation_label else confirmation_label.capitalize(), QMessageBox.YesRole) + self.bt_yes.setCursor(QCursor(Qt.PointingHandCursor)) self.bt_yes.setStyleSheet(css.OK_BUTTON) self.setDefaultButton(self.bt_yes) if deny_button: self.bt_no = self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole) + self.bt_no.setCursor(QCursor(Qt.PointingHandCursor)) if not confirmation_button: self.bt_no.setStyleSheet(css.OK_BUTTON) diff --git a/bauh/view/qt/dialog.py b/bauh/view/qt/dialog.py index 62cad49a..b6ae292f 100644 --- a/bauh/view/qt/dialog.py +++ b/bauh/view/qt/dialog.py @@ -1,7 +1,7 @@ from typing import List from PyQt5.QtCore import Qt -from PyQt5.QtGui import QIcon +from PyQt5.QtGui import QIcon, QCursor from PyQt5.QtWidgets import QMessageBox, QLabel, QWidget, QHBoxLayout from bauh.api.abstract.view import MessageType @@ -47,9 +47,11 @@ def ask_confirmation(title: str, body: str, i18n: I18n, icon: QIcon = QIcon(reso bt_yes = diag.addButton(i18n['popup.button.yes'], QMessageBox.YesRole) bt_yes.setStyleSheet(css.OK_BUTTON) + bt_yes.setCursor(QCursor(Qt.PointingHandCursor)) diag.setDefaultButton(bt_yes) - diag.addButton(i18n['popup.button.no'], QMessageBox.NoRole) + bt_no = diag.addButton(i18n['popup.button.no'], QMessageBox.NoRole) + bt_no.setCursor(QCursor(Qt.PointingHandCursor)) if icon: diag.setWindowIcon(icon) From 26fa25cebb4b5e7c9134a2fbe7277ec17167fc32 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 6 Sep 2020 01:10:13 -0300 Subject: [PATCH 80/99] [snap] feature -> allowing to select an available channel during the installation process --- CHANGELOG.md | 9 ++++ README.md | 4 ++ bauh/api/abstract/controller.py | 2 +- bauh/gems/appimage/controller.py | 2 +- bauh/gems/arch/controller.py | 2 +- bauh/gems/flatpak/controller.py | 4 +- bauh/gems/snap/__init__.py | 3 +- bauh/gems/snap/config.py | 7 +++ bauh/gems/snap/controller.py | 80 ++++++++++++++++++++++++++++-- bauh/gems/snap/resources/locale/ca | 3 ++ bauh/gems/snap/resources/locale/de | 3 ++ bauh/gems/snap/resources/locale/en | 3 ++ bauh/gems/snap/resources/locale/es | 3 ++ bauh/gems/snap/resources/locale/it | 3 ++ bauh/gems/snap/resources/locale/pt | 3 ++ bauh/gems/snap/resources/locale/ru | 3 ++ bauh/gems/snap/resources/locale/tr | 3 ++ bauh/gems/snap/snap.py | 7 ++- bauh/gems/web/controller.py | 4 +- bauh/view/qt/components.py | 1 + 20 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 bauh/gems/snap/config.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 562f0454..354bff3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

+- Snap + - new settings property **install_channel**: it allows to select an available channel during the application installation. Default: false. +

+ +

+ +

+ +

### Improvements - AppImage diff --git a/README.md b/README.md index cfa0039c..17fbbd61 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,10 @@ installation_level: null # defines a default installation level: user or system. - Supported actions: search, install, uninstall, launch, downgrade - Custom actions: refresh +- The configuration file is located at **~/.config/bauh/snap.yml** and it allows the following customizations: +``` +install_channel: false # it allows to select an available channel during the application installation. Default: false +``` - Required dependencies: - Any distro: **snapd** ( it must be enabled after its installation. Details at https://snapcraft.io/docs/installing-snapd ) diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index 67a61d29..deef917b 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -350,7 +350,7 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: """ pass - def save_settings(self, component: ViewComponent) -> Tuple[bool, List[str]]: + def save_settings(self, component: ViewComponent) -> Tuple[bool, Optional[List[str]]]: """ :return: a tuple with a bool informing if the settings were saved and a list of error messages """ diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 686db47f..066c9708 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -723,7 +723,7 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: return PanelComponent([FormComponent(updater_opts, self.i18n['appimage.config.db_updates'])]) - def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[str]]]: config = read_config() panel = component.components[0] diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 93f7876b..2dbc0573 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -2682,7 +2682,7 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: return PanelComponent([FormComponent(fields, spaces=False)]) - def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[str]]]: config = read_config() form_install = component.components[0] diff --git a/bauh/gems/flatpak/controller.py b/bauh/gems/flatpak/controller.py index 81b86365..fc03d487 100644 --- a/bauh/gems/flatpak/controller.py +++ b/bauh/gems/flatpak/controller.py @@ -5,7 +5,7 @@ from math import floor from pathlib import Path from threading import Thread -from typing import List, Set, Type, Tuple +from typing import List, Set, Type, Tuple, Optional from bauh.api.abstract.controller import SearchResult, SoftwareManager, ApplicationContext, UpgradeRequirements, \ UpgradeRequirement, TransactionResult @@ -570,7 +570,7 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: return PanelComponent([FormComponent(fields, self.i18n['installation'].capitalize())]) - def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[str]]]: config = read_config() config['installation_level'] = component.components[0].components[0].get_selected() diff --git a/bauh/gems/snap/__init__.py b/bauh/gems/snap/__init__.py index bfd47893..0a3521c2 100644 --- a/bauh/gems/snap/__init__.py +++ b/bauh/gems/snap/__init__.py @@ -1,10 +1,11 @@ import os -from bauh.api.constants import CACHE_PATH +from bauh.api.constants import CACHE_PATH, CONFIG_PATH from bauh.commons import resource ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) SNAP_CACHE_PATH = CACHE_PATH + '/snap' +CONFIG_FILE = '{}/snap.yml'.format(CONFIG_PATH) CATEGORIES_FILE_PATH = SNAP_CACHE_PATH + '/categories.txt' URL_CATEGORIES_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/categories.txt' SUGGESTIONS_FILE = 'https://raw.githubusercontent.com/vinifmor/bauh-files/master/snap/suggestions.txt' diff --git a/bauh/gems/snap/config.py b/bauh/gems/snap/config.py new file mode 100644 index 00000000..99d87637 --- /dev/null +++ b/bauh/gems/snap/config.py @@ -0,0 +1,7 @@ +from bauh.commons.config import read_config as read +from bauh.gems.snap import CONFIG_FILE + + +def read_config(update_file: bool = False) -> dict: + template = {'install_channel': False} + return read(CONFIG_FILE, template, update_file=update_file) diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index 0bc67433..b241e287 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -2,7 +2,7 @@ import time import traceback from threading import Thread -from typing import List, Set, Type, Optional +from typing import List, Set, Type, Optional, Tuple from bauh.api.abstract.controller import SoftwareManager, SearchResult, ApplicationContext, UpgradeRequirements, \ TransactionResult @@ -10,13 +10,17 @@ from bauh.api.abstract.handler import ProcessWatcher, TaskManager from bauh.api.abstract.model import SoftwarePackage, PackageHistory, PackageUpdate, PackageSuggestion, \ SuggestionPriority, CustomSoftwareAction, PackageStatus -from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption +from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption, ViewComponent, PanelComponent, \ + FormComponent from bauh.commons import resource, internet 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_root_subprocess, get_human_size_str +from bauh.commons.view_utils import new_select from bauh.gems.snap import snap, URL_CATEGORIES_FILE, SNAP_CACHE_PATH, CATEGORIES_FILE_PATH, SUGGESTIONS_FILE, \ - get_icon_path, snapd + get_icon_path, snapd, CONFIG_FILE +from bauh.gems.snap.config import read_config from bauh.gems.snap.model import SnapApplication from bauh.gems.snap.snapd import SnapdClient @@ -175,7 +179,20 @@ def install(self, pkg: SnapApplication, root_password: str, disk_loader: DiskCac installed_names = {s['name'] for s in SnapdClient(self.logger).list_all_snaps()} - res, output = ProcessHandler(watcher).handle_simple(snap.install_and_stream(pkg.name, pkg.confinement, root_password)) + client = SnapdClient(self.logger) + snap_config = read_config() + + try: + channel = self._request_channel_installation(pkg=pkg, snap_config=snap_config, snapd_client=client, watcher=watcher) + pkg.channel = channel + except: + watcher.print('Aborted by user') + return TransactionResult.fail() + + res, output = ProcessHandler(watcher).handle_simple(snap.install_and_stream(app_name=pkg.name, + confinement=pkg.confinement, + root_password=root_password, + channel=channel)) if 'error:' in output: res = False @@ -247,6 +264,8 @@ def _finish_category_task(self, task_man: TaskManager): task_man.finish_task('snap_cats') def prepare(self, task_manager: TaskManager, root_password: str, internet_available: bool): + Thread(target=read_config, args=(True,), daemon=True).start() + CategoriesDownloader(id_='snap', manager=self, http_client=self.http_client, logger=self.logger, url_categories_file=URL_CATEGORIES_FILE, disk_cache_dir=SNAP_CACHE_PATH, categories_path=CATEGORIES_FILE_PATH, @@ -382,3 +401,56 @@ def launch(self, pkg: SnapApplication): def get_screenshots(self, pkg: SnapApplication) -> List[str]: return pkg.screenshots if pkg.has_screenshots() else [] + + def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: + snap_config = read_config() + + install_channel = new_select(label=self.i18n['snap.config.install_channel'], + opts=[(self.i18n['yes'].capitalize(), True, None), + (self.i18n['no'].capitalize(), False, None)], + value=bool(snap_config['install_channel']), + id_='install_channel', + max_width=200, + tip=self.i18n['snap.config.install_channel.tip']) + + return PanelComponent([FormComponent([install_channel], self.i18n['installation'].capitalize())]) + + def save_settings(self, component: ViewComponent) -> Tuple[bool, Optional[List[str]]]: + config = read_config() + config['install_channel'] = component.components[0].components[0].get_selected() + + try: + save_config(config, CONFIG_FILE) + return True, None + except: + return False, [traceback.format_exc()] + + def _request_channel_installation(self, pkg: SnapApplication, snap_config: dict, snapd_client: SnapdClient, watcher: ProcessWatcher) -> Optional[str]: + if snap_config['install_channel']: + 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)) + return + + if not data: + self.logger.warning("snapd client could find a match for name '{}' when retrieving its channels".format(pkg.name)) + else: + if not data[0].get('channels'): + self.logger.info("No channel available for '{}'. Skipping selection.".format(pkg.name)) + else: + opts = [InputOption(label=c, value=c) for c in sorted(data[0]['channels'].keys())] + def_opt = [o for o in opts if o.value == 'latest/{}'.format(data[0].get('channel', 'stable'))] + select = SingleSelectComponent(label='', + options=opts, + default_option=def_opt[0] if def_opt else opts[0], + type_=SelectViewType.RADIO) + + if not watcher.request_confirmation(title=self.i18n['snap.install.available_channels.title'], + body=self.i18n['snap.install.channel.body'] + ':', + components=[select], + confirmation_label=self.i18n['proceed'].capitalize(), + deny_label=self.i18n['cancel'].capitalize()): + raise Exception('aborted') + else: + return select.get_selected() diff --git a/bauh/gems/snap/resources/locale/ca b/bauh/gems/snap/resources/locale/ca index 935f9ae1..14c84ee5 100644 --- a/bauh/gems/snap/resources/locale/ca +++ b/bauh/gems/snap/resources/locale/ca @@ -2,6 +2,8 @@ gem.snap.info=Aplicacions publicades a https://snapcraft.io/store snap.action.refresh.confirm=Actualitza {} ? snap.action.refresh.label=Actualitza snap.action.refresh.status=S’està actualitzant +snap.config.install_channel=Display channels +snap.config.install_channel.tip=It allows to select one of the available channels for the given application to be installed snap.info.channel=channel snap.info.commands=ordres snap.info.contact=contacte @@ -18,6 +20,7 @@ snap.info.version=versió snap.install.available_channels.help=Seleccioneu-ne un si voleu continuar snap.install.available_channels.message=No hi ha un canal {} (stable) disponible per a {}. Però més avall n’hi ha els següents snap.install.available_channels.title=Canals disponibles +snap.install.channel.body=Select a channel to proceed snap.notification.snap.disable=Si no voleu utilitzar aplicacions snap, desmarqueu {} a {} snap.notification.snapd_unavailable=Sembla que no s’ha iniciat o activat {}. Els paquets {} no estaran disponibles. snap.notifications.api.unavailable=Sembla que l’API de {} no està disponible en aquests moments. No podreu cercar aplicacions {} noves. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/de b/bauh/gems/snap/resources/locale/de index 7a672965..ed62a619 100644 --- a/bauh/gems/snap/resources/locale/de +++ b/bauh/gems/snap/resources/locale/de @@ -2,6 +2,8 @@ gem.snap.info=Anwendungen von https://snapcraft.io/store snap.action.refresh.confirm=Erneuern {} ? snap.action.refresh.label=Erneuern snap.action.refresh.status=Erneuern +snap.config.install_channel=Display channels +snap.config.install_channel.tip=It allows to select one of the available channels for the given application to be installed snap.info.channel=channel snap.info.commands=Kommandos snap.info.contact=Kontakt @@ -18,6 +20,7 @@ snap.info.version=Version snap.install.available_channels.help=Wähle einen aus um fortzufahren snap.install.available_channels.message=Es ist kein {} Channel verfügbar für {}. Es gibt jedoch folgende snap.install.available_channels.title=Verfügbare Channels +snap.install.channel.body=Select a channel to proceed snap.notification.snap.disable=Um keine Snap Anwerndungen zu nutzen deaktivieren {} unter {} snap.notification.snapd_unavailable={} wurde nicht gestartet oder ist deaktiviert. {} Pakete werden nicht verfügbar sein. snap.notifications.api.unavailable=Es scheint als ob die {} API aktuell nicht verfügbar ist. Die Suche nach neuen {} Anwendungen ist nicht möglich. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/en b/bauh/gems/snap/resources/locale/en index 46ed39d0..185466fb 100644 --- a/bauh/gems/snap/resources/locale/en +++ b/bauh/gems/snap/resources/locale/en @@ -2,6 +2,8 @@ gem.snap.info=Applications published at https://snapcraft.io/store snap.action.refresh.confirm=Refresh {} ? snap.action.refresh.label=Refresh snap.action.refresh.status=Refreshing +snap.config.install_channel=Display channels +snap.config.install_channel.tip=It allows to select one of the available channels for the given application to be installed snap.info.channel=channel snap.info.commands=commands snap.info.contact=contact @@ -18,6 +20,7 @@ snap.info.version=version snap.install.available_channels.help=Select one if you want to continue snap.install.available_channels.message=There is no {} channel available for {}. But there are these below snap.install.available_channels.title=Available channels +snap.install.channel.body=Select a channel to proceed snap.notification.snap.disable=If you do not want to use Snap applications, uncheck {} in {} snap.notification.snapd_unavailable={} seems not to be started or enabled. {} packages will not be available. snap.notifications.api.unavailable=It seems the {} API is unavailable at the moment. It will not be possible to search for new {} applications. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index d2d612f3..cfd58062 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -2,6 +2,8 @@ gem.snap.info=Aplicativos publicados en https://snapcraft.io/store snap.action.refresh.confirm=Actualizar {} ? snap.action.refresh.label=Actualizar snap.action.refresh.status=Actualizando +snap.config.install_channel=Mostrar canales +snap.config.install_channel.tip=Permite seleccionar uno de los canales disponibles para la aplicación a instalar snap.info.channel=canal snap.info.commands=comandos snap.info.contact=contacto @@ -18,6 +20,7 @@ snap.info.version=versión snap.install.available_channels.help=Seleccione uno si desea continuar snap.install.available_channels.message=No hay un canal {} ( stable ) disponible para {}. Pero hay estos otros abajo snap.install.available_channels.title=Canales disponibles +snap.install.channel.body=Seleccione un canal para continuar snap.notification.snap.disable=Si no desea usar aplicativos Snap, desmarque {} en {} snap.notification.snapd_unavailable={} parece no estar inicializado o habilitado. Los paquetes {} estarán indisponibles. snap.notifications.api.unavailable=Parece que la API de {} no está disponible en este momento. No será posible buscar nuevos aplicativos {}. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/it b/bauh/gems/snap/resources/locale/it index 254cfadc..a5448426 100644 --- a/bauh/gems/snap/resources/locale/it +++ b/bauh/gems/snap/resources/locale/it @@ -2,6 +2,8 @@ gem.snap.info=Applicazioni pubblicate su https://snapcraft.io/store snap.action.refresh.confirm=Ripristina {} ? snap.action.refresh.label=Ripristina snap.action.refresh.status=Ripristinare +snap.config.install_channel=Display channels +snap.config.install_channel.tip=It allows to select one of the available channels for the given application to be installed snap.info.channel=channel snap.info.commands=commands snap.info.contact=contact @@ -18,6 +20,7 @@ snap.info.version=version snap.install.available_channels.help=Selezionane uno se vuoi continuare snap.install.available_channels.message=Non esiste un {} canale disponibile per {}. Ma ci sono questi sotto snap.install.available_channels.title=Canali disponibili +snap.install.channel.body=Select a channel to proceed snap.notification.snap.disable=Se non si desidera utilizzare le applicazioni Snap, deselezionare {} in {} snap.notification.snapd_unavailable={} sembra non essere avviato o abilitato. {} i pacchetti non saranno disponibili. snap.notifications.api.unavailable=Sembra che l'API {} non sia al momento disponibile. Non sarà possibile cercare nuove {} applicazioni. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/pt b/bauh/gems/snap/resources/locale/pt index 54fafa34..415175f9 100644 --- a/bauh/gems/snap/resources/locale/pt +++ b/bauh/gems/snap/resources/locale/pt @@ -2,6 +2,8 @@ gem.snap.info=Aplicativos publicados em https://snapcraft.io/store snap.action.refresh.confirm=Atualizar {} ? snap.action.refresh.label=Atualizar snap.action.refresh.status=Atualizando +snap.config.install_channel=Exibir canais +snap.config.install_channel.tip=Permite-se selecionar um dos canais disponíveis para o aplicativo a ser instalado snap.info.channel=canal snap.info.commands=comandos snap.info.contact=contato @@ -18,6 +20,7 @@ snap.info.version=versão snap.install.available_channels.help=Selecione algum se quiser continuar snap.install.available_channels.message=Não há um canal {} ( stable ) disponível para {}. Porém existem estes outros abaixo snap.install.available_channels.title=Canais disponíveis +snap.install.channel.body=Selecione um canal para continuar snap.notification.snap.disable=Se não deseja usar aplicativos Snap, desmarque {} em {} snap.notification.snapd_unavailable={} parece não estar inicializado ou habilitado. Os pacotes {} estarão indisponíveis. snap.notifications.api.unavailable=Parece que a API de {} está indisponível no momento. Não será possível buscar novos aplicativos {}. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/ru b/bauh/gems/snap/resources/locale/ru index 76fc3d96..747292d2 100644 --- a/bauh/gems/snap/resources/locale/ru +++ b/bauh/gems/snap/resources/locale/ru @@ -2,6 +2,8 @@ gem.snap.info=Приложения, опубликованные на https://sn snap.action.refresh.confirm=Обновить {} ? snap.action.refresh.label=Обновить snap.action.refresh.status=Обновляется +snap.config.install_channel=Display channels +snap.config.install_channel.tip=It allows to select one of the available channels for the given application to be installed snap.info.channel=channel snap.info.commands=Команды snap.info.contact=Контакт @@ -18,6 +20,7 @@ snap.info.version=Версия snap.install.available_channels.help=Выберите один, если вы хотите продолжить snap.install.available_channels.message=Нет одного канала {}, доступного для {}. Но есть такие ниже snap.install.available_channels.title=Доступные каналы +snap.install.channel.body=Select a channel to proceed snap.notification.snap.disable=Если вы не хотите использовать приложения Snap, снимите флажок {} в {} snap.notification.snapd_unavailable={} кажется, не запускается и не включается. {} пакеты будут недоступны. snap.notifications.api.unavailable=Похоже, что API {} в данный момент недоступен. Невозможно будет искать новые приложения {}. \ No newline at end of file diff --git a/bauh/gems/snap/resources/locale/tr b/bauh/gems/snap/resources/locale/tr index 9799d921..9b0861a3 100644 --- a/bauh/gems/snap/resources/locale/tr +++ b/bauh/gems/snap/resources/locale/tr @@ -2,6 +2,8 @@ gem.snap.info=https://snapcraft.io/store adresinde yayınlanan uygulamalar snap.action.refresh.confirm=Yenile {} ? snap.action.refresh.label=Yenile snap.action.refresh.status=Yenileniyor +snap.config.install_channel=Display channels +snap.config.install_channel.tip=It allows to select one of the available channels for the given application to be installed snap.info.channel=channel snap.info.commands=komutlar snap.info.contact=iletişim @@ -18,6 +20,7 @@ snap.info.version=sürüm snap.install.available_channels.help=Devam etmek istiyorsanız birini seçin snap.install.available_channels.message={} için hiçbir {} kanalı yok. Ama aşağıda bunlar var snap.install.available_channels.title=Kullanılabilir kanallar +snap.install.channel.body=Select a channel to proceed snap.notification.snap.disable=Snap uygulamalarını kullanmak istemiyorsanız, {} içindeki {} işaretini kaldırın snap.notification.snapd_unavailable={} başlatılmamış veya etkinleştirilmemiş gibi görünüyor. {} paketler mevcut olmayacak. snap.notifications.api.unavailable=Görünüşe göre {} API şu anda kullanılamıyor. Yeni {} uygulamaları aramak mümkün olmayacak. diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index 0558df23..244f61cd 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -1,7 +1,7 @@ import os import subprocess from io import StringIO -from typing import Tuple +from typing import Tuple, Optional from bauh.commons.system import run_cmd, SimpleProcess @@ -18,13 +18,16 @@ def uninstall_and_stream(app_name: str, root_password: str) -> SimpleProcess: shell=True) -def install_and_stream(app_name: str, confinement: str, root_password: str) -> SimpleProcess: +def install_and_stream(app_name: str, confinement: str, root_password: str, channel: Optional[str] = None) -> SimpleProcess: install_cmd = [BASE_CMD, 'install', app_name] # default if confinement == 'classic': install_cmd.append('--classic') + if channel: + install_cmd.append('--channel={}'.format(channel)) + return SimpleProcess(install_cmd, root_password=root_password, shell=True) diff --git a/bauh/gems/web/controller.py b/bauh/gems/web/controller.py index 6361aec2..bfc0bfad 100644 --- a/bauh/gems/web/controller.py +++ b/bauh/gems/web/controller.py @@ -9,7 +9,7 @@ from math import floor from pathlib import Path from threading import Thread -from typing import List, Type, Set, Tuple +from typing import List, Type, Set, Tuple, Optional import requests import yaml @@ -996,7 +996,7 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: return PanelComponent([form_env]) - def save_settings(self, component: PanelComponent) -> Tuple[bool, List[str]]: + def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[str]]]: config = read_config() form_env = component.components[0] diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index e247b12a..1e827cba 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -266,6 +266,7 @@ def __init__(self, model: InputOption, model_parent: SingleSelectComponent): self.model = model self.model_parent = model_parent self.toggled.connect(self._set_checked) + self.setCursor(QCursor(Qt.PointingHandCursor)) if model.icon_path: if model.icon_path.startswith('/'): From c5e421d270d83b051739c91d135fb246200425c2 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 6 Sep 2020 01:17:11 -0300 Subject: [PATCH 81/99] [snap] fix -> app not launchable after recent installation --- bauh/gems/snap/controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index b241e287..f4a84f40 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -221,17 +221,17 @@ def install(self, pkg: SnapApplication, root_password: str, disk_loader: DiskCac def _gen_installation_response(self, success: bool, pkg: SnapApplication, installed: Set[str], disk_loader: DiskCacheLoader): if success: - new_installed = [pkg] - + new_installed = [] try: current_installed = self.read_installed(disk_loader=disk_loader, internet_available=internet.is_available()).installed except: + new_installed = [pkg] traceback.print_exc() current_installed = None if current_installed: for p in current_installed: - if p.name != pkg.name and (not installed or p.name not in installed): + if p.name == pkg.name or (not installed or p.name not in installed): new_installed.append(p) return TransactionResult(success=success, installed=new_installed, removed=[]) From 2d395b608fd83346e7ccabdee0fa92b53778f2e4 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Sun, 6 Sep 2020 11:41:39 -0300 Subject: [PATCH 82/99] [snap] feature -> new custom action 'change channel' --- CHANGELOG.md | 5 +++ README.md | 4 +- bauh/gems/snap/controller.py | 61 +++++++++++++++++++++++++++--- bauh/gems/snap/resources/locale/ca | 4 ++ bauh/gems/snap/resources/locale/de | 4 ++ bauh/gems/snap/resources/locale/en | 4 ++ bauh/gems/snap/resources/locale/es | 4 ++ bauh/gems/snap/resources/locale/it | 4 ++ bauh/gems/snap/resources/locale/pt | 4 ++ bauh/gems/snap/resources/locale/ru | 4 ++ bauh/gems/snap/resources/locale/tr | 4 ++ bauh/gems/snap/snap.py | 9 ++++- bauh/view/qt/apps_table.py | 43 +++++++++++---------- 13 files changed, 125 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 354bff3c..ba45e6a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

+ + - new custom action **Change channel**: allows to change the current channel of an installed Snap +

+ +

### Improvements - AppImage diff --git a/README.md b/README.md index 17fbbd61..390fd401 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,9 @@ installation_level: null # defines a default installation level: user or system. #### Snap (snap) - Supported actions: search, install, uninstall, launch, downgrade -- Custom actions: refresh +- Custom actions: + - refresh: tries to update the current Snap application revision + - change channel: allows to change the Snap application channel - The configuration file is located at **~/.config/bauh/snap.yml** and it allows the following customizations: ``` install_channel: false # it allows to select an available channel during the application installation. Default: false diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index f4a84f40..f0b9c471 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -12,6 +12,7 @@ SuggestionPriority, CustomSoftwareAction, PackageStatus from bauh.api.abstract.view import SingleSelectComponent, SelectViewType, InputOption, ViewComponent, PanelComponent, \ FormComponent +from bauh.api.exception import NoInternetException from bauh.commons import resource, internet from bauh.commons.category import CategoriesDownloader from bauh.commons.config import save_config @@ -47,7 +48,13 @@ def __init__(self, context: ApplicationContext): icon_path=resource.get_path('img/refresh.svg', context.get_view_path()), manager_method='refresh', requires_root=True, - i18n_confirm_key='snap.action.refresh.confirm') + i18n_confirm_key='snap.action.refresh.confirm'), + CustomSoftwareAction(i18n_status_key='snap.action.channel.status', + i18n_label_key='snap.action.channel.label', + i18n_confirm_key='snap.action.channel.confirm', + icon_path=resource.get_path('img/refresh.svg', context.get_view_path()), + manager_method='change_channel', + requires_root=True) ] def _fill_categories(self, app: SnapApplication): @@ -253,6 +260,28 @@ def requires_root(self, action: str, pkg: SnapApplication): def refresh(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: return ProcessHandler(watcher).handle_simple(snap.refresh_and_stream(pkg.name, root_password))[0] + def change_channel(self, pkg: SnapApplication, root_password: str, watcher: ProcessWatcher) -> bool: + if not internet.is_available(): + raise NoInternetException() + + try: + channel = self._request_channel_installation(pkg=pkg, + snap_config=None, + snapd_client=SnapdClient(self.logger), + watcher=watcher, + exclude_current=True) + + if not channel: + watcher.show_message(title=self.i18n['snap.action.channel.label'], + body=self.i18n['snap.action.channel.error.no_channel']) + return True + + return ProcessHandler(watcher).handle_simple(snap.refresh_and_stream(app_name=pkg.name, + root_password=root_password, + channel=channel))[0] + except: + return True + def _start_category_task(self, task_man: TaskManager): if task_man: task_man.register_task('snap_cats', self.i18n['task.download_categories'].format('Snap'), get_icon_path()) @@ -425,8 +454,8 @@ def save_settings(self, component: ViewComponent) -> Tuple[bool, Optional[List[s except: return False, [traceback.format_exc()] - def _request_channel_installation(self, pkg: SnapApplication, snap_config: dict, snapd_client: SnapdClient, watcher: ProcessWatcher) -> Optional[str]: - if snap_config['install_channel']: + def _request_channel_installation(self, pkg: SnapApplication, snap_config: Optional[dict], snapd_client: SnapdClient, watcher: ProcessWatcher, exclude_current: bool = False) -> Optional[str]: + if snap_config is None or snap_config['install_channel']: try: data = [r for r in snapd_client.find_by_name(pkg.name) if r['name'] == pkg.name] except: @@ -439,11 +468,31 @@ def _request_channel_installation(self, pkg: SnapApplication, snap_config: dict, if not data[0].get('channels'): self.logger.info("No channel available for '{}'. Skipping selection.".format(pkg.name)) else: - opts = [InputOption(label=c, value=c) for c in sorted(data[0]['channels'].keys())] - def_opt = [o for o in opts if o.value == 'latest/{}'.format(data[0].get('channel', 'stable'))] + if pkg.channel: + current_channel = pkg.channel if '/' in pkg.channel else 'latest/{}'.format(pkg.channel) + else: + current_channel = 'latest/{}'.format(data[0].get('channel', 'stable')) + + opts = [] + def_opt = None + for channel in sorted(data[0]['channels'].keys()): + if exclude_current: + if channel != current_channel: + opts.append(InputOption(label=channel, value=channel)) + else: + op = InputOption(label=channel, value=channel) + opts.append(op) + + if not def_opt and channel == current_channel: + def_opt = op + + if not opts: + self.logger.info("No different channel available for '{}'. Skipping selection.".format(pkg.name)) + return + select = SingleSelectComponent(label='', options=opts, - default_option=def_opt[0] if def_opt else opts[0], + default_option=def_opt if def_opt else opts[0], type_=SelectViewType.RADIO) if not watcher.request_confirmation(title=self.i18n['snap.install.available_channels.title'], diff --git a/bauh/gems/snap/resources/locale/ca b/bauh/gems/snap/resources/locale/ca index 14c84ee5..1341ff1c 100644 --- a/bauh/gems/snap/resources/locale/ca +++ b/bauh/gems/snap/resources/locale/ca @@ -1,4 +1,8 @@ gem.snap.info=Aplicacions publicades a https://snapcraft.io/store +snap.action.channel.confirm=Change channel of {} ? +snap.action.channel.error.no_channel=No available channel for {} +snap.action.channel.label=Change channel +snap.action.channel.status=Changing channel snap.action.refresh.confirm=Actualitza {} ? snap.action.refresh.label=Actualitza snap.action.refresh.status=S’està actualitzant diff --git a/bauh/gems/snap/resources/locale/de b/bauh/gems/snap/resources/locale/de index ed62a619..50666790 100644 --- a/bauh/gems/snap/resources/locale/de +++ b/bauh/gems/snap/resources/locale/de @@ -1,4 +1,8 @@ gem.snap.info=Anwendungen von https://snapcraft.io/store +snap.action.channel.error.no_channel=No available channel for {} +snap.action.channel.confirm=Change channel of {} ? +snap.action.channel.label=Change channel +snap.action.channel.status=Changing channel snap.action.refresh.confirm=Erneuern {} ? snap.action.refresh.label=Erneuern snap.action.refresh.status=Erneuern diff --git a/bauh/gems/snap/resources/locale/en b/bauh/gems/snap/resources/locale/en index 185466fb..d22604bb 100644 --- a/bauh/gems/snap/resources/locale/en +++ b/bauh/gems/snap/resources/locale/en @@ -1,4 +1,8 @@ gem.snap.info=Applications published at https://snapcraft.io/store +snap.action.channel.confirm=Change channel of {} ? +snap.action.channel.error.no_channel=No available channel for {} +snap.action.channel.label=Change channel +snap.action.channel.status=Changing channel snap.action.refresh.confirm=Refresh {} ? snap.action.refresh.label=Refresh snap.action.refresh.status=Refreshing diff --git a/bauh/gems/snap/resources/locale/es b/bauh/gems/snap/resources/locale/es index cfd58062..3a69fc4a 100644 --- a/bauh/gems/snap/resources/locale/es +++ b/bauh/gems/snap/resources/locale/es @@ -1,4 +1,8 @@ gem.snap.info=Aplicativos publicados en https://snapcraft.io/store +snap.action.channel.confirm=Cambiar canal de {}? +snap.action.channel.error.no_channel=No hay canal disponible para {} +snap.action.channel.status=Cambiando canal +snap.action.channel.label=Cambiar canal snap.action.refresh.confirm=Actualizar {} ? snap.action.refresh.label=Actualizar snap.action.refresh.status=Actualizando diff --git a/bauh/gems/snap/resources/locale/it b/bauh/gems/snap/resources/locale/it index a5448426..7675c75e 100644 --- a/bauh/gems/snap/resources/locale/it +++ b/bauh/gems/snap/resources/locale/it @@ -1,4 +1,8 @@ gem.snap.info=Applicazioni pubblicate su https://snapcraft.io/store +snap.action.channel.confirm=Change channel of {} ? +snap.action.channel.error.no_channel=No available channel for {} +snap.action.channel.label=Change channel +snap.action.channel.status=Changing channel snap.action.refresh.confirm=Ripristina {} ? snap.action.refresh.label=Ripristina snap.action.refresh.status=Ripristinare diff --git a/bauh/gems/snap/resources/locale/pt b/bauh/gems/snap/resources/locale/pt index 415175f9..67e983ce 100644 --- a/bauh/gems/snap/resources/locale/pt +++ b/bauh/gems/snap/resources/locale/pt @@ -1,4 +1,8 @@ gem.snap.info=Aplicativos publicados em https://snapcraft.io/store +snap.action.channel.confirm=Alterar canal de {} ? +snap.action.channel.error.no_channel=Nenhum canal disponível para {} +snap.action.channel.label=Alterar canal +snap.action.channel.status=Alterando canal snap.action.refresh.confirm=Atualizar {} ? snap.action.refresh.label=Atualizar snap.action.refresh.status=Atualizando diff --git a/bauh/gems/snap/resources/locale/ru b/bauh/gems/snap/resources/locale/ru index 747292d2..f6312c71 100644 --- a/bauh/gems/snap/resources/locale/ru +++ b/bauh/gems/snap/resources/locale/ru @@ -1,4 +1,8 @@ gem.snap.info=Приложения, опубликованные на https://snapcraft.io/store +snap.action.channel.confirm=Change channel of {} ? +snap.action.channel.error.no_channel=No available channel for {} +snap.action.channel.label=Change channel +snap.action.channel.status=Changing channel snap.action.refresh.confirm=Обновить {} ? snap.action.refresh.label=Обновить snap.action.refresh.status=Обновляется diff --git a/bauh/gems/snap/resources/locale/tr b/bauh/gems/snap/resources/locale/tr index 9b0861a3..dfb7d804 100644 --- a/bauh/gems/snap/resources/locale/tr +++ b/bauh/gems/snap/resources/locale/tr @@ -1,4 +1,8 @@ gem.snap.info=https://snapcraft.io/store adresinde yayınlanan uygulamalar +snap.action.channel.confirm=Change channel of {} ? +snap.action.channel.error.no_channel=No available channel for {} +snap.action.channel.label=Change channel +snap.action.channel.status=Changing channel snap.action.refresh.confirm=Yenile {} ? snap.action.refresh.label=Yenile snap.action.refresh.status=Yenileniyor diff --git a/bauh/gems/snap/snap.py b/bauh/gems/snap/snap.py index 244f61cd..01fe60fe 100644 --- a/bauh/gems/snap/snap.py +++ b/bauh/gems/snap/snap.py @@ -37,8 +37,13 @@ def downgrade_and_stream(app_name: str, root_password: str) -> SimpleProcess: shell=True) -def refresh_and_stream(app_name: str, root_password: str) -> SimpleProcess: - return SimpleProcess(cmd=[BASE_CMD, 'refresh', app_name], +def refresh_and_stream(app_name: str, root_password: str, channel: Optional[str] = None) -> SimpleProcess: + cmd = [BASE_CMD, 'refresh', app_name] + + if channel: + cmd.append('--channel={}'.format(channel)) + + return SimpleProcess(cmd=cmd, root_password=root_password, error_phrases={'no updates available'}, shell=True) diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index 380e05a4..4d70c958 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -11,7 +11,7 @@ QHeaderView, QLabel, QHBoxLayout, QToolBar, QSizePolicy from bauh.api.abstract.cache import MemoryCache -from bauh.api.abstract.model import PackageStatus +from bauh.api.abstract.model import PackageStatus, CustomSoftwareAction from bauh.commons.html import strip_html, bold from bauh.view.qt import dialog from bauh.view.qt.colors import GREEN, BROWN @@ -166,30 +166,33 @@ def ignore_updates(): menu_row.addAction(action_ignore_updates) if bool(pkg.model.get_custom_supported_actions()): - for action in pkg.model.get_custom_supported_actions(): - item = QAction(self.i18n[action.i18n_label_key]) + actions = [self._map_custom_action(pkg, a) for a in pkg.model.get_custom_supported_actions()] + menu_row.addActions(actions) - if action.icon_path: - item.setIcon(QIcon(action.icon_path)) + menu_row.adjustSize() + menu_row.popup(QCursor.pos()) + menu_row.exec_() - def custom_action(): - if action.i18n_confirm_key: - body = self.i18n[action.i18n_confirm_key].format(bold(pkg.model.name)) - else: - body = '{} ?'.format(self.i18n[action.i18n_label_key]) + def _map_custom_action(self, pkg: PackageView, action: CustomSoftwareAction) -> QAction: + item = QAction(self.i18n[action.i18n_label_key]) - if dialog.ask_confirmation( - title=self.i18n[action.i18n_label_key], - body=self._parag(body), - i18n=self.i18n): - self.window.begin_execute_custom_action(pkg, action) + if action.icon_path: + item.setIcon(QIcon(action.icon_path)) - item.triggered.connect(custom_action) - menu_row.addAction(item) + def custom_action(): + if action.i18n_confirm_key: + body = self.i18n[action.i18n_confirm_key].format(bold(pkg.model.name)) + else: + body = '{} ?'.format(self.i18n[action.i18n_label_key]) - menu_row.adjustSize() - menu_row.popup(QCursor.pos()) - menu_row.exec_() + if dialog.ask_confirmation( + title=self.i18n[action.i18n_label_key], + body=self._parag(body), + i18n=self.i18n): + self.window.begin_execute_custom_action(pkg, action) + + item.triggered.connect(custom_action) + return item def refresh(self, pkg: PackageView): self._update_row(pkg, update_check_enabled=False, change_update_col=False) From 1c1aad1e0e2c9c62c21222ed0186d0d9494c02e6 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Wed, 9 Sep 2020 18:51:37 -0300 Subject: [PATCH 83/99] [arch] fix -> AUR: not properly handling AUR package dependencies with specific versions --- CHANGELOG.md | 1 + bauh/gems/arch/aur.py | 6 ++++-- bauh/gems/arch/controller.py | 8 +++++++- bauh/gems/arch/dependencies.py | 27 +++++++++++++++------------ bauh/gems/arch/resources/locale/ca | 2 +- bauh/gems/arch/resources/locale/de | 2 +- bauh/gems/arch/resources/locale/en | 2 +- bauh/gems/arch/resources/locale/es | 2 +- bauh/gems/arch/resources/locale/it | 2 +- bauh/gems/arch/resources/locale/pt | 2 +- bauh/gems/arch/resources/locale/ru | 2 +- bauh/gems/arch/resources/locale/tr | 2 +- 12 files changed, 35 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ba45e6a1..8bef3bb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -116,6 +116,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - some packages dependencies cannot be downloaded due to the wrong download URL (missing the 'pkgbase' field to determine the proper url) - not properly extracting srcinfo data when several pkgnames are declared (leads to wrong dependencies requirements) - not detecting some package updates + - not properly handling AUR package dependencies with specific versions. e.g: abc>=1.20 - Flatpak - downgrading crashing with version 1.8.X diff --git a/bauh/gems/arch/aur.py b/bauh/gems/arch/aur.py index 947f6f76..fcd1ef95 100644 --- a/bauh/gems/arch/aur.py +++ b/bauh/gems/arch/aur.py @@ -7,7 +7,7 @@ import requests from bauh.api.http import HttpClient -from bauh.gems.arch import pacman, AUR_INDEX_FILE +from bauh.gems.arch import AUR_INDEX_FILE from bauh.gems.arch.exceptions import PackageNotFoundException URL_INFO = 'https://aur.archlinux.org/rpc/?v=5&type=info&' @@ -160,14 +160,16 @@ def get_src_info(self, name: str, real_name: Optional[str] = None) -> dict: def extract_required_dependencies(self, srcinfo: dict) -> Set[str]: deps = set() + for attr in ('makedepends', 'makedepends_{}'.format('x86_64' if self.x86_64 else 'i686'), 'depends', 'depends_{}'.format('x86_64' if self.x86_64 else 'i686'), 'checkdepends', 'checkdepends_{}'.format('x86_64' if self.x86_64 else 'i686')): + if srcinfo.get(attr): - deps.update([pacman.RE_DEP_OPERATORS.split(dep)[0] for dep in srcinfo[attr]]) + deps.update(srcinfo[attr]) return deps diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index 2dbc0573..a13ca7fb 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1904,7 +1904,13 @@ def _list_missing_deps(self, context: TransactionContext) -> List[Tuple[str, str return missing_deps def _handle_missing_deps(self, context: TransactionContext) -> bool: - missing_deps = self._list_missing_deps(context) + try: + missing_deps = self._list_missing_deps(context) + except PackageNotFoundException: + return False + except: + traceback.print_exc() + return False if missing_deps is None: return False # called off by the user diff --git a/bauh/gems/arch/dependencies.py b/bauh/gems/arch/dependencies.py index 9f4759e7..1f129a40 100644 --- a/bauh/gems/arch/dependencies.py +++ b/bauh/gems/arch/dependencies.py @@ -250,10 +250,10 @@ def _fill_missing_dep(self, dep_name: str, dep_exp: str, aur_index: Iterable[str missing_deps.add((dep_name, 'aur')) else: if watcher: - message.show_dep_not_found(dep_name, self.i18n, watcher) - raise PackageNotFoundException(dep_name) + message.show_dep_not_found(dep_exp, self.i18n, watcher) + raise PackageNotFoundException(dep_exp) else: - raise PackageNotFoundException(dep_name) + raise PackageNotFoundException(dep_exp) def __fill_aur_update_data(self, pkgname: str, output: dict): output[pkgname] = self.aur_client.map_update_data(pkgname, None) @@ -308,17 +308,20 @@ def map_missing_deps(self, pkgs_data: Dict[str, dict], provided_map: Dict[str, S version_informed = parse_version(version_informed) op = dep_split[1] if dep_split[1] != '=' else '==' - if not eval('version_found {} version_informed'.format(op)): - self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, - missing_deps=missing_deps, - remote_provided_map=remote_provided_map, - remote_repo_map=remote_repo_map, - repo_deps=repo_missing, aur_deps=aur_missing, - watcher=watcher, - deps_data=deps_data, - automatch_providers=automatch_providers) + match = eval('version_found {} version_informed'.format(op)) except: + match = False traceback.print_exc() + + if not match: + self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, + missing_deps=missing_deps, + remote_provided_map=remote_provided_map, + remote_repo_map=remote_repo_map, + repo_deps=repo_missing, aur_deps=aur_missing, + watcher=watcher, + deps_data=deps_data, + automatch_providers=automatch_providers) else: self._fill_missing_dep(dep_name=dep_name, dep_exp=dep, aur_index=aur_index, missing_deps=missing_deps, diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 042f40cf..5d6c2a5e 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -170,7 +170,7 @@ arch.install.conflict.popup.body=Les aplicacions {} estan en conflicte. Heu de d arch.install.conflict.popup.title=S’ha detectat un conflicte arch.install.dep_not_found.body.l1=No s’ha trobat la dependència requerida {} a l’AUR ni als dipòsits arch.install.dep_not_found.body.l2=It might be a package database synchronization problem. -arch.install.dep_not_found.body.l3=S’ha cancel·lat la instal·lació. +arch.install.dep_not_found.body.l3=Operation cancelled. arch.install.dep_not_found.title=No s’ha trobat la dependència arch.install.dependency.install=S’està instal·lant el paquet depenent {} arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 5a05b4c7..827fcaf0 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -170,7 +170,7 @@ arch.install.conflict.popup.body=Die Anwendungen {} können nicht gleichzeitig i arch.install.conflict.popup.title=Konflikt entdeckt arch.install.dep_not_found.body.l1=Nötige Abhängigkeit {} wurde weder im AUR noch in den standard Spiegelservern gefunden arch.install.dep_not_found.body.l2=It might be a package database synchronization problem. -arch.install.dep_not_found.body.l3=Installation abgebrochen. +arch.install.dep_not_found.body.l3=Operation cancelled. arch.install.dep_not_found.title=Abhängigkeit nicht gefunden arch.install.dependency.install=Abhängigket {} wird installiert arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index 9855d84b..c85e103a 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -171,7 +171,7 @@ arch.install.conflict.popup.body=The applications {} are in conflict. You must u arch.install.conflict.popup.title=Conflict detected arch.install.dep_not_found.body.l1=Required dependency {} was not found in AUR nor in the repositories. arch.install.dep_not_found.body.l2=It might be a package database synchronization problem. -arch.install.dep_not_found.body.l3=Installation cancelled. +arch.install.dep_not_found.body.l3=Operation cancelled. arch.install.dep_not_found.title=Dependency not found arch.install.dependency.install=Installing package dependency {} arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 4a16fafa..32127891 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -170,7 +170,7 @@ arch.install.conflict.popup.body=Los aplicativos {} estan en conflicto. Debe des arch.install.conflict.popup.title=Conflicto detectado arch.install.dep_not_found.body.l1=No se encontró la dependencia requerida {} en AUR ni en los repositorios. arch.install.dep_not_found.body.l2=Puede ser un problema de sincronización de la base de paquetes. -arch.install.dep_not_found.body.l3=Instalación cancelada. +arch.install.dep_not_found.body.l3=Operación cancelada. arch.install.dep_not_found.title=Dependencia no encontrada arch.install.dependency.install=Instalando el paquete dependiente {} arch.install.dependency.install.error=No se pudo instalar los paquetes dependientes: {}. Instalación de {} abortada. diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index 09477ed5..d576ef8d 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -170,7 +170,7 @@ arch.install.conflict.popup.body=Le applicazioni {} sono in conflitto. È necess arch.install.conflict.popup.title=Conflitto rilevato arch.install.dep_not_found.body.l1=La dipendenza richiesta {} non è stata trovata in AUR né nei depositos. arch.install.dep_not_found.body.l2=It might be a package database synchronization problem. -arch.install.dep_not_found.body.l3=Installazione annullata. +arch.install.dep_not_found.body.l3=Operation cancelled. arch.install.dep_not_found.title=Dipendenza non trovata arch.install.dependency.install=Installazione della dipendenza pacchetto {} arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index 5396b5a3..d1ef855f 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -170,7 +170,7 @@ arch.install.conflict.popup.body=Os aplicativos {} estão em conflito. Você pre arch.install.conflict.popup.title=Conflito detectado arch.install.dep_not_found.body.l1=A dependência {} não foi encontrado no AUR nem nos repositórios. arch.install.dep_not_found.body.l2=Pode ser um problema de sincronização das bases de pacotes. -arch.install.dep_not_found.body.l3=Instalação cancelada. +arch.install.dep_not_found.body.l3=Operação cancelada. arch.install.dep_not_found.title=Dependência não encontrada arch.install.dependency.install=Instalando o pacote dependente {} arch.install.dependency.install.error=Não foi possível instalar os pacotes dependentes: {}. Instalação de {} abortada. diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index b964821c..751f10b1 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -170,7 +170,7 @@ arch.install.conflict.popup.body=Приложения {} находятся в arch.install.conflict.popup.title=Обнаружен конфликт arch.install.dep_not_found.body.l1=Требуемая зависимость {} не была найдена ни в AUR, ни в зеркалах по умолчанию. arch.install.dep_not_found.body.l2=It might be a package database synchronization problem. -arch.install.dep_not_found.body.l3=Установка отменена. +arch.install.dep_not_found.body.l3=Operation cancelled. arch.install.dep_not_found.title=Зависимость не найдена arch.install.dependency.install=Установка зависимостей пакета {} arch.install.dependency.install.error=Could not install the dependent packages: {}. Installation of {} aborted. diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 1c2550c0..023bea68 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -170,7 +170,7 @@ arch.install.conflict.popup.body={} Uygulamaları çakışıyor. Diğerini kurma arch.install.conflict.popup.title=Çakışma tespit edildi arch.install.dep_not_found.body.l1=Gerekli bağımlılık {} ne AUR ne de resmi depolarda bulunamadı. arch.install.dep_not_found.body.l2=Bir paket veritabanı senkronizasyon sorunu olabilir. -arch.install.dep_not_found.body.l3=Yükleme iptal edildi. +arch.install.dep_not_found.body.l3=Operation cancelled. arch.install.dep_not_found.title=Bağımlılık bulunamadı arch.install.dependency.install=Paket bağımlılığını yükleniyor {} arch.install.dependency.install.error=Bağımlı paketler yüklenemedi: {}. {} Kurulumu iptal edildi. From bd40d9f2a89e5ae685528315c53df6490d775130 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 10 Sep 2020 13:40:11 -0300 Subject: [PATCH 84/99] [flatpak] fix log --- bauh/api/http.py | 5 +++-- bauh/gems/flatpak/worker.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/bauh/api/http.py b/bauh/api/http.py index 0d04e558..fc65d165 100644 --- a/bauh/api/http.py +++ b/bauh/api/http.py @@ -1,6 +1,7 @@ import logging import time import traceback +from typing import Optional import requests import yaml @@ -17,7 +18,7 @@ def __init__(self, logger: logging.Logger, max_attempts: int = 2, timeout: int = self.sleep = sleep self.logger = logger - def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False, session: bool = True) -> requests.Response: + def get(self, url: str, params: dict = None, headers: dict = None, allow_redirects: bool = True, ignore_ssl: bool = False, single_call: bool = False, session: bool = True) -> Optional[requests.Response]: cur_attempts = 1 while cur_attempts <= self.max_attempts: @@ -44,7 +45,7 @@ def get(self, url: str, params: dict = None, headers: dict = None, allow_redirec return res if single_call: - return + return res if self.sleep > 0: time.sleep(self.sleep) diff --git a/bauh/gems/flatpak/worker.py b/bauh/gems/flatpak/worker.py index fe1e0403..80d48885 100644 --- a/bauh/gems/flatpak/worker.py +++ b/bauh/gems/flatpak/worker.py @@ -87,7 +87,7 @@ def run(self): self.api_cache.add(self.app.id, loaded_data) self.persist = self.app.supports_disk_cache() else: - self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code, res.content.decode())) + self.logger.warning("Could not retrieve app data for id '{}'. Server response: {}. Body: {}".format(self.app.id, res.status_code if res else '?', res.content.decode() if res else '?')) except: self.logger.error("Could not retrieve app data for id '{}'".format(self.app.id)) traceback.print_exc() From 590eb2abd242b86f2222334b3fd82b9928fe33d9 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Thu, 10 Sep 2020 13:41:21 -0300 Subject: [PATCH 85/99] [api] marking argument type as optional --- bauh/api/abstract/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/api/abstract/controller.py b/bauh/api/abstract/controller.py index deef917b..933f4ef7 100644 --- a/bauh/api/abstract/controller.py +++ b/bauh/api/abstract/controller.py @@ -223,7 +223,7 @@ def can_work(self) -> bool: :return: if the instance can work based on what is installed in the user's machine. """ - def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: bytes, only_icon: bool): + def cache_to_disk(self, pkg: SoftwarePackage, icon_bytes: Optional[bytes], only_icon: bool): """ Saves the package data to the hard disk. :param pkg: From 293d2a5fb0484f23f5cc985d419daf65591cb424 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 11:17:27 -0300 Subject: [PATCH 86/99] [arch] fix -> uninstall: not detecting hard requirements properly --- CHANGELOG.md | 11 +- README.md | 4 +- bauh/commons/system.py | 5 + bauh/gems/arch/config.py | 4 +- bauh/gems/arch/controller.py | 203 ++++++++++++++++++----------- bauh/gems/arch/exceptions.py | 8 ++ bauh/gems/arch/pacman.py | 46 ++++++- bauh/gems/arch/resources/locale/ca | 5 + bauh/gems/arch/resources/locale/de | 5 + bauh/gems/arch/resources/locale/en | 5 + bauh/gems/arch/resources/locale/es | 5 + bauh/gems/arch/resources/locale/it | 5 + bauh/gems/arch/resources/locale/pt | 5 + bauh/gems/arch/resources/locale/ru | 5 + bauh/gems/arch/resources/locale/tr | 5 + 15 files changed, 235 insertions(+), 86 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bef3bb6..63b85d57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- allowing the user to bypass dependency breakage scenarios (a popup will be displayed) + - new settings property **suggest_unneeded_uninstall**: defines if the dependencies apparently no longer necessary associated with the uninstalled packages should be suggested for uninstallation. When this property is enabled it automatically disables the property **suggest_optdep_uninstall**. Default: false (to prevent new users from making mistakes) + - new settings property **suggest_optdep_uninstall**: defines if the optional dependencies associated with uninstalled packages should be suggested for uninstallation. Only the optional dependencies that are not dependencies of other packages will be suggested. Default: false (to prevent new users from making mistakes) +

+ +

- AUR - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) @@ -104,10 +109,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - some conflict resolution scenarios when upgrading several packages - not handling conflicting files errors during the installation process - - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) - - not displaying and uninstalling dependent packages during conflict resolutions - - not retrieving all packages that would break if a given package is uninstalled - displaying wrong progress during the upgrade process when there are packages to install and upgrade + - uninstall: not detecting hard requirements properly + - not displaying and uninstalling dependent packages during conflict resolutions + - some environment variables are not available during the common operations (install, upgrade, downgrade, uninstall, makepkg, launch) - AUR - info dialog of installed packages displays the latest PKGBUILD file instead of the one used for installation/upgrade/downgrade (the fix will only work for new installed packages) - multi-threaded download: not retrieving correctly some source files URLs (e.g: linux-xanmod-lts) diff --git a/README.md b/README.md index 390fd401..6fb25aa0 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,9 @@ edit_aur_pkgbuild: false # if the AUR PKGBUILD file should be displayed for edi aur_build_dir: null # defines a custom build directory for AUR packages (a null value will point to /tmp/bauh/arch (non-root user) or /tmp/bauh_root/arch (root user)). Default: null. aur_remove_build_dir: true # it defines if a package's generated build directory should be removed after the operation is finished (installation, upgrading, ...). Options: true, false (default: true). aur_build_only_chosen : true # some AUR packages have a common file definition declaring several packages to be built. When this property is 'true' only the package the user select to install will be built (unless its name is different from those declared in the PKGBUILD base). With a 'null' value a popup asking if the user wants to build all of them will be displayed. 'false' will build and install all packages. Default: true. -check_dependency_breakage: true # If, during the verification of the update requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. If A and B were selected to upgrade, and B would be upgrade to 2.0, then B would be excluded from the transaction. Default: true. +check_dependency_breakage: true # if, during the verification of the update requirements, specific versions of dependencies must also be checked. Example: package A depends on version 1.0 of B. If A and B were selected to upgrade, and B would be upgrade to 2.0, then B would be excluded from the transaction. Default: true. +suggest_unneeded_uninstall: false # if the dependencies apparently no longer necessary associated with the uninstalled packages should be suggested for uninstallation. When this property is enabled it automatically disables the property 'suggest_optdep_uninstall'. Default: false (to prevent new users from making mistakes) +suggest_optdep_uninstall: false # if the optional dependencies associated with uninstalled packages should be suggested for uninstallation. Only the optional dependencies that are not dependencies of other packages will be suggested. Default: false (to prevent new users from making mistakes) ``` - Required dependencies: - **pacman** diff --git a/bauh/commons/system.py b/bauh/commons/system.py index 08f4bc4a..2b130803 100644 --- a/bauh/commons/system.py +++ b/bauh/commons/system.py @@ -320,3 +320,8 @@ def check_enabled_services(*names: str) -> Dict[str, bool]: else: status = output.split('\n') 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) + return p.returncode, p.stdout.decode() diff --git a/bauh/gems/arch/config.py b/bauh/gems/arch/config.py index 402c0ce2..2fd89b1b 100644 --- a/bauh/gems/arch/config.py +++ b/bauh/gems/arch/config.py @@ -19,7 +19,9 @@ def read_config(update_file: bool = False) -> dict: 'aur_build_dir': None, 'aur_remove_build_dir': True, 'aur_build_only_chosen': True, - 'check_dependency_breakage': True} + 'check_dependency_breakage': True, + 'suggest_unneeded_uninstall': False, + 'suggest_optdep_uninstall': False} return read(CONFIG_FILE, template, update_file=update_file) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index a13ca7fb..eac27bed 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -39,7 +39,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 +from bauh.gems.arch.exceptions import PackageNotFoundException, PackageInHoldException from bauh.gems.arch.mapper import ArchDataMapper from bauh.gems.arch.model import ArchPackage from bauh.gems.arch.output import TransactionStatusHandler @@ -1140,7 +1140,7 @@ def _request_uninstall_confirmation(self, to_uninstall: Iterable[str], required: return True - def _request_unncessary_uninstall_confirmation(self, uninstalled: Iterable[str], unnecessary: Iterable[str], watcher: ProcessWatcher) -> Optional[List[str]]: + def _request_unncessary_uninstall_confirmation(self, unnecessary: Iterable[str], watcher: ProcessWatcher) -> Optional[Set[str]]: reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=False) for p in unnecessary] reqs_select = MultipleSelectComponent(options=reqs, default_options=set(reqs), label="", max_per_line=3 if len(reqs) > 9 else 1) @@ -1150,7 +1150,7 @@ def _request_unncessary_uninstall_confirmation(self, uninstalled: Iterable[str], deny_label=self.i18n['arch.uninstall.unnecessary.proceed'].capitalize(), confirmation_label=self.i18n['arch.uninstall.unnecessary.cancel'].capitalize(), window_cancel=False): - return reqs_select.get_selected_values() + return {*reqs_select.get_selected_values()} def _request_all_unncessary_uninstall_confirmation(self, pkgs: Iterable[str], context: TransactionContext): reqs = [InputOption(label=p, value=p, icon_path=get_icon_path(), read_only=True) for p in pkgs] @@ -1172,47 +1172,45 @@ def _uninstall(self, context: TransactionContext, names: Set[str], remove_unneed net_available = internet.is_available() if disk_loader else True - required_by = self.deps_analyser.map_all_required_by(names, set()) + hard_requirements = set() - if required_by: - target_provided = pacman.map_provided(pkgs={*names, *required_by}).keys() - - if target_provided: - required_by_deps = pacman.map_all_deps(required_by, only_installed=True) - - if required_by_deps: - all_provided = pacman.map_provided() - - for pkg, deps in required_by_deps.items(): - target_required_by = 0 - for dep in deps: - dep_split = pacman.RE_DEP_OPERATORS.split(dep) - if dep_split[0] in target_provided or dep_split[0] in required_by: - dep_providers = all_provided.get(dep_split[0]) - - if dep_providers: - target_required_by += 1 if not dep_providers.difference(target_provided) else 0 + for n in names: + try: + pkg_reqs = pacman.list_hard_requirements(n, self.logger) - if not target_required_by: - required_by.remove(pkg) + if pkg_reqs: + hard_requirements.update(pkg_reqs) + except PackageInHoldException: + context.watcher.show_message(title=self.i18n['error'].capitalize(), + body=self.i18n['arch.uninstall.error.hard_dep_in_hold'].format(bold(n)), + type_=MessageType.ERROR) + return False - self._update_progress(context, 50) + self._update_progress(context, 25) to_uninstall = set() to_uninstall.update(names) - if required_by: - to_uninstall.update(required_by) + if hard_requirements: + to_uninstall.update(hard_requirements) if not self._request_uninstall_confirmation(to_uninstall=names, - required=required_by, + required=hard_requirements, watcher=context.watcher): return False if remove_unneeded: - all_deps_map = pacman.map_all_deps(names=to_uninstall, only_installed=True) # retrieving the deps to check if they are still necessary + unnecessary_packages = pacman.list_post_uninstall_unneeded_packages(to_uninstall) + self.logger.info("Checking unnecessary optdeps") + + if context.config['suggest_optdep_uninstall']: + unnecessary_packages.update(self._list_opt_deps_with_no_hard_requirements(source_pkgs=to_uninstall)) + + self.logger.info("Packages no longer needed found: {}".format(len(unnecessary_packages))) else: - all_deps_map = None + unnecessary_packages = None + + self._update_progress(context, 50) if disk_loader and to_uninstall: # loading package instances in case the uninstall succeeds instances = self.read_installed(disk_loader=disk_loader, @@ -1234,60 +1232,50 @@ def _uninstall(self, context: TransactionContext, names: Set[str], remove_unneed self._update_progress(context, 70) - if all_deps_map: - context.watcher.change_substatus(self.i18n['arch.checking_unnecessary_deps']) - all_deps = set() - - all_provided = pacman.map_provided(remote=False) - for deps in all_deps_map.values(): - for dep in deps: - real_deps = all_provided.get(dep) - - if real_deps: - all_deps.update(real_deps) + if unnecessary_packages: + unnecessary_to_uninstall = self._request_unncessary_uninstall_confirmation(unnecessary=unnecessary_packages, + watcher=context.watcher) - if all_deps: - self.logger.info("Mapping dependencies required packages of uninstalled packages") - alldeps_reqs = pacman.map_required_by(all_deps) + if unnecessary_to_uninstall: + context.watcher.change_substatus(self.i18n['arch.checking_unnecessary_deps']) + unnecessary_requirements = set() - no_longer_needed = set() - if alldeps_reqs: - for dep, reqs in alldeps_reqs.items(): - if not reqs: - no_longer_needed.add(dep) + for pkg in unnecessary_to_uninstall: + try: + pkg_reqs = pacman.list_hard_requirements(pkg) - if no_longer_needed: - self.logger.info("{} packages no longer needed found".format(len(no_longer_needed))) - unnecessary_to_uninstall = self._request_unncessary_uninstall_confirmation(uninstalled=to_uninstall, - unnecessary=no_longer_needed, - watcher=context.watcher) + if pkg_reqs: + unnecessary_requirements.update(pkg_reqs) - if unnecessary_to_uninstall: - unnecessary_to_uninstall_deps = pacman.list_unnecessary_deps(unnecessary_to_uninstall, all_provided) - all_unnecessary_to_uninstall = {*unnecessary_to_uninstall, *unnecessary_to_uninstall_deps} + except PackageInHoldException: + context.watcher.show_message(title=self.i18n['warning'].capitalize(), + body=self.i18n['arch.uninstall.error.hard_dep_in_hold'].format(bold(p)), + type_=MessageType.WARNING) - if not unnecessary_to_uninstall_deps or self._request_all_unncessary_uninstall_confirmation(all_unnecessary_to_uninstall, context): + all_unnecessary_to_uninstall = {*unnecessary_to_uninstall, *unnecessary_requirements} - if disk_loader: # loading package instances in case the uninstall succeeds - unnecessary_instances = self.read_installed(disk_loader=disk_loader, - internet_available=net_available, - names=all_unnecessary_to_uninstall).installed - else: - unnecessary_instances = None + if not unnecessary_requirements or self._request_all_unncessary_uninstall_confirmation(all_unnecessary_to_uninstall, context): + if disk_loader: # loading package instances in case the uninstall succeeds + unnecessary_instances = self.read_installed(disk_loader=disk_loader, + internet_available=net_available, + names=all_unnecessary_to_uninstall).installed + else: + unnecessary_instances = None - unneded_uninstalled = self._uninstall_pkgs(all_unnecessary_to_uninstall, context.root_password, context.handler) + unneded_uninstalled = self._uninstall_pkgs(all_unnecessary_to_uninstall, context.root_password, context.handler) - if unneded_uninstalled: - to_uninstall.update(all_unnecessary_to_uninstall) + if unneded_uninstalled: + to_uninstall.update(all_unnecessary_to_uninstall) - if disk_loader and unnecessary_instances: # loading package instances in case the uninstall succeeds - for p in unnecessary_instances: - context.removed[p.name] = p - else: - self.logger.error("Could not uninstall some unnecessary packages") - context.watcher.print("Could not uninstall some unnecessary packages") + if disk_loader and unnecessary_instances: # loading package instances in case the uninstall succeeds + for p in unnecessary_instances: + context.removed[p.name] = p + else: + self.logger.error("Could not uninstall some unnecessary packages") + context.watcher.print("Could not uninstall some unnecessary packages") self._update_progress(context, 90) + if bool(context.config['clean_cached']): # cleaning old versions context.watcher.change_substatus(self.i18n['arch.uninstall.clean_cached.substatus']) if os.path.isdir('/var/cache/pacman/pkg'): @@ -1315,13 +1303,14 @@ def uninstall(self, pkg: ArchPackage, root_password: str, watcher: ProcessWatche return TransactionResult.fail() removed = {} + arch_config = read_config() success = self._uninstall(TransactionContext(change_progress=True, - arch_config=read_config(), + arch_config=arch_config, watcher=watcher, root_password=root_password, handler=handler, removed=removed), - remove_unneeded=True, + remove_unneeded=arch_config['suggest_unneeded_uninstall'], names={pkg.name}, disk_loader=disk_loader) # to be able to return all uninstalled packages if success: @@ -1545,7 +1534,7 @@ def _request_conflict_resolution(self, pkg: str, conflicting_pkg: str, context: if context.removed is None: context.removed = {} - res = self._uninstall(context=context, names={conflicting_pkg}, disk_loader=context.disk_loader) + res = self._uninstall(context=context, names={conflicting_pkg}, disk_loader=context.disk_loader, remove_unneeded=False) context.restabilish_progress() return res @@ -2568,16 +2557,27 @@ def launch(self, pkg: ArchPackage): def get_screenshots(self, pkg: SoftwarePackage) -> List[str]: pass - def _gen_bool_selector(self, id_: str, label_key: str, tooltip_key: str, value: bool, max_width: int, capitalize_label: bool = True) -> SingleSelectComponent: + def _gen_bool_selector(self, id_: str, label_key: str, tooltip_key: str, value: bool, max_width: int, + capitalize_label: bool = True, label_params: Optional[list] = None, tooltip_params: Optional[list] = None) -> SingleSelectComponent: opts = [InputOption(label=self.i18n['yes'].capitalize(), value=True), InputOption(label=self.i18n['no'].capitalize(), value=False)] - return SingleSelectComponent(label=self.i18n[label_key], + lb = self.i18n[label_key] + + if label_params: + lb = lb.format(*label_params) + + tip = self.i18n[tooltip_key] + + if tooltip_params: + tip = tip.format(*tooltip_params) + + return SingleSelectComponent(label=lb, options=opts, default_option=[o for o in opts if o.value == value][0], max_per_line=len(opts), type_=SelectViewType.RADIO, - tooltip=self.i18n[tooltip_key], + tooltip=tip, max_width=max_width, id_=id_, capitalize_label=capitalize_label) @@ -2638,6 +2638,17 @@ def get_settings(self, screen_width: int, screen_height: int) -> ViewComponent: tooltip_key='arch.config.clean_cache.tip', value=bool(local_config['clean_cached']), max_width=max_width), + self._gen_bool_selector(id_='suggest_unneeded_uninstall', + label_key='arch.config.suggest_unneeded_uninstall', + tooltip_params=['"{}"'.format(self.i18n['arch.config.suggest_optdep_uninstall'])], + tooltip_key='arch.config.suggest_unneeded_uninstall.tip', + value=bool(local_config['suggest_unneeded_uninstall']), + max_width=max_width), + self._gen_bool_selector(id_='suggest_optdep_uninstall', + label_key='arch.config.suggest_optdep_uninstall', + tooltip_key='arch.config.suggest_optdep_uninstall.tip', + value=bool(local_config['suggest_optdep_uninstall']), + max_width=max_width), self._gen_bool_selector(id_='ref_mirs', label_key='arch.config.refresh_mirrors', tooltip_key='arch.config.refresh_mirrors.tip', @@ -2707,6 +2718,8 @@ def save_settings(self, component: PanelComponent) -> Tuple[bool, Optional[List[ config['aur_build_dir'] = form_install.get_component('aur_build_dir').file_path config['aur_build_only_chosen'] = form_install.get_component('aur_build_only_chosen').get_selected() config['check_dependency_breakage'] = form_install.get_component('check_dependency_breakage').get_selected() + config['suggest_optdep_uninstall'] = form_install.get_component('suggest_optdep_uninstall').get_selected() + config['suggest_unneeded_uninstall'] = form_install.get_component('suggest_unneeded_uninstall').get_selected() if not config['aur_build_dir']: config['aur_build_dir'] = None @@ -3132,3 +3145,35 @@ def _gen_custom_pkgbuild_if_required(self, context: TransactionContext) -> Optio f.write(new_srcinfo) return custom_pkgbuild_path + + def _list_opt_deps_with_no_hard_requirements(self, source_pkgs: Set[str], installed_provided: Optional[Dict[str, Set[str]]] = None) -> Set[str]: + optdeps = set() + + for deps in pacman.map_optional_deps(names=source_pkgs, remote=False).values(): + optdeps.update(deps.keys()) + + res = set() + if optdeps: + all_provided = pacman.map_provided() if not installed_provided else installed_provided + + real_optdeps = set() + for o in optdeps: + dep_providers = all_provided.get(o) + + if dep_providers: + for p in dep_providers: + if p not in source_pkgs: + real_optdeps.add(p) + + if real_optdeps: + for p in real_optdeps: + try: + reqs = pacman.list_hard_requirements(p, self.logger) + + if reqs is not None and (not reqs or reqs.issubset(source_pkgs)): + res.add(p) + except PackageInHoldException: + self.logger.warning("There is a requirement in hold for opt dep '{}'".format(p)) + continue + + return res diff --git a/bauh/gems/arch/exceptions.py b/bauh/gems/arch/exceptions.py index d868cb15..a5c29584 100644 --- a/bauh/gems/arch/exceptions.py +++ b/bauh/gems/arch/exceptions.py @@ -1,5 +1,13 @@ +from typing import Optional + class PackageNotFoundException(Exception): def __init__(self, name: str): self.name = name + + +class PackageInHoldException(Exception): + + def __init__(self, name: Optional[str] = None): + self.name = name diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 5c042d2d..18cd8000 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -1,12 +1,15 @@ +import logging import os import re from threading import Thread -from typing import List, Set, Tuple, Dict, Iterable +from typing import List, Set, Tuple, Dict, Iterable, Optional + +from colorama import Fore from bauh.commons import system from bauh.commons.system import run_cmd, new_subprocess, new_root_subprocess, SystemProcess, SimpleProcess from bauh.commons.util import size_to_byte -from bauh.gems.arch.exceptions import PackageNotFoundException +from bauh.gems.arch.exceptions import PackageNotFoundException, PackageInHoldException RE_DEPS = re.compile(r'[\w\-_]+:[\s\w_\-\.]+\s+\[\w+\]') RE_OPTDEPS = re.compile(r'[\w\._\-]+\s*:') @@ -1128,3 +1131,42 @@ def get_packages_to_sync_first() -> Set[str]: def is_snapd_installed() -> bool: return bool(run_cmd('pacman -Qq snapd', print_error=False)) + + +def list_hard_requirements(name: str, logger: Optional[logging.Logger] = None) -> Optional[Set[str]]: + code, output = system.execute('pacman -Rc {} --print-format=%n'.format(name), shell=True) + + if code != 0: + if 'HoldPkg' in output: + raise PackageInHoldException() + elif 'target not found' in output: + raise PackageNotFoundException(name) + elif logger: + logger.error("Unexpected error while listing hard requirements of: {}".format(name)) + print('{}{}{}'.format(Fore.RED, output, Fore.RESET)) + elif output: + reqs = set() + + for line in output.split('\n'): + if line: + line_strip = line.strip() + + if line_strip and line_strip != name: + reqs.add(line_strip) + + return reqs + + +def list_post_uninstall_unneeded_packages(names: Set[str]) -> Set[str]: + output = run_cmd('pacman -Rss {} --print-format=%n'.format(' '.join(names)), print_error=False) + + reqs = set() + if output: + for line in output.split('\n'): + if line: + line_strip = line.strip() + + if line_strip and line_strip not in names: + reqs.add(line_strip) + + return reqs diff --git a/bauh/gems/arch/resources/locale/ca b/bauh/gems/arch/resources/locale/ca index 5d6c2a5e..410cd4c1 100644 --- a/bauh/gems/arch/resources/locale/ca +++ b/bauh/gems/arch/resources/locale/ca @@ -56,6 +56,10 @@ arch.config.refresh_mirrors=Refresh mirrors on startup arch.config.refresh_mirrors.tip=Refresh the package mirrors once a day on startup ( or after a device reboot ) arch.config.repos=Repositories packages arch.config.repos.tip=It allows to manage packages from the repositories set +arch.config.suggest_optdep_uninstall=Uninstall optional dependencies +arch.config.suggest_optdep_uninstall.tip=If the optional dependencies associated with uninstalled packages should be suggested for uninstallation. Only the optional dependencies that are not dependencies of other packages will be suggested. +arch.config.suggest_unneeded_uninstall=Uninstall unneeded dependencies +arch.config.suggest_unneeded_uninstall.tip=If the dependencies apparently no longer necessary associated with the uninstalled packages should be suggested for uninstallation. When this property is enabled it automatically disables the property {}. arch.config.sync_dbs=Synchronize packages databases arch.config.sync_dbs.tip=Synchronizes the package databases once a day before the first package installation, upgrade or downgrade. This option helps to prevent errors during these operations. arch.config.sync_dbs_start.tip=Synchronizes the package databases during the initialization once a day @@ -213,6 +217,7 @@ arch.task.sync_sb.status=Actualitzen {} arch.uncompressing.package=S’està descomprimint el paquet arch.uninstall.clean_cached.error=No s'ha pogut eliminar {} versions antigues que es troba al disc arch.uninstall.clean_cached.substatus=Eliminació de versions antigues del disc +arch.uninstall.error.hard_dep_in_hold=It is not possible to uninstall {} because one of its dependencies is marked as "InHold" arch.uninstall.required_by=The {} packages listed below depend on {} to work properly arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled diff --git a/bauh/gems/arch/resources/locale/de b/bauh/gems/arch/resources/locale/de index 827fcaf0..55120a11 100644 --- a/bauh/gems/arch/resources/locale/de +++ b/bauh/gems/arch/resources/locale/de @@ -56,6 +56,10 @@ arch.config.refresh_mirrors=Refresh mirrors on startup arch.config.refresh_mirrors.tip=Refresh the package mirrors once a day on startup ( or after a device reboot ) arch.config.repos=Repositories packages arch.config.repos.tip=It allows to manage packages from the repositories set +arch.config.suggest_optdep_uninstall=Uninstall optional dependencies +arch.config.suggest_optdep_uninstall.tip=If the optional dependencies associated with uninstalled packages should be suggested for uninstallation. Only the optional dependencies that are not dependencies of other packages will be suggested. +arch.config.suggest_unneeded_uninstall=Uninstall unneeded dependencies +arch.config.suggest_unneeded_uninstall.tip=If the dependencies apparently no longer necessary associated with the uninstalled packages should be suggested for uninstallation. When this property is enabled it automatically disables the property {}. arch.config.sync_dbs=Synchronize packages databases arch.config.sync_dbs.tip=Synchronizes the package databases once a day before the first package installation, upgrade or downgrade. This option helps to prevent errors during these operations. arch.config.sync_dbs_start.tip=Synchronizes the package databases during the initialization once a day @@ -213,6 +217,7 @@ arch.task.sync_sb.status=Updating {} arch.uncompressing.package=Paket entpacken arch.uninstall.clean_cached.error=It was not possible to remove old {} versions found on disk arch.uninstall.clean_cached.substatus=Removing old versions from disk +arch.uninstall.error.hard_dep_in_hold=It is not possible to uninstall {} because one of its dependencies is marked as "InHold" arch.uninstall.required_by=The {} packages listed below depend on {} to work properly arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled diff --git a/bauh/gems/arch/resources/locale/en b/bauh/gems/arch/resources/locale/en index c85e103a..e8a940b6 100644 --- a/bauh/gems/arch/resources/locale/en +++ b/bauh/gems/arch/resources/locale/en @@ -57,6 +57,10 @@ arch.config.refresh_mirrors=Refresh mirrors on startup arch.config.refresh_mirrors.tip=Refresh the package mirrors once a day on startup ( or after a device reboot ) arch.config.repos=Repositories packages arch.config.repos.tip=It allows to manage packages from the repositories set +arch.config.suggest_optdep_uninstall=Uninstall optional dependencies +arch.config.suggest_optdep_uninstall.tip=If the optional dependencies associated with uninstalled packages should be suggested for uninstallation. Only the optional dependencies that are not dependencies of other packages will be suggested. +arch.config.suggest_unneeded_uninstall=Uninstall unneeded dependencies +arch.config.suggest_unneeded_uninstall.tip=If the dependencies apparently no longer necessary associated with the uninstalled packages should be suggested for uninstallation. When this property is enabled it automatically disables the property {}. arch.config.sync_dbs=Synchronize packages databases arch.config.sync_dbs.tip=Synchronizes the package databases once a day before the first package installation, upgrade or downgrade. This option helps to prevent errors during these operations. arch.config.sync_dbs_start.tip=Synchronizes the package databases during the initialization once a day @@ -214,6 +218,7 @@ arch.task.sync_sb.status=Updating {} arch.uncompressing.package=Uncompressing the package arch.uninstall.clean_cached.error=It was not possible to remove old {} versions found on disk arch.uninstall.clean_cached.substatus=Removing old versions from disk +arch.uninstall.error.hard_dep_in_hold=It is not possible to uninstall {} because one of its dependencies is marked as "InHold" arch.uninstall.required_by=The {} packages listed below depend on {} to work properly arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled diff --git a/bauh/gems/arch/resources/locale/es b/bauh/gems/arch/resources/locale/es index 32127891..6eb584b1 100644 --- a/bauh/gems/arch/resources/locale/es +++ b/bauh/gems/arch/resources/locale/es @@ -56,6 +56,10 @@ arch.config.refresh_mirrors=Actualizar espejos al iniciar arch.config.refresh_mirrors.tip=Actualiza los espejos de paquetes una vez al día al iniciar ( o después de reiniciar el dispositivo ) arch.config.repos=Paquetes de repositorios arch.config.repos.tip=Permite gestionar paquetes de los repositorios configurados +arch.config.suggest_optdep_uninstall=Desinstalar dependencias opcionales +arch.config.suggest_optdep_uninstall.tip=Si las dependencias opcionales asociadas con los paquetes desinstalados deben se sugeridas para desinstalación. Solo se sugerirán las dependencias opcionales que no sean dependencias de otros paquetes. +arch.config.suggest_unneeded_uninstall=Desinstalar las dependencias innecesarias +arch.config.suggest_unneeded_uninstall.tip=Si las dependencias aparentemente ya no necesarias asociadas con los paquetes desinstalados deben ser sugeridas para desinstalación. Cuando esta propiedad está habilitada, automáticamente deshabilita la propiedad {}. arch.config.sync_dbs=Sincronizar las bases de paquetes arch.config.sync_dbs.tip=Sincroniza las bases de paquetes una vez al día antes de la primera instalación, actualización o reversión de paquete. Esta opción ayuda a prevenir errores durante estas operaciones. arch.config.sync_dbs_start.tip=Sincroniza las bases de paquetes durante la inicialización una vez al día @@ -213,6 +217,7 @@ arch.task.sync_sb.status=Actualizando {} arch.uncompressing.package=Descomprimindo el paquete arch.uninstall.clean_cached.error=No fue posible eliminar versiones antiguas de {} encontradas en disco arch.uninstall.clean_cached.substatus=Eliminando versiones antiguas del disco +arch.uninstall.error.hard_dep_in_hold=No es posible desinstalar {} porque una de sus dependencias está bloqueada (InHold) arch.uninstall.required_by=Los {} paquetes enumerados abajo dependen de {} para funcionar correctamente arch.uninstall.required_by.advice=Es necesario desinstalarlos también para continuar arch.uninstall.unnecessary.all=Los {} seguintes paquetes serán desinstalados diff --git a/bauh/gems/arch/resources/locale/it b/bauh/gems/arch/resources/locale/it index d576ef8d..003d5536 100644 --- a/bauh/gems/arch/resources/locale/it +++ b/bauh/gems/arch/resources/locale/it @@ -56,6 +56,10 @@ arch.config.refresh_mirrors=Refresh mirrors on startup arch.config.refresh_mirrors.tip=Refresh the package mirrors once a day on startup ( or after a device reboot ) arch.config.repos=Repositories packages arch.config.repos.tip=It allows to manage packages from the repositories set +arch.config.suggest_optdep_uninstall=Uninstall optional dependencies +arch.config.suggest_optdep_uninstall.tip=If the optional dependencies associated with uninstalled packages should be suggested for uninstallation. Only the optional dependencies that are not dependencies of other packages will be suggested. +arch.config.suggest_unneeded_uninstall=Uninstall unneeded dependencies +arch.config.suggest_unneeded_uninstall.tip=If the dependencies apparently no longer necessary associated with the uninstalled packages should be suggested for uninstallation. When this property is enabled it automatically disables the property {}. arch.config.sync_dbs=Synchronize packages databases arch.config.sync_dbs.tip=Synchronizes the package databases once a day before the first package installation, upgrade or downgrade. This option helps to prevent errors during these operations. arch.config.sync_dbs_start.tip=Synchronizes the package databases during the initialization once a day @@ -213,6 +217,7 @@ arch.task.sync_sb.status=Aggiornando {} arch.uncompressing.package=Non comprimere il pacchetto arch.uninstall.clean_cached.error=Non è stato possibile rimuovere le vecchie {} versioni trovate sul disco arch.uninstall.clean_cached.substatus=Rimozione di versioni precedenti dal disco +arch.uninstall.error.hard_dep_in_hold=It is not possible to uninstall {} because one of its dependencies is marked as "InHold" arch.uninstall.required_by=The {} packages listed below depend on {} to work properly arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled diff --git a/bauh/gems/arch/resources/locale/pt b/bauh/gems/arch/resources/locale/pt index d1ef855f..4b7e43fd 100644 --- a/bauh/gems/arch/resources/locale/pt +++ b/bauh/gems/arch/resources/locale/pt @@ -56,6 +56,10 @@ arch.config.refresh_mirrors=Atualizar espelhos ao iniciar arch.config.refresh_mirrors.tip=Atualiza os espelhos de pacotes uma vez ao dia na inicialização arch.config.repos=Pacotes de repositórios arch.config.repos.tip=Permite gerenciar pacotes dos repositórisos +arch.config.suggest_optdep_uninstall=Desinstalar dependências opcionais +arch.config.suggest_optdep_uninstall.tip=Se as dependências opcionais associadas a pacotes desinstalados devem ser sugeridas para desinstalação. Somente as dependências opcionais que não sejam dependências de outros pacotes serão sugeridas. +arch.config.suggest_unneeded_uninstall=Desinstalar dependências desnecessárias +arch.config.suggest_unneeded_uninstall.tip=Se as dependências aparentemente não mais necessárias associadas aos pacotes desinstalados devem ser sugeridas para desinstalação. Quando essa proprieda está habilitada automaticamente desabilita a propriedade {}. arch.config.sync_dbs=Sincronizar bases de pacotes arch.config.sync_dbs.tip=Sincroniza as bases de pacotes uma vez ao dia antes da primeira instalação, atualização ou reversão de pacote. Essa opção ajuda a evitar erros durante essa operações. arch.config.sync_dbs_start.tip=Sincroniza as bases de pacotes durante a inicialização uma vez ao dia @@ -213,6 +217,7 @@ arch.task.sync_sb.status=Atualizando {} arch.uncompressing.package=Descompactando o pacote arch.uninstall.clean_cached.error=Não foi possível remover versões antigas de {} encontradas em disco arch.uninstall.clean_cached.substatus=Removendo versões antigas do disco +arch.uninstall.error.hard_dep_in_hold=Não é possível desinstalar {} pois uma de suas dependências está bloqueada (InHold) arch.uninstall.required_by=Os {} pacotes listados abaixo dependem de {} para funcionar corretamente arch.uninstall.required_by.advice=Para prosseguir será necessário desinstá-los também arch.uninstall.unnecessary.all=Os seguintes {} pacotes serão desinstalados diff --git a/bauh/gems/arch/resources/locale/ru b/bauh/gems/arch/resources/locale/ru index 751f10b1..035ce7cb 100644 --- a/bauh/gems/arch/resources/locale/ru +++ b/bauh/gems/arch/resources/locale/ru @@ -56,6 +56,10 @@ arch.config.refresh_mirrors=Обновить зеркала при запуск arch.config.refresh_mirrors.tip=Обновляйте зеркала пакета один раз в день при запуске (или после перезагрузки устройства) arch.config.repos=Пакеты репозиториев arch.config.repos.tip=Он позволяет управлять пакетами из набора репозиториев +arch.config.suggest_optdep_uninstall=Uninstall optional dependencies +arch.config.suggest_optdep_uninstall.tip=If the optional dependencies associated with uninstalled packages should be suggested for uninstallation. Only the optional dependencies that are not dependencies of other packages will be suggested. +arch.config.suggest_unneeded_uninstall=Uninstall unneeded dependencies +arch.config.suggest_unneeded_uninstall.tip=If the dependencies apparently no longer necessary associated with the uninstalled packages should be suggested for uninstallation. When this property is enabled it automatically disables the property {}. arch.config.sync_dbs=Синхронизация баз данных пакетов arch.config.sync_dbs.tip=Синхронизируйте базы данных пакетов один раз в день перед первой установкой, обновлением или понижением версии пакета. Этот параметр помогает предотвратить ошибки во время этих операций. arch.config.sync_dbs_start.tip=Синхронизирует базы данных пакетов во время инициализации один раз в день @@ -213,6 +217,7 @@ arch.task.sync_sb.status=Updating {} arch.uncompressing.package=Распаковка пакета arch.uninstall.clean_cached.error=It was not possible to remove old {} versions found on disk arch.uninstall.clean_cached.substatus=Removing old versions from disk +arch.uninstall.error.hard_dep_in_hold=It is not possible to uninstall {} because one of its dependencies is marked as "InHold" arch.uninstall.required_by=The {} packages listed below depend on {} to work properly arch.uninstall.required_by.advice=It is necessary to uninstall them as well to proceed arch.uninstall.unnecessary.all=The following {} packages will be uninstalled diff --git a/bauh/gems/arch/resources/locale/tr b/bauh/gems/arch/resources/locale/tr index 023bea68..a6a12a76 100644 --- a/bauh/gems/arch/resources/locale/tr +++ b/bauh/gems/arch/resources/locale/tr @@ -56,6 +56,10 @@ arch.config.refresh_mirrors=Başlangıçta yansıları yenile arch.config.refresh_mirrors.tip=Paket yansılarını başlangıçta günde bir kez (veya cihaz yeniden başlatıldıktan sonra) yenileyin arch.config.repos=Depo paketleri arch.config.repos.tip=Paketleri depolardan yönetmeye izin verir +arch.config.suggest_optdep_uninstall=Uninstall optional dependencies +arch.config.suggest_optdep_uninstall.tip=If the optional dependencies associated with uninstalled packages should be suggested for uninstallation. Only the optional dependencies that are not dependencies of other packages will be suggested. +arch.config.suggest_unneeded_uninstall=Uninstall unneeded dependencies +arch.config.suggest_unneeded_uninstall.tip=If the dependencies apparently no longer necessary associated with the uninstalled packages should be suggested for uninstallation. When this property is enabled it automatically disables the property {}. arch.config.sync_dbs=Paket veritabanlarını senkronize et arch.config.sync_dbs.tip=İlk veritabanını kurmadan, yükseltmeden veya indirmeden önce paket veritabanlarını günde bir kez senkronize eder. Bu seçenek, bu işlemler sırasında hataların önlenmesine yardımcı olur. arch.config.sync_dbs_start.tip=Başlatma sırasında paket veritabanlarını günde bir kez senkronize eder @@ -213,6 +217,7 @@ arch.task.sync_sb.status={} güncelleniyor arch.uncompressing.package=Paket arşivden çıkarılıyor arch.uninstall.clean_cached.error=Diskte bulunan eski {} sürümleri kaldırılamadı arch.uninstall.clean_cached.substatus=Eski sürümler diskten kaldırılıyor +arch.uninstall.error.hard_dep_in_hold=It is not possible to uninstall {} because one of its dependencies is marked as "InHold" arch.uninstall.required_by=The {} packages listed below depend on {} to work properly arch.uninstall.required_by.advice=Devam etmek için bunları da kaldırmak gerekir arch.uninstall.unnecessary.all=Aşağıdaki {} paketler kaldırılacak From c18eab8d6299f3a24b7a0d991c2e65a77649f879 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 11:22:05 -0300 Subject: [PATCH 87/99] [ui] improvement -> 'name' filter now holds for 3 seconds instead of 2 before being applied --- CHANGELOG.md | 1 + bauh/view/qt/components.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63b85d57..04cfd76e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -89,6 +89,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - UI - faster initialization dialog: improved the way it checks for finished tasks + - 'name' filter now holds for 3 seconds instead of 2 before being applied - minor improvements ### Fixes diff --git a/bauh/view/qt/components.py b/bauh/view/qt/components.py index 1e827cba..75b6106e 100644 --- a/bauh/view/qt/components.py +++ b/bauh/view/qt/components.py @@ -659,7 +659,7 @@ def keyPressEvent(self, event): if self.typing.isActive(): return - self.typing.start(2000) + self.typing.start(3000) def get_text(self): return self.last_text From cd7984bc82906e00abe717b64b7e0d5bc54b5e34 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 11:24:25 -0300 Subject: [PATCH 88/99] [ui] improvement -> removing spaces from package type parentheses --- bauh/view/qt/view_model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/view/qt/view_model.py b/bauh/view/qt/view_model.py index 8c802b33..b3dd646c 100644 --- a/bauh/view/qt/view_model.py +++ b/bauh/view/qt/view_model.py @@ -33,7 +33,7 @@ def update_model(self, model: SoftwarePackage): self.status = PackageViewStatus.LOADING if model.status == PackageStatus.LOADING_DATA else PackageViewStatus.READY def __repr__(self): - return '{} ( {} )'.format(self.model.name, self.get_type_label()) + return '{} ({})'.format(self.model.name, self.get_type_label()) def __eq__(self, other): if isinstance(other, PackageView): From 231c77c7daf9fba53e07ee68f857ac65bb020e46 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 11:39:28 -0300 Subject: [PATCH 89/99] [arch] fix -> wrong substatus when checking optdeps conflicts --- bauh/gems/arch/controller.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index eac27bed..a60472ac 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -1564,7 +1564,11 @@ def _install_deps(self, context: TransactionContext, deps: List[Tuple[str, str]] if repo_deps: repo_dep_names = [d[0] for d in repo_deps] - context.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(context.name))) + + if context.dependency: + context.watcher.change_substatus(self.i18n['arch.substatus.conflicts']) + else: + context.watcher.change_substatus(self.i18n['arch.checking.conflicts'].format(bold(context.name))) all_provided = context.get_provided_map() @@ -1772,6 +1776,7 @@ def _build(self, context: TransactionContext) -> bool: context.watcher.change_substatus(self.i18n['arch.optdeps.checking'].format(bold(context.name))) self._update_progress(context, 100) + if self._install_optdeps(context): return True @@ -2023,6 +2028,7 @@ def _install_optdeps(self, context: TransactionContext) -> bool: old_progress_behavior = context.change_progress context.change_progress = True + context.dependency = True deps_not_installed = self._install_deps(context, sorted_deps) context.change_progress = old_progress_behavior From 6095c1a934b7cbed9882e248e1d5c148d241c375 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 12:24:41 -0300 Subject: [PATCH 90/99] [arch] fix -> 'conflict with' field not being displayed as a list on the info window --- bauh/gems/arch/pacman.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bauh/gems/arch/pacman.py b/bauh/gems/arch/pacman.py index 18cd8000..71b2add1 100644 --- a/bauh/gems/arch/pacman.py +++ b/bauh/gems/arch/pacman.py @@ -70,7 +70,7 @@ def get_info_list(pkg_name: str, remote: bool = False) -> List[tuple]: def get_info_dict(pkg_name: str, remote: bool = False) -> dict: - list_attrs = {'depends on', 'required by'} + list_attrs = {'depends on', 'required by', 'conflicts with'} info_list = get_info_list(pkg_name, remote) if info_list: From 1becd281fab45254c265e3ab6f3a6ee5209bcc6c Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 14:52:18 -0300 Subject: [PATCH 91/99] [arch] fix -> AUR: not displaying all available actions per package after a fresh install --- bauh/gems/arch/controller.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/bauh/gems/arch/controller.py b/bauh/gems/arch/controller.py index a60472ac..478e7c9f 100644 --- a/bauh/gems/arch/controller.py +++ b/bauh/gems/arch/controller.py @@ -2348,6 +2348,7 @@ def install(self, pkg: ArchPackage, root_password: str, disk_loader: DiskCacheLo if res: pkg.name = install_context.name # changes the package name in case the PKGBUILD was edited + if os.path.exists(pkg.get_disk_data_path()): with open(pkg.get_disk_data_path()) as f: data = f.read() @@ -2376,14 +2377,20 @@ def install(self, pkg: ArchPackage, root_password: str, disk_loader: DiskCacheLo names=installed_to_load, internet_available=True).installed - installed.extend(installed_loaded) + if installed_loaded: + installed.extend(installed_loaded) - if len(installed_loaded) + 1 != len(install_context.installed): - missing = ','.join({p for p in installed_loaded if p.name not in install_context.installed}) - self.logger.warning("Could not load all installed packages. Missing: {}".format(missing)) + if len(installed_loaded) + 1 != len(install_context.installed): + missing = ','.join({p for p in installed_loaded if p.name not in install_context.installed}) + self.logger.warning("Could not load all installed packages. Missing: {}".format(missing)) removed = [*install_context.removed.values()] if install_context.removed else [] + if installed: + downgrade_enabled = self.is_downgrade_enabled() + for p in installed: + p.downgrade_enabled = downgrade_enabled + return TransactionResult(success=res, installed=installed, removed=removed) def _install_from_repository(self, context: TransactionContext) -> bool: From e4e153c55e13493e18b67758e043a2bbeaac43dd Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 15:44:53 -0300 Subject: [PATCH 92/99] [snap] fix -> returning success when the user declines the Snap channel change --- bauh/gems/snap/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bauh/gems/snap/controller.py b/bauh/gems/snap/controller.py index f0b9c471..397f709c 100644 --- a/bauh/gems/snap/controller.py +++ b/bauh/gems/snap/controller.py @@ -274,13 +274,13 @@ def change_channel(self, pkg: SnapApplication, root_password: str, watcher: Proc if not channel: watcher.show_message(title=self.i18n['snap.action.channel.label'], body=self.i18n['snap.action.channel.error.no_channel']) - return True + return False return ProcessHandler(watcher).handle_simple(snap.refresh_and_stream(app_name=pkg.name, root_password=root_password, channel=channel))[0] except: - return True + return False def _start_category_task(self, task_man: TaskManager): if task_man: From e09e47316af4c670040b15cbb67ea8ceeb132b97 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 15:58:41 -0300 Subject: [PATCH 93/99] [flatpak] improvement -> removing spaces from some i18n parentheses --- bauh/gems/flatpak/resources/locale/ca | 4 ++-- bauh/gems/flatpak/resources/locale/de | 2 +- bauh/gems/flatpak/resources/locale/en | 4 ++-- bauh/gems/flatpak/resources/locale/es | 4 ++-- bauh/gems/flatpak/resources/locale/it | 4 ++-- bauh/gems/flatpak/resources/locale/pt | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/bauh/gems/flatpak/resources/locale/ca b/bauh/gems/flatpak/resources/locale/ca index e7cc6745..41c10420 100644 --- a/bauh/gems/flatpak/resources/locale/ca +++ b/bauh/gems/flatpak/resources/locale/ca @@ -46,11 +46,11 @@ flatpak.info.translateurl=Traducció flatpak.info.type=tipus flatpak.info.version=versió flatpak.install.bad_install_level.body=Valor invàlid per a {field} al fitxer de configuració {file} -flatpak.install.install_level.body=S'ha d'instal·lar {} per a tots els usuaris del dispositiu ( sistema ) ? +flatpak.install.install_level.body=S'ha d'instal·lar {} per a tots els usuaris del dispositiu (sistema) ? flatpak.install.install_level.title=Tipus d'instal·lació flatpak.install.ref_choose.title=Multiple references flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=Si no voleu utilitzar aplicacions Flatpak, desmarqueu {} a {} flatpak.notification.no_remotes=No hi ha dipòsits («remotes») del Flatpak configurats. No podreu cercar aplicacions Flatpak. -flatpak.remotes.system_flathub.error=No s'ha pogut afegir Flathub com a dipòsit del sistema ( remote ) +flatpak.remotes.system_flathub.error=No s'ha pogut afegir Flathub com a dipòsit del sistema (remote) gem.flatpak.info=Aplicacions disponibles a Flathub i altres repositoris configurats al vostre sistema \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/de b/bauh/gems/flatpak/resources/locale/de index 59bdf7f2..afd6ce47 100644 --- a/bauh/gems/flatpak/resources/locale/de +++ b/bauh/gems/flatpak/resources/locale/de @@ -46,7 +46,7 @@ flatpak.info.translateurl=Übersetzungs-URL flatpak.info.type=Typ flatpak.info.version=Version flatpak.install.bad_install_level.body=Ungültiger Wert für {field} in der Konfigurationsdatei {file} -flatpak.install.install_level.body=Sollte {} für alle Gerätebenutzer installiert werden ( system ) ? +flatpak.install.install_level.body=Sollte {} für alle Gerätebenutzer installiert werden (system) ? flatpak.install.install_level.title=Installationstyp flatpak.install.ref_choose.title=Multiple references flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: diff --git a/bauh/gems/flatpak/resources/locale/en b/bauh/gems/flatpak/resources/locale/en index 3e1c7a16..726d6214 100644 --- a/bauh/gems/flatpak/resources/locale/en +++ b/bauh/gems/flatpak/resources/locale/en @@ -46,11 +46,11 @@ flatpak.info.translateurl=translate url flatpak.info.type=type flatpak.info.version=version flatpak.install.bad_install_level.body=Invalid value for {field} in the configuration file {file} -flatpak.install.install_level.body=Should {} be installed for all the device users ( system ) ? +flatpak.install.install_level.body=Should {} be installed for all the device users (system) ? flatpak.install.install_level.title=Installation type flatpak.install.ref_choose.title=Multiple references flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=If you do not want to use Flatpak applications, uncheck {} in {} flatpak.notification.no_remotes=No Flatpak remotes set. It will not be possible to search for Flatpak apps. -flatpak.remotes.system_flathub.error=It was not possible to add Flathub as a system repository ( remote ) +flatpak.remotes.system_flathub.error=It was not possible to add Flathub as a system repository (remote) gem.flatpak.info=Applications available on Flathub and other repositories configured on your system \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/es b/bauh/gems/flatpak/resources/locale/es index e73cb12a..24e88dff 100644 --- a/bauh/gems/flatpak/resources/locale/es +++ b/bauh/gems/flatpak/resources/locale/es @@ -46,11 +46,11 @@ flatpak.info.translateurl=Traducción flatpak.info.type=tipo flatpak.info.version=versión flatpak.install.bad_install_level.body=Valor inválido para {field} en el archivo de configuración {file} -flatpak.install.install_level.body=¿Debería {} estar instalado para todos los usuarios del dispositivo ( sistema )? +flatpak.install.install_level.body=¿Debería {} estar instalado para todos los usuarios del dispositivo (sistema)? flatpak.install.install_level.title=Tipo de instalación flatpak.install.ref_choose.title=Varias referencias flatpak.install.ref_choose.body=Hay varias referencias para {}. Seleccione una para proceder: flatpak.notification.disable=Si no desea usar aplicativos Flatpak, desmarque {} en {} flatpak.notification.no_remotes=No hay repositorios (remotes) Flatpak configurados. No será posible buscar aplicativos Flatpak. -flatpak.remotes.system_flathub.error=No fue posible agregar Flathub como repositorio del sistema ( remote ) +flatpak.remotes.system_flathub.error=No fue posible agregar Flathub como repositorio del sistema (remote) gem.flatpak.info=Aplicaciones disponibles en Flathub y otros repositorios configurados en su sistema \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/it b/bauh/gems/flatpak/resources/locale/it index a735febc..76ae88d7 100644 --- a/bauh/gems/flatpak/resources/locale/it +++ b/bauh/gems/flatpak/resources/locale/it @@ -46,11 +46,11 @@ flatpak.info.translateurl=traduci url flatpak.info.type=type flatpak.info.version=version flatpak.install.bad_install_level.body=Valore non valido per {field} nel file di configurazione {file} -flatpak.install.install_level.body={} deve essere installato per tutti gli utenti del dispositivo ( sistema ) ? +flatpak.install.install_level.body={} deve essere installato per tutti gli utenti del dispositivo (sistema) ? flatpak.install.install_level.title=Tipo di installazione flatpak.install.ref_choose.title=Multiple references flatpak.install.ref_choose.body=There are multiple references for {}. Select one to proceed: flatpak.notification.disable=Ise non si desidera utilizzare le applicazioni Flatpak, deselezionare {} in {} flatpak.notification.no_remotes=Nessun set remoti Flatpak. Non sarà possibile cercare app Flatpak. -flatpak.remotes.system_flathub.error=Non è stato possibile aggiungere Flathub come repository di sistema ( remote ) +flatpak.remotes.system_flathub.error=Non è stato possibile aggiungere Flathub come repository di sistema (remote) gem.flatpak.info=Applicazioni disponibili su Flathub e altri repository configurati sul tuo sistema \ No newline at end of file diff --git a/bauh/gems/flatpak/resources/locale/pt b/bauh/gems/flatpak/resources/locale/pt index 694afd97..724bcb8a 100644 --- a/bauh/gems/flatpak/resources/locale/pt +++ b/bauh/gems/flatpak/resources/locale/pt @@ -46,11 +46,11 @@ flatpak.info.translateurl=tradução flatpak.info.type=tipo flatpak.info.version=versão flatpak.install.bad_install_level.body=Valor inválido para {field} no arquivo de configuração {file} -flatpak.install.install_level.body={} deve ser instalado para todos os usuários desse dispositivo ( sistema ) ? +flatpak.install.install_level.body={} deve ser instalado para todos os usuários desse dispositivo (sistema) ? flatpak.install.install_level.title=Tipo de instalação flatpak.install.ref_choose.title=Múltiplas referências flatpak.install.ref_choose.body=Existem múltiplas referências para {}. Selecione uma para continuar: flatpak.notification.disable=Se não deseja usar aplicativos Flatpak, desmarque {} em {} flatpak.notification.no_remotes=Não há repositórios (remotes) Flatpak configurados. Não será possível buscar aplicativos Flatpak. -flatpak.remotes.system_flathub.error=Não foi possível adicionar o Flathub como um repositório do sistema ( remote ) +flatpak.remotes.system_flathub.error=Não foi possível adicionar o Flathub como um repositório do sistema (remote) gem.flatpak.info=Aplicativos disponíveis no Flathub e outros repositórios configurados no seu sistema \ No newline at end of file From 935f731b7300379b2150e89ba241fe59f4ebb6af Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 16:38:14 -0300 Subject: [PATCH 94/99] [ui] fix -> not hiding the console when a new search is done --- bauh/view/qt/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 5ff7b61d..6823bcc1 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -1263,6 +1263,7 @@ def _begin_search(self, word, action_id: int = None): def search(self): word = self.inp_search.text().strip() if word: + self._handle_console(False) self._begin_search(word, action_id=ACTION_SEARCH) self.comp_manager.set_components_visible(False) self.thread_search.word = word From a44a28c8bc87371e6a94f1f56391934634ad4a48 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 16:41:01 -0300 Subject: [PATCH 95/99] [i18n] improvement -> trim: removing spaces from parentheses --- bauh/view/resources/locale/ca | 2 +- bauh/view/resources/locale/de | 2 +- bauh/view/resources/locale/en | 2 +- bauh/view/resources/locale/es | 2 +- bauh/view/resources/locale/it | 2 +- bauh/view/resources/locale/pt | 2 +- bauh/view/resources/locale/ru | 2 +- bauh/view/resources/locale/tr | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bauh/view/resources/locale/ca b/bauh/view/resources/locale/ca index 5880b54a..c16279f1 100644 --- a/bauh/view/resources/locale/ca +++ b/bauh/view/resources/locale/ca @@ -55,7 +55,7 @@ action.backups=Backups action.backups.status=Opening backups tool action.backups.tool_error=It was not possible to open the backups tool ({}). action.cancelled=l’usuari ha cancel·lat l’operació -action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim=Optimizing disc (TRIM) action.disk_trim.error=There was a problem while optimizing the disk action.failed=L'acció ha fallat action.history.no_history.body=There is no available history for {} diff --git a/bauh/view/resources/locale/de b/bauh/view/resources/locale/de index 5a183b21..075b306f 100644 --- a/bauh/view/resources/locale/de +++ b/bauh/view/resources/locale/de @@ -55,7 +55,7 @@ action.backups=Backups action.backups.status=Opening backups tool action.backups.tool_error=It was not possible to open the backups tool ({}). action.cancelled=Aktion vom Nutzer abgebrochen -action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim=Optimizing disc (TRIM) action.disk_trim.error=There was a problem while optimizing the disk action.failed=Aktion fehlgeschlagen action.history.no_history.body=There is no available history for {} diff --git a/bauh/view/resources/locale/en b/bauh/view/resources/locale/en index 4eb3355f..6864177c 100644 --- a/bauh/view/resources/locale/en +++ b/bauh/view/resources/locale/en @@ -56,7 +56,7 @@ action.backups.status=Opening backups tool action.backups.tool_error=It was not possible to open the backups tool ({}). action.cancelled=operation cancelled by the user action.disk_trim.error=There was a problem while optimizing the disk -action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim=Optimizing disc (TRIM) action.failed=Action failed action.history.no_history.body=There is no available history for {} action.history.no_history.title=No history diff --git a/bauh/view/resources/locale/es b/bauh/view/resources/locale/es index 7b311f67..2587cf4a 100644 --- a/bauh/view/resources/locale/es +++ b/bauh/view/resources/locale/es @@ -55,7 +55,7 @@ action.backups=Copias de seguridad action.backups.status=Abriendo herramienta de copias de seguridad action.backups.tool_error=No fue posible abrir la herramienta de copias de seguridad ({}). action.cancelled=operación cancelada por el usuario -action.disk_trim=Optimizando el disco ( TRIM ) +action.disk_trim=Optimizando el disco (TRIM) action.disk_trim.error=Hubo un problema al optimizar el disco action.failed=Acción fallida action.history.no_history.body=There is no available history for {} diff --git a/bauh/view/resources/locale/it b/bauh/view/resources/locale/it index 6e51c78a..b5d3b617 100644 --- a/bauh/view/resources/locale/it +++ b/bauh/view/resources/locale/it @@ -55,7 +55,7 @@ action.backups=Backups action.backups.status=Opening backups tool action.backups.tool_error=It was not possible to open the backups tool ({}). action.cancelled=operazione annullata dall'utente -action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim=Optimizing disc (TRIM) action.disk_trim.error=There was a problem while optimizing the disk action.failed=Azione non riuscita action.history.no_history.body=There is no available history for {} diff --git a/bauh/view/resources/locale/pt b/bauh/view/resources/locale/pt index dced5379..e33a7530 100644 --- a/bauh/view/resources/locale/pt +++ b/bauh/view/resources/locale/pt @@ -56,7 +56,7 @@ action.backups.status=Abrindo ferramenta de cópias de segurança action.backups.tool_error=Não foi possível abrir a ferramenta para backups ({}). action.cancelled=operação cancelada pelo usuário action.disk_trim.error=Ocorreu um problema ao otimizar o disco -action.disk_trim=Otimizando disco ( TRIM ) +action.disk_trim=Otimizando disco (TRIM) action.failed=Ação falhou action.history.no_history.body=Não existe histórico disponível para {} action.history.no_history.title=Sem histórico diff --git a/bauh/view/resources/locale/ru b/bauh/view/resources/locale/ru index 176f0166..0760e91e 100644 --- a/bauh/view/resources/locale/ru +++ b/bauh/view/resources/locale/ru @@ -55,7 +55,7 @@ action.backups=Backups action.backups.status=Opening backups tool action.backups.tool_error=It was not possible to open the backups tool ({}). action.cancelled=Операция отменена пользователем -action.disk_trim=Optimizing disc ( TRIM ) +action.disk_trim=Optimizing disc (TRIM) action.disk_trim.error=There was a problem while optimizing the disk action.failed=Действие не удалось action.history.no_history.body=There is no available history for {} diff --git a/bauh/view/resources/locale/tr b/bauh/view/resources/locale/tr index 8e6e74a5..c2ac8256 100644 --- a/bauh/view/resources/locale/tr +++ b/bauh/view/resources/locale/tr @@ -56,7 +56,7 @@ action.backups.status=Opening backups tool action.backups.timeshift_not_installed=It was not possible to open the backups tool ({}). action.cancelled=işlem kullanıcı tarafından iptal edildi action.disk_trim.error=Diski optimize ederken bir sorun oluştu -action.disk_trim=Diski optimize etme ( TRIM ) +action.disk_trim=Diski optimize etme (TRIM) action.failed=Eylem başarısız action.history.no_history.body={} için kullanılabilir geçmiş yok action.history.no_history.title=Geçmiş yok From 6b634545c9185edd7e3137c35cdf4f1101386a3a Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 17:11:54 -0300 Subject: [PATCH 96/99] Updating CHANGELOG --- CHANGELOG.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04cfd76e..228d4864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Features - Arch - AUR - - allowing to edit the PKGBUILD file of a package to be installed/upgraded/downgraded. If enabled, a popup will be displayed during this actions allowing the PKGBUILD to be edited. + - allowing to edit the PKGBUILD file of a package to be installed/upgraded/downgraded. If enabled, a popup will be displayed during these actions allowing the PKGBUILD to be edited.

@@ -20,12 +20,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- - new "Check Snaps support" action: checks all system requirements for Snaps to work properly (only available if the 'snapd' package is installed) + - new "Check Snaps support" action: it checks all system requirements for Snaps to work properly (only available if the 'snapd' package is installed)

- Snap - - new settings property **install_channel**: it allows to select an available channel during the application installation. Default: false. + - new settings property **install_channel**: it allows to select an available channel during the application installation. Default: false. [#90](https://github.com/vinifmor/bauh/issues/90)

@@ -66,7 +66,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- AUR - - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**) + - caching the PKGBUILD file used for the package installation/upgrade/downgrade (**~/.cache/bauh/arch/installed/$pkgname/PKGBUILD**). Directory: **~/.cache/bauh/arch/installed/my_package/PKGBUILD - new settings property **aur_build_dir** -> it allows to define a custom build dir.

@@ -95,7 +95,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixes - AppImage - manual file installation - - crashing the application icon is not on the extracted folder root path [#132](https://github.com/vinifmor/bauh/issues/132) + - crashing if the AppImage icon is not on the extracted folder root path [#132](https://github.com/vinifmor/bauh/issues/132) - some environment variable are not available during the launch process - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) @@ -127,7 +127,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Flatpak - downgrading crashing with version 1.8.X - history: the top commit is returned as "(null)" in version 1.8.X - - crashing when an update size cannot be read [#130](https://github.com/vinifmor/bauh/issues/130) + - crashing when an update size cannot be read -> [#130](https://github.com/vinifmor/bauh/issues/130) [#133](https://github.com/vinifmor/bauh/issues/130) - installation fails when there are multiple references for a given package (e.g: openh264)

@@ -144,7 +144,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - UI - crashing when nothing can be upgraded - random C++ wrapper errors with some forms due to missing references - - application icons that cannot be rendered are being displayed as an empty space (now the type icon is displayed instead) + - application icons that cannot be rendered are being displayed as empty spaces (now the type icon is displayed instead) - some application icons without a full path are not being rendered on the 'Upgrade summary' From ce8c2bd37093707eb7ad196510d6e37e2c91cf43 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 17:18:28 -0300 Subject: [PATCH 97/99] [appimage] fix -> not properly retrieving the 'Category' field options translated --- CHANGELOG.md | 1 + bauh/gems/appimage/controller.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 228d4864..3b930552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - AppImage - manual file installation - crashing if the AppImage icon is not on the extracted folder root path [#132](https://github.com/vinifmor/bauh/issues/132) + - not properly retrieving the 'Category' field options translated - some environment variable are not available during the launch process - Arch - not able to upgrade a package that explicitly defines a conflict with itself (e.g: grub) diff --git a/bauh/gems/appimage/controller.py b/bauh/gems/appimage/controller.py index 066c9708..e08882c9 100644 --- a/bauh/gems/appimage/controller.py +++ b/bauh/gems/appimage/controller.py @@ -105,7 +105,7 @@ def install_file(self, root_password: str, watcher: ProcessWatcher) -> bool: input_description = TextInputComponent(label=self.i18n['description'].capitalize()) cat_ops = [InputOption(label=self.i18n['category.none'].capitalize(), value=0)] - cat_ops.extend([InputOption(label=self.i18n[c.lower()].capitalize(), value=c) for c in self.context.default_categories]) + cat_ops.extend([InputOption(label=self.i18n.get('category.{}'.format(c.lower()), c.lower()).capitalize(), value=c) for c in self.context.default_categories]) inp_cat = SingleSelectComponent(label=self.i18n['category'], type_=SelectViewType.COMBO, options=cat_ops, default_option=cat_ops[0]) From 3c5f37ca5778817d6808f38f40f232ec1e0a9401 Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 17:38:31 -0300 Subject: [PATCH 98/99] [tray] fix -> always displaying the About dialog in english --- CHANGELOG.md | 1 + bauh/view/qt/systray.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b930552..5ccd3e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,6 +147,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - random C++ wrapper errors with some forms due to missing references - application icons that cannot be rendered are being displayed as empty spaces (now the type icon is displayed instead) - some application icons without a full path are not being rendered on the 'Upgrade summary' + - tray mode: always displaying the "About" dialog in english ## [0.9.6] 2020-06-26 diff --git a/bauh/view/qt/systray.py b/bauh/view/qt/systray.py index bd2c50ac..7d58e283 100755 --- a/bauh/view/qt/systray.py +++ b/bauh/view/qt/systray.py @@ -123,6 +123,7 @@ class TrayIcon(QSystemTrayIcon): def __init__(self, config: dict, screen_size: QSize, logger: logging.Logger, manage_process: Popen = None, settings_process: Popen = None): super(TrayIcon, self).__init__() + self.app_config = config self.i18n = generate_i18n(config, resource.get_path('locale/tray')) self.screen_size = screen_size self.manage_process = manage_process @@ -262,7 +263,7 @@ def show_settings_window(self): def show_about(self): if self.dialog_about is None: - self.dialog_about = AboutDialog(self.i18n) + self.dialog_about = AboutDialog(self.app_config) if self.dialog_about.isHidden(): self.dialog_about.show() From 21e62724fc60e15e73f7de603b2f0ed50efa7ddc Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Fri, 11 Sep 2020 17:39:10 -0300 Subject: [PATCH 99/99] Updating CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ccd3e29..e83395c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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.7] 2020 +## [0.9.7] 2020-09-11 ### Features - Arch - AUR

D;8koeS@9Q-B_L~ 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 a;tkC~p{AVX1E&DN4Im|FBwbCUe)boNo;5rG diff --git a/pictures/releases/0.9.2/color_design.png b/pictures/releases/0.9.2/color_design.png deleted file mode 100644 index 7c4a41a35d9fd93a0e928cb34edfdaac28761322..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1671757 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