Merge branch 'gentoolkit' of git+ssh://overlays.gentoo.org/proj/gentoolkit into gento...
[gentoolkit.git] / pym / gentoolkit / package.py
1 #!/usr/bin/python
2 #
3 # Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org>
4 # Copyright(c) 2004-2010, Gentoo Foundation
5 #
6 # Licensed under the GNU General Public License, v2
7 #
8 # $Header$
9
10 """Provides an interface to package information stored by package managers.
11
12 The Package class is the heart of much of Gentoolkit. Given a CPV
13 (category/package-version) string, it can reveal the package's status in the
14 tree and VARDB (/var/db/), provide rich comparison and sorting, and expose
15 important parts of Portage's back-end.
16
17 Example usage:
18         >>> portage = Package('sys-apps/portage-2.1.6.13')
19         >>> portage.ebuild_path()
20         '/usr/portage/sys-apps/portage/portage-2.1.6.13.ebuild'
21         >>> portage.is_masked()
22         False
23         >>> portage.is_installed()
24         True
25 """
26
27 __all__ = (
28         'Package',
29         'PackageFormatter',
30         'FORMAT_TMPL_VARS'
31 )
32
33 # =======
34 # Globals
35 # =======
36
37 FORMAT_TMPL_VARS = (
38         '$location', '$mask', '$mask2', '$cp', '$cpv', '$category', '$name',
39         '$version', '$revision', '$fullversion', '$slot', '$repo', '$keywords'
40
41
42 # =======
43 # Imports
44 # =======
45
46 import os
47 from string import Template
48
49 import portage
50 from portage import settings
51 from portage.util import LazyItemsDict
52
53 import gentoolkit.pprinter as pp
54 from gentoolkit import errors
55 from gentoolkit.cpv import CPV
56 from gentoolkit.dbapi import PORTDB, VARDB
57 from gentoolkit.keyword import determine_keyword
58 from gentoolkit.flag import get_flags
59
60 # =======
61 # Classes
62 # =======
63
64 class Package(CPV):
65         """Exposes the state of a given CPV."""
66
67         def __init__(self, cpv, validate=False):
68                 if isinstance(cpv, CPV):
69                         self.__dict__.update(cpv.__dict__)
70                 else:
71                         CPV.__init__(self, cpv, validate=validate)
72
73                 if validate and not all(
74                         hasattr(self, x) for x in ('category', 'version')
75                 ):
76                         # CPV allows some things that Package must not
77                         raise errors.GentoolkitInvalidPackage(self.cpv)
78
79                 # Set dynamically
80                 self._package_path = None
81                 self._dblink = None
82                 self._metadata = None
83                 self._deps = None
84                 self._portdir_path = None
85
86         def __repr__(self):
87                 return "<%s %r>" % (self.__class__.__name__, self.cpv)
88
89         def __hash__(self):
90                 return hash(self.cpv)
91
92         def __contains__(self, key):
93                 return key in self.cpv
94
95         def __str__(self):
96                 return self.cpv
97
98         @property
99         def metadata(self):
100                 """Instantiate a L{gentoolkit.metadata.MetaData} object here."""
101
102                 from gentoolkit.metadata import MetaData
103
104                 if self._metadata is None:
105                         metadata_path = os.path.join(
106                                 self.package_path(), 'metadata.xml'
107                         )
108                         try:
109                                 self._metadata = MetaData(metadata_path)
110                         except IOError as e:
111                                 import errno
112                                 if e.errno != errno.ENOENT:
113                                         raise
114                                 return None
115
116                 return self._metadata
117
118         @property
119         def dblink(self):
120                 """Instantiate a L{portage.dbapi.vartree.dblink} object here."""
121
122                 if self._dblink is None:
123                         self._dblink = portage.dblink(
124                                 self.category,
125                                 "%s-%s" % (self.name, self.fullversion),
126                                 settings["ROOT"],
127                                 settings
128                         )
129
130                 return self._dblink
131
132         @property
133         def deps(self):
134                 """Instantiate a L{gentoolkit.dependencies.Dependencies} object here."""
135
136                 from gentoolkit.dependencies import Dependencies
137
138                 if self._deps is None:
139                         self._deps = Dependencies(self.cpv)
140
141                 return self._deps
142
143         def environment(self, envvars, prefer_vdb=True, fallback=True):
144                 """Returns one or more of the predefined environment variables.
145
146                 Some available envvars are:
147                 ----------------------
148                         BINPKGMD5  COUNTER         FEATURES   LICENSE  SRC_URI
149                         CATEGORY   CXXFLAGS        HOMEPAGE   PDEPEND  USE
150                         CBUILD     DEFINED_PHASES  INHERITED  PF
151                         CFLAGS     DEPEND          IUSE       PROVIDE
152                         CHOST      DESCRIPTION     KEYWORDS   RDEPEND
153                         CONTENTS   EAPI            LDFLAGS    SLOT
154
155                 Example usage:
156                         >>> pkg = Package('sys-apps/portage-2.1.6.13')
157                         >>> pkg.environment('USE')
158                         'elibc_glibc kernel_linux userland_GNU x86'
159                         >>> pkg.environment(('USE', 'IUSE'))
160                         ['elibc_glibc kernel_linux userland_GNU x86',
161                                 'build doc epydoc selinux linguas_pl']
162
163                 @type envvars: str or array
164                 @param envvars: one or more of (DEPEND, SRC_URI, etc.)
165                 @type prefer_vdb: bool
166                 @keyword prefer_vdb: if True, look in the vardb before portdb, else
167                         reverse order. Specifically KEYWORDS will get more recent
168                         information by preferring portdb.
169                 @type fallback: bool
170                 @keyword fallback: query only the preferred db if False
171                 @rtype: str or list
172                 @return: str if envvars is str, list if envvars is array
173                 @raise KeyError: if key is not found in requested db(s)
174                 """
175
176                 got_string = False
177                 if isinstance(envvars, str):
178                         got_string = True
179                         envvars = (envvars,)
180                 if prefer_vdb:
181                         try:
182                                 result = VARDB.aux_get(self.cpv, envvars)
183                         except KeyError:
184                                 try:
185                                         if not fallback:
186                                                 raise KeyError
187                                         result = PORTDB.aux_get(self.cpv, envvars)
188                                 except KeyError:
189                                         err = "aux_get returned unexpected results"
190                                         raise errors.GentoolkitFatalError(err)
191                 else:
192                         try:
193                                 result = PORTDB.aux_get(self.cpv, envvars)
194                         except KeyError:
195                                 try:
196                                         if not fallback:
197                                                 raise KeyError
198                                         result = VARDB.aux_get(self.cpv, envvars)
199                                 except KeyError:
200                                         err = "aux_get returned unexpected results"
201                                         raise errors.GentoolkitFatalError(err)
202
203                 if got_string:
204                         return result[0]
205                 return result
206
207         def exists(self):
208                 """Return True if package exists in the Portage tree, else False"""
209
210                 return bool(PORTDB.cpv_exists(self.cpv))
211
212         @staticmethod
213         def settings(key):
214                 """Returns the value of the given key for this package (useful
215                 for package.* files."""
216
217                 if settings.locked:
218                         settings.unlock()
219                 try:
220                         result = settings[key]
221                 finally:
222                         settings.lock()
223                 return result
224
225         def mask_status(self):
226                 """Shortcut to L{portage.getmaskingstatus}.
227
228                 @rtype: None or list
229                 @return: a list containing none or some of:
230                         'profile'
231                         'package.mask'
232                         license(s)
233                         "kmask" keyword
234                         'missing keyword'
235                 """
236
237                 if settings.locked:
238                         settings.unlock()
239                 try:
240                         result = portage.getmaskingstatus(self.cpv,
241                                 settings=settings,
242                                 portdb=PORTDB)
243                 except KeyError:
244                         # getmaskingstatus doesn't support packages without ebuilds in the
245                         # Portage tree.
246                         result = None
247
248                 return result
249
250         def mask_reason(self):
251                 """Shortcut to L{portage.getmaskingreason}.
252
253                 @rtype: None or tuple
254                 @return: empty tuple if pkg not masked OR
255                         ('mask reason', 'mask location')
256                 """
257
258                 try:
259                         result = portage.getmaskingreason(self.cpv,
260                                 settings=settings,
261                                 portdb=PORTDB,
262                                 return_location=True)
263                         if result is None:
264                                 result = tuple()
265                 except KeyError:
266                         # getmaskingstatus doesn't support packages without ebuilds in the
267                         # Portage tree.
268                         result = None
269
270                 return result
271
272         def ebuild_path(self, in_vartree=False):
273                 """Returns the complete path to the .ebuild file.
274
275                 Example usage:
276                         >>> pkg.ebuild_path()
277                         '/usr/portage/sys-apps/portage/portage-2.1.6.13.ebuild'
278                         >>> pkg.ebuild_path(in_vartree=True)
279                         '/var/db/pkg/sys-apps/portage-2.1.6.13/portage-2.1.6.13.ebuild'
280                 """
281
282                 if in_vartree:
283                         return VARDB.findname(self.cpv)
284                 return PORTDB.findname(self.cpv)
285
286         def package_path(self, in_vartree=False):
287                 """Return the path to where the ebuilds and other files reside."""
288
289                 if in_vartree:
290                         return self.dblink.getpath()
291                 return os.sep.join(self.ebuild_path().split(os.sep)[:-1])
292
293         def repo_name(self, fallback=True):
294                 """Determine the repository name.
295
296                 @type fallback: bool
297                 @param fallback: if the repo_name file does not exist, return the
298                         repository name from the path
299                 @rtype: str
300                 @return: output of the repository metadata file, which stores the
301                         repo_name variable, or try to get the name of the repo from
302                         the path.
303                 @raise GentoolkitFatalError: if fallback is False and repo_name is
304                         not specified by the repository.
305                 """
306
307                 try:
308                         return self.environment('repository')
309                 except errors.GentoolkitFatalError:
310                         if fallback:
311                                 return self.package_path().split(os.sep)[-3]
312                         raise
313
314         def use(self):
315                 """Returns the USE flags active at time of installation."""
316
317                 return self.dblink.getstring("USE")
318
319         def use_status(self):
320                 """Returns the USE flags active for installation."""
321
322                 iuse, final_flags = get_flags(self.cpv, final_setting=True)
323                 return final_flags
324
325         def parsed_contents(self):
326                 """Returns the parsed CONTENTS file.
327
328                 @rtype: dict
329                 @return: {'/full/path/to/obj': ['type', 'timestamp', 'md5sum'], ...}
330                 """
331
332                 return self.dblink.getcontents()
333
334         def size(self):
335                 """Estimates the installed size of the contents of this package.
336
337                 @rtype: tuple
338                 @return: (size, number of files in total, number of uncounted files)
339                 """
340
341                 seen = set()
342                 size = n_files = n_uncounted = 0
343                 for f in self.parsed_contents():
344                         try:
345                                 st = os.lstat(f)
346                         except OSError:
347                                 continue
348
349                         # Remove hardlinks by checking for duplicate inodes. Bug #301026.
350                         file_inode = st.st_ino
351                         if file_inode in seen:
352                                 continue
353                         seen.add(file_inode)
354
355                         try:
356                                 size += st.st_size
357                                 n_files += 1
358                         except OSError:
359                                 n_uncounted += 1
360
361                 return (size, n_files, n_uncounted)
362
363         def is_installed(self):
364                 """Returns True if this package is installed (merged)."""
365
366                 return self.dblink.exists()
367
368         def is_overlay(self):
369                 """Returns True if the package is in an overlay."""
370
371                 ebuild, tree = PORTDB.findname2(self.cpv)
372                 if not ebuild:
373                         return None
374                 if self._portdir_path is None:
375                         self._portdir_path = os.path.realpath(settings["PORTDIR"])
376                 return (tree and tree != self._portdir_path)
377
378         def is_masked(self):
379                 """Returns True if this package is masked against installation.
380
381                 @note: We blindly assume that the package actually exists on disk.
382                 """
383
384                 unmasked = PORTDB.xmatch("match-visible", self.cpv)
385                 return self.cpv not in unmasked
386
387
388 class PackageFormatter(object):
389         """When applied to a L{gentoolkit.package.Package} object, determine the
390         location (Portage Tree vs. overlay), install status and masked status. That
391         information can then be easily formatted and displayed.
392
393         Example usage:
394                 >>> from gentoolkit.helpers import find_packages
395                 >>> from gentoolkit.package import PackageFormatter
396                 >>> pkgs = [PackageFormatter(x) for x in find_packages('gcc')]
397                 >>> for pkg in pkgs:
398                 ...     # Only print packages that are installed and from the Portage
399                 ...     # tree
400                 ...     if set('IP').issubset(pkg.location):
401                 ...             print pkg
402                 ...
403                 [IP-] [  ] sys-devel/gcc-4.3.2-r3 (4.3)
404
405         @type pkg: L{gentoolkit.package.Package}
406         @param pkg: package to format
407         @type do_format: bool
408         @param do_format: Whether to format the package name or not.
409                 Essentially C{do_format} should be set to False when piping or when
410                 quiet output is desired. If C{do_format} is False, only the location
411                 attribute will be created to save time.
412         """
413
414         _tmpl_verbose = "[$location] [$mask] $cpv:$slot"
415         _tmpl_quiet = "$cpv"
416
417         def __init__(self, pkg, do_format=True, custom_format=None):
418                 self._pkg = None
419                 self._do_format = do_format
420                 self._str = None
421                 self._location = None
422                 if not custom_format:
423                         if do_format:
424                                 custom_format = self._tmpl_verbose
425                         else:
426                                 custom_format = self._tmpl_quiet
427                 self.tmpl = Template(custom_format)
428                 self.format_vars = LazyItemsDict()
429                 self.pkg = pkg
430
431         def __repr__(self):
432                 return "<%s %s @%#8x>" % (self.__class__.__name__, self.pkg, id(self))
433
434         def __str__(self):
435                 if self._str is None:
436                         self._str = self.tmpl.safe_substitute(self.format_vars)
437                 return self._str
438
439         @property
440         def location(self):
441                 if self._location is None:
442                         self._location = self.format_package_location()
443                 return self._location
444
445         @property
446         def pkg(self):
447                 """Package to format"""
448                 return self._pkg
449
450         @pkg.setter
451         def pkg(self, value):
452                 if self._pkg == value:
453                         return
454                 self._pkg = value
455                 self._location = None
456
457                 fmt_vars = self.format_vars
458                 self.format_vars.clear()
459                 fmt_vars.addLazySingleton("location",
460                         lambda: getattr(self, "location"))
461                 fmt_vars.addLazySingleton("mask", self.format_mask)
462                 fmt_vars.addLazySingleton("mask2", self.format_mask_status2)
463                 fmt_vars.addLazySingleton("cpv", self.format_cpv)
464                 fmt_vars.addLazySingleton("cp", self.format_cpv, "cp")
465                 fmt_vars.addLazySingleton("category", self.format_cpv, "category")
466                 fmt_vars.addLazySingleton("name", self.format_cpv, "name")
467                 fmt_vars.addLazySingleton("version", self.format_cpv, "version")
468                 fmt_vars.addLazySingleton("revision", self.format_cpv, "revision")
469                 fmt_vars.addLazySingleton("fullversion", self.format_cpv,
470                         "fullversion")
471                 fmt_vars.addLazySingleton("slot", self.format_slot)
472                 fmt_vars.addLazySingleton("repo", self.pkg.repo_name)
473                 fmt_vars.addLazySingleton("keywords", self.format_keywords)
474
475         def format_package_location(self):
476                 """Get the install status (in /var/db/?) and origin (from an overlay
477                 and the Portage tree?).
478
479                 @rtype: str
480                 @return: one of:
481                         'I--' : Installed but ebuild doesn't exist on system anymore
482                         '-P-' : Not installed and from the Portage tree
483                         '--O' : Not installed and from an overlay
484                         'IP-' : Installed and from the Portage tree
485                         'I-O' : Installed and from an overlay
486                 """
487
488                 result = ['-', '-', '-']
489
490                 if self.pkg.is_installed():
491                         result[0] = 'I'
492
493                 overlay = self.pkg.is_overlay()
494                 if overlay is None:
495                         pass
496                 elif overlay:
497                         result[2] = 'O'
498                 else:
499                         result[1] = 'P'
500
501                 return ''.join(result)
502
503         def format_mask_status(self):
504                 """Get the mask status of a given package.
505
506                 @rtype: tuple: (int, list)
507                 @return: int = an index for this list:
508                         ["  ", " ~", " -", "M ", "M~", "M-", "??"]
509                         0 = not masked
510                         1 = keyword masked
511                         2 = arch masked
512                         3 = hard masked
513                         4 = hard and keyword masked,
514                         5 = hard and arch masked
515                         6 = ebuild doesn't exist on system anymore
516
517                         list = original output of portage.getmaskingstatus
518                 """
519
520                 result = 0
521                 masking_status = self.pkg.mask_status()
522                 if masking_status is None:
523                         return (6, [])
524
525                 if ("~%s keyword" % self.pkg.settings("ARCH")) in masking_status:
526                         result += 1
527                 if "missing keyword" in masking_status:
528                         result += 2
529                 if set(('profile', 'package.mask')).intersection(masking_status):
530                         result += 3
531
532                 return (result, masking_status)
533
534         def format_mask_status2(self):
535                 """Get the mask status of a given package.
536                 """
537                 mask = self.pkg.mask_status()
538                 if mask:
539                         return pp.masking(mask)
540                 else:
541                         arch = self.pkg.settings("ARCH")
542                         keywords = self.pkg.environment('KEYWORDS')
543                         mask =  [determine_keyword(arch,
544                                 portage.settings["ACCEPT_KEYWORDS"],
545                                 keywords)]
546                 return pp.masking(mask)
547
548         def format_mask(self):
549                 maskmodes = ['  ', ' ~', ' -', 'M ', 'M~', 'M-', '??']
550                 maskmode = maskmodes[self.format_mask_status()[0]]
551                 return pp.keyword(
552                         maskmode,
553                         stable=not maskmode.strip(),
554                         hard_masked=set(('M', '?', '-')).intersection(maskmode)
555                 )
556
557         def format_cpv(self, attr=None):
558                 if attr is None:
559                         value = self.pkg.cpv
560                 else:
561                         value = getattr(self.pkg, attr)
562                 if self._do_format:
563                         return pp.cpv(value)
564                 else:
565                         return value
566
567         def format_slot(self):
568                 value = self.pkg.environment("SLOT")
569                 if self._do_format:
570                         return pp.slot(value)
571                 else:
572                         return value
573
574         def format_keywords(self):
575                 value = self.pkg.environment("KEYWORDS")
576                 if self._do_format:
577                         return pp.keyword(value)
578                 else:
579                         return value
580
581
582 # vim: set ts=4 sw=4 tw=79: