2 # -*- coding: utf-8 -*-
6 # This code is based on previous work by Rémy Oudompheng <remy@archlinux.org>
8 # Copyright © 2013-2018 Antergos
10 # This file is part of Cnchi.
12 # Cnchi is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 3 of the License, or
15 # (at your option) any later version.
17 # Cnchi is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # GNU General Public License for more details.
22 # The following additional terms are in effect as per Section 7 of the license:
24 # The preservation of all legal notices and author attributions in
25 # the material or in the Appropriate Legal Notices displayed
26 # by works containing it is required.
28 # You should have received a copy of the GNU General Public License
29 # along with Cnchi; If not, see <http://www.gnu.org/licenses/>.
32 """ Module interface to pyalpm """
34 from collections import OrderedDict
40 from misc.events import Events
42 import pacman.alpm_include as _alpm
43 import pacman.pkginfo as pkginfo
44 import pacman.pacman_conf as config
48 except ImportError as err:
49 # This is already logged elsewhere
53 # When testing, no _() is available
56 except NameError as err:
60 _DEFAULT_ROOT_DIR = "/"
61 _DEFAULT_DB_PATH = "/var/lib/pacman"
65 """ Communicates with libalpm using pyalpm """
66 LOG_FOLDER = '/var/log/cnchi'
68 def __init__(self, conf_path="/etc/pacman.conf", callback_queue=None):
69 self.events = Events(callback_queue)
71 self.conflict_to_remove = None
78 # Some download indicators (used in cb_dl callback)
81 self.already_transferred = 0
82 # Store package total download size
87 # Total packages to download
88 self.total_packages_to_download = 0
89 self.downloaded_packages = 0
93 if not os.path.exists(conf_path):
96 if conf_path is not None and os.path.exists(conf_path):
97 self.config = config.PacmanConfig(conf_path)
98 self.initialize_alpm()
99 logging.debug('ALPM repository database order is: %s',
100 self.config.repo_order)
105 def format_size(size):
106 """ Formats downloaded size into a string """
107 kib_size = size / 1024
109 size_string = _('%.1f KiB') % (kib_size)
111 size_string = _('%.2f MiB') % (kib_size / 1024)
114 def get_handle(self):
115 """ Return alpm handle """
118 def get_config(self):
119 """ Get pacman.conf config """
122 def initialize_alpm(self):
123 """ Set alpm setup """
124 if self.config is not None:
125 root_dir = self.config.options["RootDir"]
126 db_path = self.config.options["DBPath"]
128 root_dir = _DEFAULT_ROOT_DIR
129 db_path = _DEFAULT_DB_PATH
131 self.handle = pyalpm.Handle(root_dir, db_path)
134 "ALPM initialised with root dir %s and db path %s", root_dir, db_path)
136 if self.handle is None:
139 if self.config is not None:
140 self.config.apply(self.handle)
142 # Set callback functions
143 # Callback used for logging
144 self.handle.logcb = self.cb_log
145 # Callback used to report download progress
146 self.handle.dlcb = self.cb_dl
147 # Callback used to report total download size
148 self.handle.totaldlcb = self.cb_totaldl
149 # Callback used for events
150 self.handle.eventcb = self.cb_event
151 # Callback used for questions
152 self.handle.questioncb = self.cb_question
153 # Callback used for operation progress
154 self.handle.progresscb = self.cb_progress
155 # Downloading callback
156 self.handle.fetchcb = None
159 """ Release alpm handle """
160 if self.handle is not None:
165 def finalize_transaction(transaction):
166 """ Commit a transaction """
168 logging.debug("Prepare alpm transaction...")
169 transaction.prepare()
170 logging.debug("Commit alpm transaction...")
172 except pyalpm.error as err:
173 logging.error("Can't finalize alpm transaction: %s", err)
174 transaction.release()
176 transaction.release()
177 logging.debug("Alpm transaction done.")
180 def init_transaction(self, options=None):
181 """ Transaction initialization """
187 transaction = self.handle.init_transaction(
188 nodeps=options.get('nodeps', False),
189 dbonly=options.get('dbonly', False),
190 force=options.get('force', False),
191 needed=options.get('needed', False),
192 alldeps=(options.get('mode', None) ==
193 pyalpm.PKG_REASON_DEPEND),
194 allexplicit=(options.get('mode', None) ==
195 pyalpm.PKG_REASON_EXPLICIT),
196 cascade=options.get('cascade', False),
197 nosave=options.get('nosave', False),
198 recurse=(options.get('recursive', 0) > 0),
199 recurseall=(options.get('recursive', 0) > 1),
200 unneeded=options.get('unneeded', False),
201 downloadonly=options.get('downloadonly', False))
202 except pyalpm.error as pyalpm_error:
203 logging.error("Can't init alpm transaction: %s", pyalpm_error)
206 def remove(self, pkg_names, options=None):
207 """ Removes a list of package names """
212 # Prepare target list
214 database = self.handle.get_localdb()
215 for pkg_name in pkg_names:
216 pkg = database.get_pkg(pkg_name)
218 logging.error("Target %s not found", pkg_name)
222 transaction = self.init_transaction(options)
224 if transaction is None:
225 logging.error("Can't init transaction")
230 "Adding package '%s' to remove transaction", pkg.name)
231 transaction.remove_pkg(pkg)
233 return self.finalize_transaction(transaction)
236 """ Sync databases like pacman -Sy """
237 if self.handle is None:
238 logging.error("alpm is not initialised")
243 for database in self.handle.get_syncdbs():
244 transaction = self.init_transaction()
246 database.update(force)
247 transaction.release()
252 def install(self, pkgs, conflicts=None, options=None):
253 """ Install a list of packages like pacman -S """
261 if self.handle is None:
262 logging.error("alpm is not initialised")
266 logging.error("Package list is empty")
270 pkgs = list(set(pkgs))
272 # `alpm.handle.get_syncdbs()` returns a list (the order is important) so we
273 # have to ensure we don't clobber the priority of the repos.
274 repos = OrderedDict()
276 db_match = [db for db in self.handle.get_syncdbs()
277 if db.name == 'Reborn-OS']
278 antdb = OrderedDict()
279 antdb['Reborn-OS'] = db_match[0]
281 one_repo_groups_names = ['cinnamon', 'mate', 'mate-extra']
283 for one_repo_group_name in one_repo_groups_names:
284 grp = antdb['Reborn-OS'].read_grp(one_repo_group_name)
286 # Group does not exist
288 one_repo_groups.append(grp)
290 one_repo_pkgs = {pkg for one_repo_group in one_repo_groups
291 for pkg in one_repo_group[1] if one_repo_group}
293 for syncdb in self.handle.get_syncdbs():
294 repo_order.append(syncdb)
295 repos[syncdb.name] = syncdb
301 if name in one_repo_pkgs:
302 # pkg should be sourced from the antergos repo only.
305 result_ok, pkg = self.find_sync_package(name, _repos)
308 # Check that added package is not in our conflicts list
309 if pkg.name not in conflicts:
310 targets.append(pkg.name)
312 # Couldn't find the package, check if it's a group
313 group_pkgs = self.get_group_pkgs(name)
314 if group_pkgs is not None:
316 for group_pkg in group_pkgs:
317 # Check that added package is not in our conflicts list
318 # Ex: connman conflicts with netctl(openresolv),
319 # which is installed by default with base group
320 if group_pkg.name not in conflicts:
321 targets.append(group_pkg.name)
323 # No, it wasn't neither a package nor a group. As we don't
324 # know if this error is fatal or not, we'll register it and
325 # we'll allow to continue.
327 "Can't find a package or group called '%s'", name)
330 targets = list(set(targets))
331 logging.debug(targets)
334 logging.error("No targets found")
337 num_targets = len(targets)
338 logging.debug("%d target(s) found", num_targets)
340 # Maybe not all this packages will be downloaded, but it's
341 # how many have to be there before starting the installation
342 self.total_packages_to_download = num_targets
344 transaction = self.init_transaction(options)
346 if transaction is None:
347 logging.error("Can't initialize alpm transaction")
350 for _index in range(0, num_targets):
351 result_ok, pkg = self.find_sync_package(targets.pop(), repos)
353 transaction.add_pkg(pkg)
357 return self.finalize_transaction(transaction)
359 def upgrade(self, pkgs, conflicts=None, options=None):
360 """ Install a list package tarballs like pacman -U """
362 conflicts = conflicts if conflicts else []
363 options = options if options else {}
365 if self.handle is None:
366 logging.error("alpm is not initialised")
370 logging.error("Package list is empty")
374 pkgs = list(set(pkgs))
376 self.handle.get_localdb()
378 # Prepare targets list
381 pkg = self.handle.load_pkg(tarball)
384 transaction = self.init_transaction(options)
386 if transaction is None:
387 logging.error("Can't initialize alpm transaction")
391 transaction.add_pkg(pkg)
393 return self.finalize_transaction(transaction)
396 def find_sync_package(pkgname, syncdbs):
397 """ Finds a package name in a list of DBs
398 :rtype : tuple (True/False, package or error message)
400 for database in syncdbs.values():
401 pkg = database.get_pkg(pkgname)
404 return False, "Package '{0}' was not found.".format(pkgname)
406 def get_group_pkgs(self, group):
407 """ Get group's packages """
408 for repo in self.handle.get_syncdbs():
409 grp = repo.read_grp(group)
415 def get_packages_info(self, pkg_names=None):
416 """ Get information about packages like pacman -Si """
421 # Store info from all packages from all repos
422 for repo in self.handle.get_syncdbs():
423 for pkg in repo.pkgcache:
424 packages_info[pkg.name] = pkginfo.get_pkginfo(
429 repos = OrderedDict((database.name, database)
430 for database in self.handle.get_syncdbs())
431 for pkg_name in pkg_names:
432 result_ok, pkg = self.find_sync_package(pkg_name, repos)
434 packages_info[pkg_name] = pkginfo.get_pkginfo(
443 def get_package_info(self, pkg_name):
444 """ Get information about packages like pacman -Si """
445 repos = OrderedDict((database.name, database)
446 for database in self.handle.get_syncdbs())
447 result_ok, pkg = self.find_sync_package(pkg_name, repos)
449 info = pkginfo.get_pkginfo(pkg, level=2, style='sync')
458 def cb_question(*args):
459 """ Called to get user input """
462 def cb_event(self, event, event_data):
463 """ Converts action ID to descriptive text and enqueues it to the events queue """
464 action = self.last_action
466 if event == _alpm.ALPM_EVENT_CHECKDEPS_START:
467 action = _('Checking dependencies...')
468 elif event == _alpm.ALPM_EVENT_FILECONFLICTS_START:
469 action = _('Checking file conflicts...')
470 elif event == _alpm.ALPM_EVENT_RESOLVEDEPS_START:
471 action = _('Resolving dependencies...')
472 elif _alpm.ALPM_EVENT_INTERCONFLICTS_START:
473 action = _('Checking inter conflicts...')
474 elif event == _alpm.ALPM_EVENT_PACKAGE_OPERATION_START:
475 # ALPM_EVENT_PACKAGE_OPERATION_START is shown in cb_progress
477 elif event == _alpm.ALPM_EVENT_INTEGRITY_START:
478 action = _('Checking integrity...')
479 self.already_transferred = 0
480 elif event == _alpm.ALPM_EVENT_LOAD_START:
481 action = _('Loading packages files...')
482 elif event == _alpm.ALPM_EVENT_DELTA_INTEGRITY_START:
483 action = _("Checking target delta integrity...")
484 elif event == _alpm.ALPM_EVENT_DELTA_PATCHES_START:
485 action = _('Applying deltas to packages...')
486 elif event == _alpm.ALPM_EVENT_DELTA_PATCH_START:
487 action = _('Generating {} with {}...')
488 action = action.format(event_data[0], event_data[1])
489 elif event == _alpm.ALPM_EVENT_RETRIEVE_START:
490 action = _('Downloading files from the repositories...')
491 elif event == _alpm.ALPM_EVENT_DISKSPACE_START:
492 action = _('Checking available disk space...')
493 elif event == _alpm.ALPM_EVENT_KEYRING_START:
494 action = _('Checking keys in keyring...')
495 elif event == _alpm.ALPM_EVENT_KEY_DOWNLOAD_START:
496 action = _('Downloading missing keys into the keyring...')
497 elif event == _alpm.ALPM_EVENT_SCRIPTLET_INFO:
498 action = _('Configuring {}').format(self.last_target)
500 if action != self.last_action:
501 self.last_action = action
502 self.events.add('info', action)
504 def cb_log(self, level, line):
505 """ Log pyalpm warning and error messages.
506 Possible message types:
507 LOG_ERROR, LOG_WARNING, LOG_DEBUG, LOG_FUNCTION """
512 # Log everything to cnchi-alpm.log
513 self.logger.debug(line)
515 logmask = pyalpm.LOG_ERROR | pyalpm.LOG_WARNING
517 if not level & logmask:
518 # Only log errors and warnings
521 if level & pyalpm.LOG_ERROR:
523 elif level & pyalpm.LOG_WARNING:
524 logging.warning(line)
525 elif level & pyalpm.LOG_DEBUG or level & pyalpm.LOG_FUNCTION:
528 def cb_progress(self, target, percent, total, current):
529 """ Shows install progress """
531 action = _("Installing {0} ({1}/{2})").format(target, current, total)
532 percent = current / total
533 self.events.add('info', action)
535 percent = round(percent / 100, 2)
537 if target != self.last_target:
538 self.last_target = target
540 if percent != self.last_percent:
541 self.last_percent = percent
542 self.events.add('percent', percent)
544 def cb_totaldl(self, total_size):
545 """ Stores total download size for use in cb_dl and cb_progress """
546 self.total_size = total_size
548 def cb_dl(self, filename, transferred, total):
549 """ Shows downloading progress """
550 if filename.endswith('.db'):
551 action = _("Updating {0} database").format(filename.replace('.db', ''))
553 action = _("Downloading {}...").format(filename.replace('.pkg.tar.xz', ''))
555 # target = self.last_target
556 percent = self.last_percent
558 if self.total_size > 0:
559 percent = round((transferred + self.already_transferred) / self.total_size, 2)
561 percent = round(transferred / total, 2)
563 if action != self.last_action:
564 self.last_action = action
565 self.events.add('info', action)
567 if percent != self.last_percent:
568 self.last_percent = percent
569 self.events.add('percent', percent)
570 elif transferred == total:
571 self.already_transferred += total
572 self.downloaded_packages += 1
574 def is_package_installed(self, package_name):
575 """ Check if package is already installed """
576 database = self.handle.get_localdb()
577 pkgs = database.search(*[package_name])
580 names.append(pkg.name)
581 return bool(package_name in names)
583 def setup_logger(self):
584 """ Configure our logger """
585 self.logger = logging.getLogger(__name__)
587 self.logger.setLevel(logging.DEBUG)
589 self.logger.propagate = False
592 formatter = logging.Formatter(
593 fmt="%(asctime)s [%(levelname)s] %(filename)s(%(lineno)d) %(funcName)s(): %(message)s")
595 if not self.logger.hasHandlers():
598 log_path = os.path.join(Pac.LOG_FOLDER, 'cnchi-alpm.log')
599 file_handler = logging.FileHandler(log_path, mode='w')
600 file_handler.setLevel(logging.DEBUG)
601 file_handler.setFormatter(formatter)
602 self.logger.addHandler(file_handler)
603 except PermissionError as permission_error:
604 print("Can't open ", log_path, " : ", permission_error)
612 formatter = logging.Formatter(
613 '[%(asctime)s] [%(module)s] %(levelname)s: %(message)s',
614 "%Y-%m-%d %H:%M:%S.%f")
615 logger = logging.getLogger()
616 logger.setLevel(logging.DEBUG)
617 stream_handler = logging.StreamHandler()
618 stream_handler.setLevel(logging.DEBUG)
619 stream_handler.setFormatter(formatter)
620 logger.addHandler(stream_handler)
623 pacman = Pac("/etc/pacman.conf")
624 except pyalpm.error as ex:
625 print("Can't initialize pyalpm: ", ex)
630 except pyalpm.error as err:
631 print("Can't update databases: ", err)
634 # pacman_options = {"downloadonly": True}
635 # pacman.do_install(pkgs=["base"], conflicts=[], options=pacman_options)
639 if __name__ == "__main__":