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