Apply updates from genscripts repo
[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-2009, Gentoo Foundation
5 #
6 # Licensed under the GNU General Public License, v2
7 #
8 # $Header$
9
10 # =======
11 # Imports 
12 # =======
13
14 import os
15
16 import portage
17 from portage.versions import catpkgsplit, vercmp
18
19 import gentoolkit.pprinter as pp
20 from gentoolkit import settings, settingslock, PORTDB, VARDB
21 from gentoolkit import errors
22 from gentoolkit.versionmatch import VersionMatch
23
24 # =======
25 # Classes
26 # =======
27
28 class Package(object):
29         """Package descriptor. Contains convenience functions for querying the
30         state of a package, its contents, name manipulation, ebuild info and
31         similar."""
32
33         def __init__(self, arg):
34
35                 self._cpv = arg
36                 self.cpv = self._cpv
37
38                 if self.cpv[0] in ('<', '>'):
39                         if self.cpv[1] == '=':
40                                 self.operator = self.cpv[:2]
41                                 self.cpv = self.cpv[2:]
42                         else:
43                                 self.operator = self.cpv[0]
44                                 self.cpv = self.cpv[1:]
45                 elif self.cpv[0] == '=':
46                         if self.cpv[-1] == '*':
47                                 self.operator = '=*'
48                                 self.cpv = self.cpv[1:-1]
49                         else:
50                                 self.cpv = self.cpv[1:]
51                                 self.operator = '='
52                 elif self.cpv[0] == '~':
53                         self.operator = '~'
54                         self.cpv = self.cpv[1:]
55                 else:
56                         self.operator = '='
57                         self._cpv = '=%s' % self._cpv
58
59                 if not portage.dep.isvalidatom(self._cpv):
60                         raise errors.GentoolkitInvalidCPV(self._cpv)
61
62                 cpv_split = portage.catpkgsplit(self.cpv)
63
64                 try:
65                         self.key = "/".join(cpv_split[:2])
66                 except TypeError:
67                         # catpkgsplit returned None
68                         raise errors.GentoolkitInvalidCPV(self._cpv)
69
70                 cpv_split = list(cpv_split)
71                 if cpv_split[0] == 'null':
72                         cpv_split[0] = ''
73                 if cpv_split[3] == 'r0':
74                         cpv_split[3] = ''
75                 self.cpv_split = cpv_split
76                 self._scpv = self.cpv_split # XXX: namespace compatability 03/09
77
78                 self._db = None
79                 self._settings = settings
80                 self._settingslock = settingslock
81                 self._portdir_path = os.path.realpath(settings["PORTDIR"])
82
83                 self.category = self.cpv_split[0]
84                 self.name = self.cpv_split[1]
85                 self.version = self.cpv_split[2]
86                 self.revision = self.cpv_split[3]
87                 if not self.revision:
88                         self.fullversion = self.version
89                 else:
90                         self.fullversion = "%s-%s" % (self.version, self.revision)
91
92         def __repr__(self):
93                 return "<%s %s @%#8x>" % (self.__class__.__name__, self._cpv, id(self))
94
95         def __eq__(self, other):
96                 return hash(self) == hash(other)
97
98         def __ne__(self, other):
99                 return hash(self) != hash(other)
100
101         def __lt__(self, other):
102                 if not isinstance(other, self.__class__):
103                         raise TypeError("other isn't of %s type, is %s" %
104                                 (self.__class__, other.__class__))
105
106                 if self.category != other.category:
107                         return self.category < other.category
108                 elif self.name != other.name:
109                         return self.name < other.name
110                 else:
111                         # FIXME: this cmp() hack is for vercmp not using -1,0,1
112                         # See bug 266493; this was fixed in portage-2.2_rc31
113                         #return portage.vercmp(self.fullversion, other.fullversion)
114                         result = cmp(portage.vercmp(self.fullversion, other.fullversion), 0)
115                         if result == -1:
116                                 return True
117                         else:
118                                 return False
119
120         def __gt__(self, other):
121                 return not self.__lt__(other)
122
123         def __hash__(self):
124                 return hash(self._cpv)
125
126         def __contains__(self, key):
127                 return key in self._cpv
128         
129         def __str__(self):
130                 return self._cpv
131
132         def get_name(self):
133                 """Returns base name of package, no category nor version"""
134                 return self.name
135
136         def get_version(self):
137                 """Returns version of package, with revision number"""
138                 return self.fullversion
139
140         def get_category(self):
141                 """Returns category of package"""
142                 return self.category
143
144         def get_settings(self, key):
145                 """Returns the value of the given key for this package (useful 
146                 for package.* files."""
147                 try:
148                         self._settingslock.acquire()
149                         self._settings.setcpv(self.cpv)
150                         result = self._settings[key]
151                 finally:
152                         self._settingslock.release()
153                 return result
154
155         def get_cpv(self):
156                 """Returns full Category/Package-Version string"""
157                 return self.cpv
158
159         def get_provide(self):
160                 """Return a list of provides, if any"""
161                 if self.is_installed():
162                         result = VARDB.get_provide(self.cpv)
163                 else:
164                         try:
165                                 result = [self.get_env_var('PROVIDE')]
166                         except KeyError:
167                                 result = []
168                 return result
169
170         def get_dependants(self):
171                 """Retrieves a list of CPVs for all packages depending on this one"""
172                 raise NotImplementedError("Not implemented yet!")
173
174         def get_runtime_deps(self):
175                 """Returns a linearised list of first-level run time dependencies for 
176                 this package, on the form [(comparator, [use flags], cpv), ...]
177                 """
178                 # Try to use the portage tree first, since emerge only uses the tree 
179                 # when calculating dependencies
180                 try:
181                         rdepends = self.get_env_var("RDEPEND", PORTDB).split()
182                 except KeyError:
183                         rdepends = self.get_env_var("RDEPEND", VARDB).split()
184                 return self._parse_deps(rdepends)[0]
185
186         def get_compiletime_deps(self):
187                 """Returns a linearised list of first-level compile time dependencies
188                 for this package, on the form [(comparator, [use flags], cpv), ...]
189                 """
190                 # Try to use the portage tree first, since emerge only uses the tree 
191                 # when calculating dependencies
192                 try:
193                         depends = self.get_env_var("DEPEND", PORTDB).split()
194                 except KeyError:
195                         depends = self.get_env_var("DEPEND", VARDB).split()
196                 return self._parse_deps(depends)[0]
197
198         def get_postmerge_deps(self):
199                 """Returns a linearised list of first-level post merge dependencies 
200                 for this package, on the form [(comparator, [use flags], cpv), ...]
201                 """
202                 # Try to use the portage tree first, since emerge only uses the tree 
203                 # when calculating dependencies
204                 try:
205                         postmerge_deps = self.get_env_var("PDEPEND", PORTDB).split()
206                 except KeyError:
207                         postmerge_deps = self.get_env_var("PDEPEND", VARDB).split()
208                 return self._parse_deps(postmerge_deps)[0]
209
210         def intersects(self, other):
211                 """Check if a passed in package atom "intersects" this atom.
212
213                 Lifted from pkgcore.
214
215                 Two atoms "intersect" if a package can be constructed that
216                 matches both:
217                   - if you query for just "dev-lang/python" it "intersects" both
218                         "dev-lang/python" and ">=dev-lang/python-2.4"
219                   - if you query for "=dev-lang/python-2.4" it "intersects"
220                         ">=dev-lang/python-2.4" and "dev-lang/python" but not
221                         "<dev-lang/python-2.3"
222
223                 @type other: L{gentoolkit.package.Package}
224                 @param other: other package to compare
225                 @see: pkgcore.ebuild.atom.py
226                 """
227                 # Our "key" (cat/pkg) must match exactly:
228                 if self.key != other.key:
229                         return False
230
231                 # If we are both "unbounded" in the same direction we intersect:
232                 if (('<' in self.operator and '<' in other.operator) or
233                         ('>' in self.operator and '>' in other.operator)):
234                         return True
235
236                 # If one of us is an exact match we intersect if the other matches it:
237                 if self.operator == '=':
238                         if other.operator == '=*':
239                                 return self.fullversion.startswith(other.fullversion)
240                         return VersionMatch(other).match(self)
241                 if other.operator == '=':
242                         if self.operator == '=*':
243                                 return other.fullversion.startswith(self.fullversion)
244                         return VersionMatch(self).match(other)
245
246                 # If we are both ~ matches we match if we are identical:
247                 if self.operator == other.operator == '~':
248                         return (self.version == other.version and
249                                         self.revision == other.revision)
250
251                 # If we are both glob matches we match if one of us matches the other.
252                 if self.operator == other.operator == '=*':
253                         return (self.fullver.startswith(other.fullver) or
254                                         other.fullver.startswith(self.fullver))
255
256                 # If one of us is a glob match and the other a ~ we match if the glob
257                 # matches the ~ (ignoring a revision on the glob):
258                 if self.operator == '=*' and other.operator == '~':
259                         return other.fullversion.startswith(self.version)
260                 if other.operator == '=*' and self.operator == '~':
261                         return self.fullversion.startswith(other.version)
262
263                 # If we get here at least one of us is a <, <=, > or >=:
264                 if self.operator in ('<', '<=', '>', '>='):
265                         ranged, other = self, other
266                 else:
267                         ranged, other = other, self
268
269                 if '<' in other.operator or '>' in other.operator:
270                         # We are both ranged, and in the opposite "direction" (or
271                         # we would have matched above). We intersect if we both
272                         # match the other's endpoint (just checking one endpoint
273                         # is not enough, it would give a false positive on <=2 vs >2)
274                         return (
275                                 VersionMatch(other).match(ranged) and
276                                 VersionMatch(ranged).match(other))
277
278                 if other.operator == '~':
279                         # Other definitely matches its own version. If ranged also
280                         # does we're done:
281                         if VersionMatch(ranged).match(other):
282                                 return True
283                         # The only other case where we intersect is if ranged is a
284                         # > or >= on other's version and a nonzero revision. In
285                         # that case other will match ranged. Be careful not to
286                         # give a false positive for ~2 vs <2 here:
287                         return ranged.operator in ('>', '>=') and VersionMatch(
288                                 other.operator, other.version, other.revision).match(ranged)
289
290                 if other.operator == '=*':
291                         # a glob match definitely matches its own version, so if
292                         # ranged does too we're done:
293                         if VersionMatch(
294                                 ranged.operator, ranged.version, ranged.revision).match(other):
295                                 return True
296                         if '<' in ranged.operator:
297                                 # If other.revision is not defined then other does not
298                                 # match anything smaller than its own fullver:
299                                 if not other.revision:
300                                         return False
301
302                                 # If other.revision is defined then we can always
303                                 # construct a package smaller than other.fullver by
304                                 # tagging e.g. an _alpha1 on.
305                                 return ranged.fullversion.startswith(other.version)
306                         else:
307                                 # Remaining cases where this intersects: there is a
308                                 # package greater than ranged.fullver and
309                                 # other.fullver that they both match.
310                                 return ranged.fullversion.startswith(other.version)
311
312                 # Handled all possible ops.
313                 raise NotImplementedError(
314                         'Someone added an operator without adding it to intersects')
315
316
317         def _parse_deps(self,deps,curuse=[],level=0):
318                 # store (comparator, [use predicates], cpv)
319                 r = []
320                 comparators = ["~","<",">","=","<=",">="]
321                 end = len(deps)
322                 i = 0
323                 while i < end:
324                         tok = deps[i]
325                         if tok == ')':
326                                 return r,i
327                         if tok[-1] == "?":
328                                 tok = tok.replace("?","")
329                                 sr,l = self._parse_deps(deps[i+2:],curuse=curuse+[tok],level=level+1)
330                                 r += sr
331                                 i += l + 3
332                                 continue
333                         if tok == "||":
334                                 sr,l = self._parse_deps(deps[i+2:],curuse,level=level+1)
335                                 r += sr
336                                 i += l + 3
337                                 continue
338                         # conjunction, like in "|| ( ( foo bar ) baz )" => recurse
339                         if tok == "(":
340                                 sr,l = self._parse_deps(deps[i+1:],curuse,level=level+1)
341                                 r += sr
342                                 i += l + 2
343                                 continue
344                         # pkg block "!foo/bar" => ignore it
345                         if tok[0] == "!":
346                                 i += 1
347                                 continue
348                         # pick out comparator, if any
349                         cmp = ""
350                         for c in comparators:
351                                 if tok.find(c) == 0:
352                                         cmp = c
353                         tok = tok[len(cmp):]
354                         r.append((cmp,curuse,tok))
355                         i += 1
356                 return r,i
357
358         def is_installed(self):
359                 """Returns True if this package is installed (merged)"""
360                 return VARDB.cpv_exists(self.cpv)
361
362         def is_overlay(self):
363                 """Returns True if the package is in an overlay."""
364                 ebuild, tree = portage.portdb.findname2(self.cpv)
365                 return tree != self._portdir_path
366
367         def is_masked(self):
368                 """Returns true if this package is masked against installation. 
369                 Note: We blindly assume that the package actually exists on disk
370                 somewhere."""
371                 unmasked = portage.portdb.xmatch("match-visible", self.cpv)
372                 return self.cpv not in unmasked
373
374         def get_ebuild_path(self, in_vartree=False):
375                 """Returns the complete path to the .ebuild file"""
376                 if in_vartree:
377                         return VARDB.getebuildpath(self.cpv)
378                 return PORTDB.findname(self.cpv)
379
380         def get_package_path(self):
381                 """Returns the path to where the ChangeLog, Manifest, .ebuild files
382                 reside"""
383                 ebuild_path = self.get_ebuild_path()
384                 path_split = ebuild_path.split("/")
385                 if path_split:
386                         return os.sep.join(path_split[:-1])
387
388         def get_env_var(self, var, tree=None):
389                 """Returns one of the predefined env vars DEPEND, RDEPEND,
390                 SRC_URI,...."""
391                 if tree == None:
392                         tree = VARDB
393                         if not self.is_installed():
394                                 tree = PORTDB
395                 result = tree.aux_get(self.cpv, [var])
396                 if not result:
397                         raise errors.GentoolkitFatalError("Could not find the package tree")
398                 if len(result) != 1:
399                         raise errors.GentoolkitFatalError("Should only get one element!")
400                 return result[0]
401
402         def get_use_flags(self):
403                 """Returns the USE flags active at time of installation"""
404                 self._initdb()
405                 if self.is_installed():
406                         return self._db.getfile("USE")
407
408         def get_contents(self):
409                 """Returns the full contents, as a dictionary, in the form
410                 ['/bin/foo' : [ 'obj', '1052505381', '45ca8b89751...' ], ... ]"""
411                 self._initdb()
412                 if self.is_installed():
413                         return self._db.getcontents()
414                 return {}               
415
416         def size(self):
417                 """Estimates the installed size of the contents of this package,
418                 if possible.
419                 Returns (size, number of files in total, number of uncounted files)
420                 """
421                 contents = self.get_contents()
422                 size = 0
423                 uncounted = 0
424                 files = 0
425                 for x in contents:
426                         try:
427                                 size += os.lstat(x).st_size
428                                 files += 1
429                         except OSError:
430                                 uncounted += 1
431                 return (size, files, uncounted)
432
433         def _initdb(self):
434                 """Internal helper function; loads package information from disk,
435                 when necessary.
436                 """
437                 if not self._db:
438                         self._db = portage.dblink(
439                                 self.category,
440                                 "%s-%s" % (self.name, self.fullversion),
441                                 settings["ROOT"],
442                                 settings
443                         )
444
445
446 class PackageFormatter(object):
447         """When applied to a L{gentoolkit.package.Package} object, determine the
448         location (Portage Tree vs. overlay), install status and masked status. That
449         information can then be easily formatted and displayed.
450         
451         Example usage:
452                 >>> from gentoolkit.helpers2 import find_packages
453                 >>> from gentoolkit.package import PackageFormatter
454                 >>> pkgs = [PackageFormatter(x) for x in find_packages('gcc')]
455                 >>> for pkg in pkgs:
456                 ...     # Only print packages that are installed and from the Portage
457                 ...     # tree
458                 ...     if set('IP').issubset(pkg.location):
459                 ...             print pkg
460                 ... 
461                 [IP-] [  ] sys-devel/gcc-4.3.2-r3 (4.3)
462
463         @type pkg: L{gentoolkit.package.Package}
464         @param pkg: package to format
465         @type format: L{bool}
466         @param format: Whether to format the package name or not. 
467                 Essentially C{format} should be set to False when piping or when
468                 quiet output is desired. If C{format} is False, only the location
469                 attribute will be created to save time.
470         """
471
472         def __init__(self, pkg, format=True):
473                 location = ''
474                 maskmodes = ['  ', ' ~', ' -', 'M ', 'M~', 'M-']
475
476                 self.pkg = pkg
477                 self.format = format
478                 if format:
479                         self.arch = settings["ARCH"]
480                         self.mask = maskmodes[self.get_mask_status()]
481                         self.slot = pkg.get_env_var("SLOT")
482                 self.location = self.get_package_location()
483
484         def __repr__(self):
485                 return "<%s %s @%#8x>" % (self.__class__.__name__, self.pkg, id(self))
486
487         def __str__(self):
488                 if self.format:
489                         return "[%(location)s] [%(mask)s] %(package)s (%(slot)s)" % {
490                                 'location': self.location,
491                                 'mask': pp.maskflag(self.mask),
492                                 'package': pp.cpv(self.pkg.cpv),
493                                 'slot': self.slot
494                         }
495                 else:
496                         return self.pkg.cpv
497
498         def get_package_location(self):
499                 """Get the physical location of a package on disk.
500
501                 @rtype: str
502                 @return: one of:
503                         '-P-' : Not installed and from the Portage tree
504                         '--O' : Not installed and from an overlay
505                         'IP-' : Installed and from the Portage tree
506                         'I-O' : Installed and from an overlay
507                 """
508
509                 result = ['-', '-', '-']
510
511                 if self.pkg.is_installed():
512                         result[0] = 'I'
513                 if self.pkg.is_overlay():
514                         result[2] = 'O'
515                 else:
516                         result[1] = 'P'
517
518                 return ''.join(result)
519
520         def get_mask_status(self):
521                 """Get the mask status of a given package. 
522
523                 @type pkg: L{gentoolkit.package.Package}
524                 @param pkg: pkg to get mask status of
525                 @type arch: str
526                 @param arch: output of gentoolkit.settings["ARCH"]
527                 @rtype: int
528                 @return: an index for this list: ["  ", " ~", " -", "M ", "M~", "M-"]
529                         0 = not masked
530                         1 = keyword masked
531                         2 = arch masked
532                         3 = hard masked
533                         4 = hard and keyword masked,
534                         5 = hard and arch masked
535                 """
536
537                 keywords = self.pkg.get_env_var("KEYWORDS").split()
538                 mask_status = 0
539                 if self.pkg.is_masked():
540                         mask_status += 3
541                 if ("~%s" % self.arch) in keywords:
542                         mask_status += 1
543                 elif ("-%s" % self.arch) in keywords or "-*" in keywords:
544                         mask_status += 2
545
546                 return mask_status