aboutsummaryrefslogtreecommitdiff
blob: 117342a556864d51fd6e3d32ad50800f4095f835 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# Copyright(c) 2009, Gentoo Foundation
#
# Licensed under the GNU General Public License, v2
#
# $Header: $

"""Provides a class for easy calculating dependencies for a given CPV."""

__docformat__ = 'epytext'
__all__ = ('Dependencies',)

# =======
# Imports
# =======

import portage
from portage.dep import paren_reduce

from gentoolkit import errors
from gentoolkit.atom import Atom
from gentoolkit.helpers import uniqify
from gentoolkit.query import Query

# =======
# Classes
# =======

class Dependencies(Query):
	"""Access a package's dependencies and reverse dependencies.

	Example usage:
		>>> from gentoolkit.dependencies import Dependencies
		>>> portage = Dependencies('sys-apps/portage-9999')
		>>> portage
		<Dependencies 'sys-apps/portage-9999'>
		>>> # All methods return gentoolkit.atom.Atom instances
		... portage.get_depend()
		... # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
		[<Atom 'python3? =dev-lang/python-3*'>,
		 <Atom '!python3? >=dev-lang/python-2.7'>, ...]

	"""
	def __init__(self, query, parser=None):
		Query.__init__(self, query)
		self.use = []
		self.depatom = str()

		# Allow a custom parser function:
		self.parser = parser if parser else self._parser

	def __eq__(self, other):
		if self.atom != other.atom:
			return False
		else:
			return True

	def __ne__(self, other):
		return not self == other

	def __hash__(self):
		return hash((self.atom, self.depatom, tuple(self.use)))

	def __repr__(self):
		return "<%s %r>" % (self.__class__.__name__, self.atom)

	def environment(self, envvars):
		"""Returns predefined env vars DEPEND, SRC_URI, etc."""

		# Try to use the Portage tree first, since emerge only uses the tree
		# when calculating dependencies
		try:
			result = portage.db[portage.root]["porttree"].dbapi.aux_get(self.cpv, envvars)
		except KeyError:
			try:
				result = portage.db[portage.root]["vartree"].dbapi.aux_get(self.cpv, envvars)
			except KeyError:
				return []
		return result

	def get_depend(self):
		"""Get the contents of DEPEND and parse it with self.parser."""

		try:
			return self.parser(self.environment(('DEPEND',))[0])
		except portage.exception.InvalidPackageName as err:
			raise errors.GentoolkitInvalidCPV(err)

	def get_pdepend(self):
		"""Get the contents of PDEPEND and parse it with self.parser."""

		try:
			return self.parser(self.environment(('PDEPEND',))[0])
		except portage.exception.InvalidPackageName as err:
			raise errors.GentoolkitInvalidCPV(err)

	def get_rdepend(self):
		"""Get the contents of RDEPEND and parse it with self.parser."""

		try:
			return self.parser(self.environment(('RDEPEND',))[0])
		except portage.exception.InvalidPackageName as err:
			raise errors.GentoolkitInvalidCPV(err)

	def get_all_depends(self):
		"""Get the contents of ?DEPEND and parse it with self.parser."""

		env_vars = ('DEPEND', 'PDEPEND', 'RDEPEND')
		try:
			return self.parser(' '.join(self.environment(env_vars)))
		except portage.exception.InvalidPackageName as err:
			raise errors.GentoolkitInvalidCPV(err)

	def graph_depends(
		self,
		max_depth=1,
		printer_fn=None,
		# The rest of these are only used internally:
		depth=1,
		seen=None,
		depcache=None,
		result=None
	):
		"""Graph direct dependencies for self.

		Optionally gather indirect dependencies.

		@type max_depth: int
		@keyword max_depth: Maximum depth to recurse if.
			<1 means no maximum depth
			>0 means recurse only this depth;
		@type printer_fn: callable
		@keyword printer_fn: If None, no effect. If set, it will be applied to
			each result.
		@rtype: list
		@return: [(depth, pkg), ...]
		"""
		if seen is None:
			seen = set()
		if depcache is None:
			depcache = dict()
		if result is None:
			result = list()

		pkgdep = None
		deps = self.get_all_depends()
		for dep in deps:
			if dep.atom in depcache:
				continue
			try:
				pkgdep = depcache[dep.atom]
			except KeyError:
				pkgdep = Query(dep.atom).find_best()
				depcache[dep.atom] = pkgdep
			if not pkgdep:
				continue
			elif pkgdep.cpv in seen:
				continue
			if depth <= max_depth or max_depth == 0:
				if printer_fn is not None:
					printer_fn(depth, pkgdep, dep)
				result.append((depth,pkgdep))

				seen.add(pkgdep.cpv)
				if depth < max_depth or max_depth == 0:
					# result is passed in and added to directly
					# so rdeps is disposable
					rdeps = pkgdep.deps.graph_depends(  # noqa
							max_depth=max_depth,
							printer_fn=printer_fn,
							# The rest of these are only used internally:
							depth=depth+1,
							seen=seen,
							depcache=depcache,
							result=result
						)
		return result

	def graph_reverse_depends(
		self,
		pkgset=None,
		max_depth=-1,
		only_direct=True,
		printer_fn=None,
		# The rest of these are only used internally:
		depth=0,
		depcache=None,
		seen=None,
		result=None
	):
		"""Graph direct reverse dependencies for self.

		Example usage:
			>>> from gentoolkit.dependencies import Dependencies
			>>> ffmpeg = Dependencies('media-video/ffmpeg-9999')
			>>> # I only care about installed packages that depend on me:
			... from gentoolkit.helpers import get_installed_cpvs
			>>> # I want to pass in a sorted list. We can pass strings or
			... # Package or Atom types, so I'll use Package to sort:
			... from gentoolkit.package import Package
			>>> installed = sorted(get_installed_cpvs())
			>>> deptree = ffmpeg.graph_reverse_depends(
			...     only_direct=False,  # Include indirect revdeps
			...     pkgset=installed)   # from installed pkgset
			>>> len(deptree)
			24

		@type pkgset: iterable
		@keyword pkgset: sorted pkg cpv strings or anything sublassing
			L{gentoolkit.cpv.CPV} to use for calculate our revdep graph.
		@type max_depth: int
		@keyword max_depth: Maximum depth to recurse if only_direct=False.
			-1 means no maximum depth;
			 0 is the same as only_direct=True;
			>0 means recurse only this many times;
		@type only_direct: bool
		@keyword only_direct: to recurse or not to recurse
		@type printer_fn: callable
		@keyword printer_fn: If None, no effect. If set, it will be applied to
			each L{gentoolkit.atom.Atom} object as it is added to the results.
		@rtype: list
		@return: L{gentoolkit.dependencies.Dependencies} objects
		"""
		if not pkgset:
			err = ("%s kwarg 'pkgset' must be set. "
				"Can be list of cpv strings or any 'intersectable' object.")
			raise errors.GentoolkitFatalError(err % (self.__class__.__name__,))

		if depcache is None:
			depcache = dict()
		if seen is None:
			seen = set()
		if result is None:
			result = list()

		if depth == 0:
			pkgset = tuple(Dependencies(x) for x in pkgset)

		pkgdep = None
		for pkgdep in pkgset:
			try:
				all_depends = depcache[pkgdep]
			except KeyError:
				all_depends = uniqify(pkgdep.get_all_depends())
				depcache[pkgdep] = all_depends

			dep_is_displayed = False
			for dep in all_depends:
				# TODO: Add ability to determine if dep is enabled by USE flag.
				#       Check portage.dep.use_reduce
				if dep.intersects(self):
					pkgdep.depth = depth
					pkgdep.matching_dep = dep
					if printer_fn is not None:
						printer_fn(pkgdep, dep_is_displayed=dep_is_displayed)
					result.append(pkgdep)
					dep_is_displayed = True

			# if --indirect specified, call ourselves again with the dep
			# Do not call if we have already called ourselves.
			if (
				dep_is_displayed and not only_direct and
				pkgdep.cpv not in seen and
				(depth < max_depth or max_depth == -1)
			):

				seen.add(pkgdep.cpv)
				result.append(
					pkgdep.graph_reverse_depends(
						pkgset=pkgset,
						max_depth=max_depth,
						only_direct=only_direct,
						printer_fn=printer_fn,
						depth=depth+1,
						depcache=depcache,
						seen=seen,
						result=result
					)
				)

		if depth == 0:
			return result
		return pkgdep

	def _parser(self, deps, use_conditional=None, depth=0):
		"""?DEPEND file parser.

		@rtype: list
		@return: L{gentoolkit.atom.Atom} objects
		"""
		result = []

		if depth == 0:
			deps = paren_reduce(deps)
		for tok in deps:
			if tok == '||':
				continue
			if tok[-1] == '?':
				use_conditional = tok[:-1]
				continue
			if isinstance(tok, list):
				sub_r = self._parser(tok, use_conditional, depth=depth+1)
				result.extend(sub_r)
				use_conditional = None
				continue
			# FIXME: This is a quick fix for bug #299260.
			#        A better fix is to not discard blockers in the parser,
			#        but to check for atom.blocker in whatever equery/depends
			#        (in this case) and ignore them there.
			# TODO: Test to see how much a performance impact ignoring
			#       blockers here rather than checking for atom.blocker has.
			if tok[0] == '!':
				# We're not interested in blockers
				continue
			# skip it if it's empty
			if tok and tok != '':
				atom = Atom(tok)
				if use_conditional is not None:
					atom.use_conditional = use_conditional
				result.append(atom)
			else:
				message = "dependencies.py: _parser() found an empty " +\
					"dep string token for: %s, deps= %s"
				raise errors.GentoolkitInvalidAtom(message %(self.cpv, deps))

		return result

# vim: set ts=4 sw=4 tw=0: