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