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