3 # Copyright(c) 2004, Karl Trygve Kalleberg <karltk@gentoo.org>
4 # Copyright(c) 2004-2009, Gentoo Foundation
6 # Licensed under the GNU General Public License, v2
17 from portage.versions import catpkgsplit, vercmp
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
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
33 def __init__(self, arg):
38 if self.cpv[0] in ('<', '>'):
39 if self.cpv[1] == '=':
40 self.operator = self.cpv[:2]
41 self.cpv = self.cpv[2:]
43 self.operator = self.cpv[0]
44 self.cpv = self.cpv[1:]
45 elif self.cpv[0] == '=':
46 if self.cpv[-1] == '*':
48 self.cpv = self.cpv[1:-1]
50 self.cpv = self.cpv[1:]
52 elif self.cpv[0] == '~':
54 self.cpv = self.cpv[1:]
57 self._cpv = '=%s' % self._cpv
59 if not portage.dep.isvalidatom(self._cpv):
60 raise errors.GentoolkitInvalidCPV(self._cpv)
62 cpv_split = portage.catpkgsplit(self.cpv)
65 self.key = "/".join(cpv_split[:2])
67 # catpkgsplit returned None
68 raise errors.GentoolkitInvalidCPV(self._cpv)
70 cpv_split = list(cpv_split)
71 if cpv_split[0] == 'null':
73 if cpv_split[3] == 'r0':
75 self.cpv_split = cpv_split
76 self._scpv = self.cpv_split # XXX: namespace compatability 03/09
79 self._settings = settings
80 self._settingslock = settingslock
81 self._portdir_path = os.path.realpath(settings["PORTDIR"])
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]
88 self.fullversion = self.version
90 self.fullversion = "%s-%s" % (self.version, self.revision)
93 return "<%s %s @%#8x>" % (self.__class__.__name__, self._cpv, id(self))
95 def __eq__(self, other):
96 return hash(self) == hash(other)
98 def __ne__(self, other):
99 return hash(self) != hash(other)
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__))
106 if self.category != other.category:
107 return self.category < other.category
108 elif self.name != other.name:
109 return self.name < other.name
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)
120 def __gt__(self, other):
121 return not self.__lt__(other)
124 return hash(self._cpv)
126 def __contains__(self, key):
127 return key in self._cpv
133 """Returns base name of package, no category nor version"""
136 def get_version(self):
137 """Returns version of package, with revision number"""
138 return self.fullversion
140 def get_category(self):
141 """Returns category of package"""
144 def get_settings(self, key):
145 """Returns the value of the given key for this package (useful
146 for package.* files."""
148 self._settingslock.acquire()
149 self._settings.setcpv(self.cpv)
150 result = self._settings[key]
152 self._settingslock.release()
156 """Returns full Category/Package-Version string"""
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)
165 result = [self.get_env_var('PROVIDE')]
170 def get_dependants(self):
171 """Retrieves a list of CPVs for all packages depending on this one"""
172 raise NotImplementedError("Not implemented yet!")
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), ...]
178 # Try to use the portage tree first, since emerge only uses the tree
179 # when calculating dependencies
181 rdepends = self.get_env_var("RDEPEND", PORTDB).split()
183 rdepends = self.get_env_var("RDEPEND", VARDB).split()
184 return self._parse_deps(rdepends)[0]
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), ...]
190 # Try to use the portage tree first, since emerge only uses the tree
191 # when calculating dependencies
193 depends = self.get_env_var("DEPEND", PORTDB).split()
195 depends = self.get_env_var("DEPEND", VARDB).split()
196 return self._parse_deps(depends)[0]
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), ...]
202 # Try to use the portage tree first, since emerge only uses the tree
203 # when calculating dependencies
205 postmerge_deps = self.get_env_var("PDEPEND", PORTDB).split()
207 postmerge_deps = self.get_env_var("PDEPEND", VARDB).split()
208 return self._parse_deps(postmerge_deps)[0]
210 def intersects(self, other):
211 """Check if a passed in package atom "intersects" this atom.
215 Two atoms "intersect" if a package can be constructed that
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"
223 @type other: L{gentoolkit.package.Package}
224 @param other: other package to compare
225 @see: pkgcore.ebuild.atom.py
227 # Our "key" (cat/pkg) must match exactly:
228 if self.key != other.key:
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)):
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)
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)
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))
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)
263 # If we get here at least one of us is a <, <=, > or >=:
264 if self.operator in ('<', '<=', '>', '>='):
265 ranged, other = self, other
267 ranged, other = other, self
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)
275 VersionMatch(other).match(ranged) and
276 VersionMatch(ranged).match(other))
278 if other.operator == '~':
279 # Other definitely matches its own version. If ranged also
281 if VersionMatch(ranged).match(other):
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)
290 if other.operator == '=*':
291 # a glob match definitely matches its own version, so if
292 # ranged does too we're done:
294 ranged.operator, ranged.version, ranged.revision).match(other):
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:
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)
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)
312 # Handled all possible ops.
313 raise NotImplementedError(
314 'Someone added an operator without adding it to intersects')
317 def _parse_deps(self,deps,curuse=[],level=0):
318 # store (comparator, [use predicates], cpv)
320 comparators = ["~","<",">","=","<=",">="]
328 tok = tok.replace("?","")
329 sr,l = self._parse_deps(deps[i+2:],curuse=curuse+[tok],level=level+1)
334 sr,l = self._parse_deps(deps[i+2:],curuse,level=level+1)
338 # conjunction, like in "|| ( ( foo bar ) baz )" => recurse
340 sr,l = self._parse_deps(deps[i+1:],curuse,level=level+1)
344 # pkg block "!foo/bar" => ignore it
348 # pick out comparator, if any
350 for c in comparators:
354 r.append((cmp,curuse,tok))
358 def is_installed(self):
359 """Returns True if this package is installed (merged)"""
360 return VARDB.cpv_exists(self.cpv)
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
368 """Returns true if this package is masked against installation.
369 Note: We blindly assume that the package actually exists on disk
371 unmasked = portage.portdb.xmatch("match-visible", self.cpv)
372 return self.cpv not in unmasked
374 def get_ebuild_path(self, in_vartree=False):
375 """Returns the complete path to the .ebuild file"""
377 return VARDB.getebuildpath(self.cpv)
378 return PORTDB.findname(self.cpv)
380 def get_package_path(self):
381 """Returns the path to where the ChangeLog, Manifest, .ebuild files
383 ebuild_path = self.get_ebuild_path()
384 path_split = ebuild_path.split("/")
386 return os.sep.join(path_split[:-1])
388 def get_env_var(self, var, tree=None):
389 """Returns one of the predefined env vars DEPEND, RDEPEND,
393 if not self.is_installed():
395 result = tree.aux_get(self.cpv, [var])
397 raise errors.GentoolkitFatalError("Could not find the package tree")
399 raise errors.GentoolkitFatalError("Should only get one element!")
402 def get_use_flags(self):
403 """Returns the USE flags active at time of installation"""
405 if self.is_installed():
406 return self._db.getfile("USE")
408 def get_contents(self):
409 """Returns the full contents, as a dictionary, in the form
410 ['/bin/foo' : [ 'obj', '1052505381', '45ca8b89751...' ], ... ]"""
412 if self.is_installed():
413 return self._db.getcontents()
417 """Estimates the installed size of the contents of this package,
419 Returns (size, number of files in total, number of uncounted files)
421 contents = self.get_contents()
427 size += os.lstat(x).st_size
431 return (size, files, uncounted)
434 """Internal helper function; loads package information from disk,
438 self._db = portage.dblink(
440 "%s-%s" % (self.name, self.fullversion),
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.
452 >>> from gentoolkit.helpers2 import find_packages
453 >>> from gentoolkit.package import PackageFormatter
454 >>> pkgs = [PackageFormatter(x) for x in find_packages('gcc')]
456 ... # Only print packages that are installed and from the Portage
458 ... if set('IP').issubset(pkg.location):
461 [IP-] [ ] sys-devel/gcc-4.3.2-r3 (4.3)
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.
472 def __init__(self, pkg, format=True):
474 maskmodes = [' ', ' ~', ' -', 'M ', 'M~', 'M-']
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()
485 return "<%s %s @%#8x>" % (self.__class__.__name__, self.pkg, id(self))
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),
498 def get_package_location(self):
499 """Get the physical location of a package on disk.
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
509 result = ['-', '-', '-']
511 if self.pkg.is_installed():
513 if self.pkg.is_overlay():
518 return ''.join(result)
520 def get_mask_status(self):
521 """Get the mask status of a given package.
523 @type pkg: L{gentoolkit.package.Package}
524 @param pkg: pkg to get mask status of
526 @param arch: output of gentoolkit.settings["ARCH"]
528 @return: an index for this list: [" ", " ~", " -", "M ", "M~", "M-"]
533 4 = hard and keyword masked,
534 5 = hard and arch masked
537 keywords = self.pkg.get_env_var("KEYWORDS").split()
539 if self.pkg.is_masked():
541 if ("~%s" % self.arch) in keywords:
543 elif ("-%s" % self.arch) in keywords or "-*" in keywords: