OSDN Git Service

2021.01.14 Update
[rebornos/cnchi-gnome-osdn.git] / Cnchi / pac.py
1 #!/usr/bin/env python
2 # -*- coding: utf-8 -*-
3 #
4 #  pac.py
5 #
6 #  This code is based on previous work by Rémy Oudompheng <remy@archlinux.org>
7 #
8 #  Copyright © 2013-2018 Antergos
9 #
10 #  This file is part of Cnchi.
11 #
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.
16 #
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.
21 #
22 #  The following additional terms are in effect as per Section 7 of the license:
23 #
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.
27 #
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/>.
30
31
32 """ Module interface to pyalpm """
33
34 from collections import OrderedDict
35
36 import logging
37 import os
38 import sys
39
40 from misc.events import Events
41
42 import pacman.alpm_include as _alpm
43 import pacman.pkginfo as pkginfo
44 import pacman.pacman_conf as config
45
46 try:
47     import pyalpm
48 except ImportError as err:
49     # This is already logged elsewhere
50     # logging.error(err)
51     pass
52
53 # When testing, no _() is available
54 try:
55     _("")
56 except NameError as err:
57     def _(message):
58         return message
59
60 _DEFAULT_ROOT_DIR = "/"
61 _DEFAULT_DB_PATH = "/var/lib/pacman"
62
63
64 class Pac():
65     """ Communicates with libalpm using pyalpm """
66     LOG_FOLDER = '/var/log/cnchi'
67
68     def __init__(self, conf_path="/etc/pacman.conf", callback_queue=None):
69         self.events = Events(callback_queue)
70
71         self.conflict_to_remove = None
72
73         self.handle = None
74
75         self.logger = None
76         self.setup_logger()
77
78         # Some download indicators (used in cb_dl callback)
79         self.last_target = ""
80         self.last_percent = 0
81         self.already_transferred = 0
82         # Store package total download size
83         self.total_size = 0
84         # Store last action
85         self.last_action = ""
86
87         # Total packages to download
88         self.total_packages_to_download = 0
89         self.downloaded_packages = 0
90
91         self.last_event = {}
92
93         if not os.path.exists(conf_path):
94             raise pyalpm.error
95
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)
101         else:
102             raise pyalpm.error
103
104     @staticmethod
105     def format_size(size):
106         """ Formats downloaded size into a string """
107         kib_size = size / 1024
108         if kib_size < 1000:
109             size_string = _('%.1f KiB') % (kib_size)
110             return size_string
111         size_string = _('%.2f MiB') % (kib_size / 1024)
112         return size_string
113
114     def get_handle(self):
115         """ Return alpm handle """
116         return self.handle
117
118     def get_config(self):
119         """ Get pacman.conf config """
120         return self.config
121
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"]
127         else:
128             root_dir = _DEFAULT_ROOT_DIR
129             db_path = _DEFAULT_DB_PATH
130
131         self.handle = pyalpm.Handle(root_dir, db_path)
132
133         logging.debug(
134             "ALPM initialised with root dir %s and db path %s", root_dir, db_path)
135
136         if self.handle is None:
137             raise pyalpm.error
138
139         if self.config is not None:
140             self.config.apply(self.handle)
141
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
157
158     def release(self):
159         """ Release alpm handle """
160         if self.handle is not None:
161             del self.handle
162             self.handle = None
163
164     @staticmethod
165     def finalize_transaction(transaction):
166         """ Commit a transaction """
167         try:
168             logging.debug("Prepare alpm transaction...")
169             transaction.prepare()
170             logging.debug("Commit alpm transaction...")
171             transaction.commit()
172         except pyalpm.error as err:
173             logging.error("Can't finalize alpm transaction: %s", err)
174             transaction.release()
175             return False
176         transaction.release()
177         logging.debug("Alpm transaction done.")
178         return True
179
180     def init_transaction(self, options=None):
181         """ Transaction initialization """
182         if options is None:
183             options = {}
184
185         transaction = None
186         try:
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)
204         return transaction
205
206     def remove(self, pkg_names, options=None):
207         """ Removes a list of package names """
208
209         if not options:
210             options = {}
211
212         # Prepare target list
213         targets = []
214         database = self.handle.get_localdb()
215         for pkg_name in pkg_names:
216             pkg = database.get_pkg(pkg_name)
217             if pkg is None:
218                 logging.error("Target %s not found", pkg_name)
219                 return False
220             targets.append(pkg)
221
222         transaction = self.init_transaction(options)
223
224         if transaction is None:
225             logging.error("Can't init transaction")
226             return False
227
228         for pkg in targets:
229             logging.debug(
230                 "Adding package '%s' to remove transaction", pkg.name)
231             transaction.remove_pkg(pkg)
232
233         return self.finalize_transaction(transaction)
234
235     def refresh(self):
236         """ Sync databases like pacman -Sy """
237         if self.handle is None:
238             logging.error("alpm is not initialised")
239             raise pyalpm.error
240
241         force = True
242         res = True
243         for database in self.handle.get_syncdbs():
244             transaction = self.init_transaction()
245             if transaction:
246                 database.update(force)
247                 transaction.release()
248             else:
249                 res = False
250         return res
251
252     def install(self, pkgs, conflicts=None, options=None):
253         """ Install a list of packages like pacman -S """
254
255         if not conflicts:
256             conflicts = []
257
258         if not options:
259             options = {}
260
261         if self.handle is None:
262             logging.error("alpm is not initialised")
263             raise pyalpm.error
264
265         if not pkgs:
266             logging.error("Package list is empty")
267             raise pyalpm.error
268
269         # Discard duplicates
270         pkgs = list(set(pkgs))
271
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()
275         repo_order = []
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]
280
281         one_repo_groups_names = ['cinnamon', 'mate', 'mate-extra']
282         one_repo_groups = []
283         for one_repo_group_name in one_repo_groups_names:
284             grp = antdb['Reborn-OS'].read_grp(one_repo_group_name)
285             if not grp:
286                 # Group does not exist
287                 grp = ['None', []]
288             one_repo_groups.append(grp)
289
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}
292
293         for syncdb in self.handle.get_syncdbs():
294             repo_order.append(syncdb)
295             repos[syncdb.name] = syncdb
296
297         targets = []
298         for name in pkgs:
299             _repos = repos
300
301             if name in one_repo_pkgs:
302                 # pkg should be sourced from the antergos repo only.
303                 _repos = antdb
304
305             result_ok, pkg = self.find_sync_package(name, _repos)
306
307             if result_ok:
308                 # Check that added package is not in our conflicts list
309                 if pkg.name not in conflicts:
310                     targets.append(pkg.name)
311             else:
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:
315                     # It's a group
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)
322                 else:
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.
326                     logging.error(
327                         "Can't find a package or group called '%s'", name)
328
329         # Discard duplicates
330         targets = list(set(targets))
331         logging.debug(targets)
332
333         if not targets:
334             logging.error("No targets found")
335             return False
336
337         num_targets = len(targets)
338         logging.debug("%d target(s) found", num_targets)
339
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
343
344         transaction = self.init_transaction(options)
345
346         if transaction is None:
347             logging.error("Can't initialize alpm transaction")
348             return False
349
350         for _index in range(0, num_targets):
351             result_ok, pkg = self.find_sync_package(targets.pop(), repos)
352             if result_ok:
353                 transaction.add_pkg(pkg)
354             else:
355                 logging.warning(pkg)
356
357         return self.finalize_transaction(transaction)
358
359     def upgrade(self, pkgs, conflicts=None, options=None):
360         """ Install a list package tarballs like pacman -U """
361
362         conflicts = conflicts if conflicts else []
363         options = options if options else {}
364
365         if self.handle is None:
366             logging.error("alpm is not initialised")
367             raise pyalpm.error
368
369         if not pkgs:
370             logging.error("Package list is empty")
371             raise pyalpm.error
372
373         # Discard duplicates
374         pkgs = list(set(pkgs))
375
376         self.handle.get_localdb()
377
378         # Prepare targets list
379         targets = []
380         for tarball in pkgs:
381             pkg = self.handle.load_pkg(tarball)
382             targets.append(pkg)
383
384         transaction = self.init_transaction(options)
385
386         if transaction is None:
387             logging.error("Can't initialize alpm transaction")
388             return False
389
390         for pkg in targets:
391             transaction.add_pkg(pkg)
392
393         return self.finalize_transaction(transaction)
394
395     @staticmethod
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)
399         """
400         for database in syncdbs.values():
401             pkg = database.get_pkg(pkgname)
402             if pkg is not None:
403                 return True, pkg
404         return False, "Package '{0}' was not found.".format(pkgname)
405
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)
410             if grp is not None:
411                 _name, pkgs = grp
412                 return pkgs
413         return None
414
415     def get_packages_info(self, pkg_names=None):
416         """ Get information about packages like pacman -Si """
417         if not pkg_names:
418             pkg_names = []
419         packages_info = {}
420         if not pkg_names:
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(
425                         pkg,
426                         level=2,
427                         style='sync')
428         else:
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)
433                 if result_ok:
434                     packages_info[pkg_name] = pkginfo.get_pkginfo(
435                         pkg,
436                         level=2,
437                         style='sync')
438                 else:
439                     packages_info = {}
440                     logging.error(pkg)
441         return packages_info
442
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)
448         if result_ok:
449             info = pkginfo.get_pkginfo(pkg, level=2, style='sync')
450         else:
451             logging.error(pkg)
452             info = {}
453         return info
454
455     # Callback functions
456
457     @staticmethod
458     def cb_question(*args):
459         """ Called to get user input """
460         pass
461
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
465
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
476             action = ''
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)
499
500         if action != self.last_action:
501             self.last_action = action
502             self.events.add('info', action)
503
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 """
508
509         # Strip ending '\n'
510         line = line.rstrip()
511
512         # Log everything to cnchi-alpm.log
513         self.logger.debug(line)
514
515         logmask = pyalpm.LOG_ERROR | pyalpm.LOG_WARNING
516
517         if not level & logmask:
518             # Only log errors and warnings
519             return
520
521         if level & pyalpm.LOG_ERROR:
522             logging.error(line)
523         elif level & pyalpm.LOG_WARNING:
524             logging.warning(line)
525         elif level & pyalpm.LOG_DEBUG or level & pyalpm.LOG_FUNCTION:
526             logging.debug(line)
527
528     def cb_progress(self, target, percent, total, current):
529         """ Shows install progress """
530         if target:
531             action = _("Installing {0} ({1}/{2})").format(target, current, total)
532             percent = current / total
533             self.events.add('info', action)
534         else:
535             percent = round(percent / 100, 2)
536
537         if target != self.last_target:
538             self.last_target = target
539
540         if percent != self.last_percent:
541             self.last_percent = percent
542             self.events.add('percent', percent)
543
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
547
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', ''))
552         else:
553             action = _("Downloading {}...").format(filename.replace('.pkg.tar.xz', ''))
554
555         # target = self.last_target
556         percent = self.last_percent
557
558         if self.total_size > 0:
559             percent = round((transferred + self.already_transferred) / self.total_size, 2)
560         elif total > 0:
561             percent = round(transferred / total, 2)
562
563         if action != self.last_action:
564             self.last_action = action
565             self.events.add('info', action)
566
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
573
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])
578         names = []
579         for pkg in pkgs:
580             names.append(pkg.name)
581         return bool(package_name in names)
582
583     def setup_logger(self):
584         """ Configure our logger """
585         self.logger = logging.getLogger(__name__)
586
587         self.logger.setLevel(logging.DEBUG)
588
589         self.logger.propagate = False
590
591         # Log format
592         formatter = logging.Formatter(
593             fmt="%(asctime)s [%(levelname)s] %(filename)s(%(lineno)d) %(funcName)s(): %(message)s")
594
595         if not self.logger.hasHandlers():
596             # File logger
597             try:
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)
605
606
607 def test():
608     """ Test case """
609     import gettext
610     _ = gettext.gettext
611
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)
621
622     try:
623         pacman = Pac("/etc/pacman.conf")
624     except pyalpm.error as ex:
625         print("Can't initialize pyalpm: ", ex)
626         sys.exit(1)
627
628     try:
629         pacman.refresh()
630     except pyalpm.error as err:
631         print("Can't update databases: ", err)
632         sys.exit(1)
633
634     # pacman_options = {"downloadonly": True}
635     # pacman.do_install(pkgs=["base"], conflicts=[], options=pacman_options)
636     pacman.release()
637
638
639 if __name__ == "__main__":
640     test()