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