DateSet: use BUILD_TIME, bug #433964
[portage.git] / pym / portage / _sets / dbapi.py
1 # Copyright 2007-2012 Gentoo Foundation
2 # Distributed under the terms of the GNU General Public License v2
3
4 import time
5
6 from portage import os
7 from portage.versions import best, catsplit, vercmp
8 from portage.dep import Atom
9 from portage.localization import _
10 from portage._sets.base import PackageSet
11 from portage._sets import SetConfigError, get_boolean
12 import portage
13
14 __all__ = ["CategorySet", "DowngradeSet",
15         "EverythingSet", "OwnerSet", "VariableSet"]
16
17 class EverythingSet(PackageSet):
18         _operations = ["merge"]
19         description = "Package set which contains SLOT " + \
20                 "atoms to match all installed packages"
21         _filter = None
22
23         def __init__(self, vdbapi, **kwargs):
24                 super(EverythingSet, self).__init__()
25                 self._db = vdbapi
26
27         def load(self):
28                 myatoms = []
29                 db_keys = ["SLOT"]
30                 aux_get = self._db.aux_get
31                 cp_list = self._db.cp_list
32
33                 for cp in self._db.cp_all():
34                         for cpv in cp_list(cp):
35                                 # NOTE: Create SLOT atoms even when there is only one
36                                 # SLOT installed, in order to avoid the possibility
37                                 # of unwanted upgrades as reported in bug #338959.
38                                 slot, = aux_get(cpv, db_keys)
39                                 atom = Atom("%s:%s" % (cp, slot))
40                                 if self._filter:
41                                         if self._filter(atom):
42                                                 myatoms.append(atom)
43                                 else:
44                                         myatoms.append(atom)
45
46                 self._setAtoms(myatoms)
47         
48         def singleBuilder(self, options, settings, trees):
49                 return EverythingSet(trees["vartree"].dbapi)
50         singleBuilder = classmethod(singleBuilder)
51
52 class OwnerSet(PackageSet):
53
54         _operations = ["merge", "unmerge"]
55
56         description = "Package set which contains all packages " + \
57                 "that own one or more files."
58
59         def __init__(self, vardb=None, exclude_files=None, files=None):
60                 super(OwnerSet, self).__init__()
61                 self._db = vardb
62                 self._exclude_files = exclude_files
63                 self._files = files
64
65         def mapPathsToAtoms(self, paths, exclude_paths=None):
66                 """
67                 All paths must have $EROOT stripped from the left side.
68                 """
69                 rValue = set()
70                 vardb = self._db
71                 aux_get = vardb.aux_get
72                 aux_keys = ["SLOT"]
73                 if exclude_paths is None:
74                         for link, p in vardb._owners.iter_owners(paths):
75                                 slot, = aux_get(link.mycpv, aux_keys)
76                                 rValue.add("%s:%s" % (link.mycpv.cp, slot))
77                 else:
78                         all_paths = set()
79                         all_paths.update(paths)
80                         all_paths.update(exclude_paths)
81                         exclude_atoms = set()
82                         for link, p in vardb._owners.iter_owners(all_paths):
83                                 slot, = aux_get(link.mycpv, aux_keys)
84                                 atom = "%s:%s" % (link.mycpv.cp, slot)
85                                 rValue.add(atom)
86                                 if p in exclude_paths:
87                                         exclude_atoms.add(atom)
88                         rValue.difference_update(exclude_atoms)
89
90                 return rValue
91
92         def load(self):
93                 self._setAtoms(self.mapPathsToAtoms(self._files,
94                         exclude_paths=self._exclude_files))
95
96         def singleBuilder(cls, options, settings, trees):
97                 if not "files" in options:
98                         raise SetConfigError(_("no files given"))
99
100                 exclude_files = options.get("exclude-files")
101                 if exclude_files is not None:
102                         exclude_files = frozenset(portage.util.shlex_split(exclude_files))
103                 return cls(vardb=trees["vartree"].dbapi, exclude_files=exclude_files,
104                         files=frozenset(portage.util.shlex_split(options["files"])))
105
106         singleBuilder = classmethod(singleBuilder)
107
108 class VariableSet(EverythingSet):
109
110         _operations = ["merge", "unmerge"]
111
112         description = "Package set which contains all packages " + \
113                 "that match specified values of a specified variable."
114
115         def __init__(self, vardb, metadatadb=None, variable=None, includes=None, excludes=None):
116                 super(VariableSet, self).__init__(vardb)
117                 self._metadatadb = metadatadb
118                 self._variable = variable
119                 self._includes = includes
120                 self._excludes = excludes
121
122         def _filter(self, atom):
123                 ebuild = best(self._metadatadb.match(atom))
124                 if not ebuild:
125                         return False
126                 values, = self._metadatadb.aux_get(ebuild, [self._variable])
127                 values = values.split()
128                 if self._includes and not self._includes.intersection(values):
129                         return False
130                 if self._excludes and self._excludes.intersection(values):
131                         return False
132                 return True
133
134         def singleBuilder(cls, options, settings, trees):
135
136                 variable = options.get("variable")
137                 if variable is None:
138                         raise SetConfigError(_("missing required attribute: 'variable'"))
139
140                 includes = options.get("includes", "")
141                 excludes = options.get("excludes", "")
142
143                 if not (includes or excludes):
144                         raise SetConfigError(_("no includes or excludes given"))
145                 
146                 metadatadb = options.get("metadata-source", "vartree")
147                 if not metadatadb in trees:
148                         raise SetConfigError(_("invalid value '%s' for option metadata-source") % metadatadb)
149
150                 return cls(trees["vartree"].dbapi,
151                         metadatadb=trees[metadatadb].dbapi,
152                         excludes=frozenset(excludes.split()),
153                         includes=frozenset(includes.split()),
154                         variable=variable)
155
156         singleBuilder = classmethod(singleBuilder)
157
158 class DowngradeSet(PackageSet):
159
160         _operations = ["merge", "unmerge"]
161
162         description = "Package set which contains all packages " + \
163                 "for which the highest visible ebuild version is lower than " + \
164                 "the currently installed version."
165
166         def __init__(self, portdb=None, vardb=None):
167                 super(DowngradeSet, self).__init__()
168                 self._portdb = portdb
169                 self._vardb = vardb
170
171         def load(self):
172                 atoms = []
173                 xmatch = self._portdb.xmatch
174                 xmatch_level = "bestmatch-visible"
175                 cp_list = self._vardb.cp_list
176                 aux_get = self._vardb.aux_get
177                 aux_keys = ["SLOT"]
178                 for cp in self._vardb.cp_all():
179                         for cpv in cp_list(cp):
180                                 slot, = aux_get(cpv, aux_keys)
181                                 slot_atom = "%s:%s" % (cp, slot)
182                                 ebuild = xmatch(xmatch_level, slot_atom)
183                                 if not ebuild:
184                                         continue
185                                 if vercmp(cpv.version, ebuild.version) > 0:
186                                         atoms.append(slot_atom)
187
188                 self._setAtoms(atoms)
189
190         def singleBuilder(cls, options, settings, trees):
191                 return cls(portdb=trees["porttree"].dbapi,
192                         vardb=trees["vartree"].dbapi)
193
194         singleBuilder = classmethod(singleBuilder)
195
196 class UnavailableSet(EverythingSet):
197
198         _operations = ["unmerge"]
199
200         description = "Package set which contains all installed " + \
201                 "packages for which there are no visible ebuilds " + \
202                 "corresponding to the same $CATEGORY/$PN:$SLOT."
203
204         def __init__(self, vardb, metadatadb=None):
205                 super(UnavailableSet, self).__init__(vardb)
206                 self._metadatadb = metadatadb
207
208         def _filter(self, atom):
209                 return not self._metadatadb.match(atom)
210
211         def singleBuilder(cls, options, settings, trees):
212
213                 metadatadb = options.get("metadata-source", "porttree")
214                 if not metadatadb in trees:
215                         raise SetConfigError(_("invalid value '%s' for option "
216                                 "metadata-source") % (metadatadb,))
217
218                 return cls(trees["vartree"].dbapi,
219                         metadatadb=trees[metadatadb].dbapi)
220
221         singleBuilder = classmethod(singleBuilder)
222
223 class UnavailableBinaries(EverythingSet):
224
225         _operations = ('merge', 'unmerge',)
226
227         description = "Package set which contains all installed " + \
228                 "packages for which corresponding binary packages " + \
229                 "are not available."
230
231         def __init__(self, vardb, metadatadb=None):
232                 super(UnavailableBinaries, self).__init__(vardb)
233                 self._metadatadb = metadatadb
234
235         def _filter(self, atom):
236                 inst_pkg = self._db.match(atom)
237                 if not inst_pkg:
238                         return False
239                 inst_cpv = inst_pkg[0]
240                 return not self._metadatadb.cpv_exists(inst_cpv)
241
242         def singleBuilder(cls, options, settings, trees):
243
244                 metadatadb = options.get("metadata-source", "bintree")
245                 if not metadatadb in trees:
246                         raise SetConfigError(_("invalid value '%s' for option "
247                                 "metadata-source") % (metadatadb,))
248
249                 return cls(trees["vartree"].dbapi,
250                         metadatadb=trees[metadatadb].dbapi)
251
252         singleBuilder = classmethod(singleBuilder)
253
254 class CategorySet(PackageSet):
255         _operations = ["merge", "unmerge"]
256         
257         def __init__(self, category, dbapi, only_visible=True):
258                 super(CategorySet, self).__init__()
259                 self._db = dbapi
260                 self._category = category
261                 self._check = only_visible
262                 if only_visible:
263                         s="visible"
264                 else:
265                         s="all"
266                 self.description = "Package set containing %s packages of category %s" % (s, self._category)
267                         
268         def load(self):
269                 myatoms = []
270                 for cp in self._db.cp_all():
271                         if catsplit(cp)[0] == self._category:
272                                 if (not self._check) or len(self._db.match(cp)) > 0:
273                                         myatoms.append(cp)
274                 self._setAtoms(myatoms)
275         
276         def _builderGetRepository(cls, options, repositories):
277                 repository = options.get("repository", "porttree")
278                 if not repository in repositories:
279                         raise SetConfigError(_("invalid repository class '%s'") % repository)
280                 return repository
281         _builderGetRepository = classmethod(_builderGetRepository)
282
283         def _builderGetVisible(cls, options):
284                 return get_boolean(options, "only_visible", True)
285         _builderGetVisible = classmethod(_builderGetVisible)
286                 
287         def singleBuilder(cls, options, settings, trees):
288                 if not "category" in options:
289                         raise SetConfigError(_("no category given"))
290
291                 category = options["category"]
292                 if not category in settings.categories:
293                         raise SetConfigError(_("invalid category name '%s'") % category)
294
295                 repository = cls._builderGetRepository(options, trees.keys())
296                 visible = cls._builderGetVisible(options)
297                 
298                 return CategorySet(category, dbapi=trees[repository].dbapi, only_visible=visible)
299         singleBuilder = classmethod(singleBuilder)
300
301         def multiBuilder(cls, options, settings, trees):
302                 rValue = {}
303         
304                 if "categories" in options:
305                         categories = options["categories"].split()
306                         invalid = set(categories).difference(settings.categories)
307                         if invalid:
308                                 raise SetConfigError(_("invalid categories: %s") % ", ".join(list(invalid)))
309                 else:
310                         categories = settings.categories
311         
312                 repository = cls._builderGetRepository(options, trees.keys())
313                 visible = cls._builderGetVisible(options)
314                 name_pattern = options.get("name_pattern", "$category/*")
315         
316                 if not "$category" in name_pattern and not "${category}" in name_pattern:
317                         raise SetConfigError(_("name_pattern doesn't include $category placeholder"))
318         
319                 for cat in categories:
320                         myset = CategorySet(cat, trees[repository].dbapi, only_visible=visible)
321                         myname = name_pattern.replace("$category", cat)
322                         myname = myname.replace("${category}", cat)
323                         rValue[myname] = myset
324                 return rValue
325         multiBuilder = classmethod(multiBuilder)
326
327 class AgeSet(EverythingSet):
328         _operations = ["merge", "unmerge"]
329         _aux_keys = ('BUILD_TIME',)
330
331         def __init__(self, vardb, mode="older", age=7):
332                 super(AgeSet, self).__init__(vardb)
333                 self._mode = mode
334                 self._age = age
335
336         def _filter(self, atom):
337         
338                 cpv = self._db.match(atom)[0]
339                 try:
340                         date, = self._db.aux_get(cpv, self._aux_keys)
341                         date = int(date)
342                 except (KeyError, ValueError):
343                         return bool(self._mode == "older")
344                 age = (time.time() - date) / (3600 * 24)
345                 if ((self._mode == "older" and age <= self._age) \
346                         or (self._mode == "newer" and age >= self._age)):
347                         return False
348                 else:
349                         return True
350         
351         def singleBuilder(cls, options, settings, trees):
352                 mode = options.get("mode", "older")
353                 if str(mode).lower() not in ["newer", "older"]:
354                         raise SetConfigError(_("invalid 'mode' value %s (use either 'newer' or 'older')") % mode)
355                 try:
356                         age = int(options.get("age", "7"))
357                 except ValueError as e:
358                         raise SetConfigError(_("value of option 'age' is not an integer"))
359                 return AgeSet(vardb=trees["vartree"].dbapi, mode=mode, age=age)
360
361         singleBuilder = classmethod(singleBuilder)
362
363 class DateSet(EverythingSet):
364         _operations = ["merge", "unmerge"]
365         _aux_keys = ('BUILD_TIME',)
366
367         def __init__(self, vardb, date, mode="older"):
368                 super(DateSet, self).__init__(vardb)
369                 self._mode = mode
370                 self._date = date
371
372         def _filter(self, atom):
373
374                 cpv = self._db.match(atom)[0]
375                 try:
376                         date, = self._db.aux_get(cpv, self._aux_keys)
377                         date = int(date)
378                 except (KeyError, ValueError):
379                         return bool(self._mode == "older")
380                 # Make sure inequality is _strict_ to exclude tested package
381                 if ((self._mode == "older" and date < self._date) \
382                         or (self._mode == "newer" and date > self._date)):
383                         return True
384                 else:
385                         return False
386
387         def singleBuilder(cls, options, settings, trees):
388                 vardbapi = trees["vartree"].dbapi
389                 mode = options.get("mode", "older")
390                 if str(mode).lower() not in ["newer", "older"]:
391                         raise SetConfigError(_("invalid 'mode' value %s (use either 'newer' or 'older')") % mode)
392
393                 formats = []
394                 if options.get("package") is not None:
395                         formats.append("package")
396                 if options.get("filestamp") is not None:
397                         formats.append("filestamp")
398                 if options.get("seconds") is not None:
399                         formats.append("seconds")
400                 if options.get("date") is not None:
401                         formats.append("date")
402
403                 if not formats:
404                         raise SetConfigError(_("none of these options specified: 'package', 'filestamp', 'seconds', 'date'"))
405                 elif len(formats) > 1:
406                         raise SetConfigError(_("no more than one of these options is allowed: 'package', 'filestamp', 'seconds', 'date'"))
407
408                 format = formats[0]
409
410                 if (format == "package"):
411                         package = options.get("package")
412                         try:
413                                 cpv = vardbapi.match(package)[0]
414                                 date, = vardbapi.aux_get(cpv, ('BUILD_TIME',))
415                                 date = int(date)
416                         except (KeyError, ValueError):
417                                 raise SetConfigError(_("cannot determine installation date of package %s") % package)
418                 elif (format == "filestamp"):
419                         filestamp = options.get("filestamp")
420                         try:
421                                 date = int(os.stat(filestamp).st_mtime)
422                         except (OSError, ValueError):
423                                 raise SetConfigError(_("cannot determine 'filestamp' of '%s'") % filestamp)
424                 elif (format == "seconds"):
425                         try:
426                                 date = int(options.get("seconds"))
427                         except ValueError:
428                                 raise SetConfigError(_("option 'seconds' must be an integer"))
429                 else:
430                         dateopt = options.get("date")
431                         try:
432                                 dateformat = options.get("dateformat", "%x %X")
433                                 date = int(time.mktime(time.strptime(dateopt, dateformat)))
434                         except ValueError:
435                                 raise SetConfigError(_("'date=%s' does not match 'dateformat=%s'") % (dateopt, dateformat))
436                 return DateSet(vardb=vardbapi, date=date, mode=mode)
437
438         singleBuilder = classmethod(singleBuilder)
439
440 class RebuiltBinaries(EverythingSet):
441         _operations = ('merge',)
442         _aux_keys = ('BUILD_TIME',)
443
444         def __init__(self, vardb, bindb=None):
445                 super(RebuiltBinaries, self).__init__(vardb, bindb=bindb)
446                 self._bindb = bindb
447
448         def _filter(self, atom):
449                 cpv = self._db.match(atom)[0]
450                 inst_build_time, = self._db.aux_get(cpv, self._aux_keys)
451                 try:
452                         bin_build_time, = self._bindb.aux_get(cpv, self._aux_keys)
453                 except KeyError:
454                         return False
455                 return bool(bin_build_time and (inst_build_time != bin_build_time))
456
457         def singleBuilder(cls, options, settings, trees):
458                 return RebuiltBinaries(trees["vartree"].dbapi,
459                         bindb=trees["bintree"].dbapi)
460
461         singleBuilder = classmethod(singleBuilder)