aboutsummaryrefslogtreecommitdiff
blob: 717ab95d5bf59ca59173bcd98bc7a9c24f144d61 (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
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
# Copyright 1998-2020 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2

__all__ = ["dbapi"]

import functools
import re

import portage

portage.proxy.lazyimport.lazyimport(
    globals(),
    "portage.dbapi.dep_expand:dep_expand@_dep_expand",
    "portage.dep:Atom,match_from_list,_match_slot",
    "portage.output:colorize",
    "portage.util:cmp_sort_key,writemsg",
    "portage.versions:catsplit,catpkgsplit,vercmp,_pkg_str",
)

from portage.const import MERGING_IDENTIFIER

from portage import os
from portage import auxdbkeys
from portage.eapi import _get_eapi_attrs
from portage.exception import InvalidData
from portage.localization import _
from _emerge.Package import Package


class dbapi:
    _category_re = re.compile(r"^\w[-.+\w]*$", re.UNICODE)
    _categories = None
    _use_mutable = False
    _known_keys = frozenset(auxdbkeys)
    _pkg_str_aux_keys = ("EAPI", "KEYWORDS", "SLOT", "repository")

    def __init__(self):
        pass

    @property
    def categories(self):
        """
        Use self.cp_all() to generate a category list. Mutable instances
        can delete the self._categories attribute in cases when the cached
        categories become invalid and need to be regenerated.
        """
        if self._categories is not None:
            return self._categories
        self._categories = tuple(sorted(set(catsplit(x)[0] for x in self.cp_all())))
        return self._categories

    def close_caches(self):
        pass

    def cp_list(self, cp, use_cache=1):
        raise NotImplementedError(self)

    @staticmethod
    def _cmp_cpv(cpv1, cpv2):
        result = vercmp(cpv1.version, cpv2.version)
        if result == 0 and cpv1.build_time is not None and cpv2.build_time is not None:
            result = (cpv1.build_time > cpv2.build_time) - (
                cpv1.build_time < cpv2.build_time
            )
        return result

    @staticmethod
    def _cpv_sort_ascending(cpv_list):
        """
        Use this to sort self.cp_list() results in ascending
        order. It sorts in place and returns None.
        """
        if len(cpv_list) > 1:
            # If the cpv includes explicit -r0, it has to be preserved
            # for consistency in findname and aux_get calls, so use a
            # dict to map strings back to their original values.
            cpv_list.sort(key=cmp_sort_key(dbapi._cmp_cpv))

    def cpv_all(self):
        """Return all CPVs in the db
        Args:
                None
        Returns:
                A list of Strings, 1 per CPV

        This function relies on a subclass implementing cp_all, this is why the hasattr is there
        """

        if not hasattr(self, "cp_all"):
            raise NotImplementedError
        cpv_list = []
        for cp in self.cp_all():
            cpv_list.extend(self.cp_list(cp))
        return cpv_list

    def cp_all(self, sort=False):
        """Implement this in a child class
        Args
                sort - return sorted results
        Returns:
                A list of strings 1 per CP in the datastore
        """
        return NotImplementedError

    def aux_get(self, mycpv, mylist, myrepo=None):
        """Return the metadata keys in mylist for mycpv
        Args:
                mycpv - "sys-apps/foo-1.0"
                mylist - ["SLOT","DEPEND","HOMEPAGE"]
                myrepo - The repository name.
        Returns:
                a list of results, in order of keys in mylist, such as:
                ["0",">=sys-libs/bar-1.0","http://www.foo.com"] or [] if mycpv not found'
        """
        raise NotImplementedError

    def aux_update(self, cpv, metadata_updates):
        """
        Args:
          cpv - "sys-apps/foo-1.0"
                metadata_updates = { key : newvalue }
        Returns:
                None
        """
        raise NotImplementedError

    def match(self, origdep, use_cache=1):
        """Given a dependency, try to find packages that match
        Args:
                origdep - Depend atom
                use_cache - Boolean indicating if we should use the cache or not
                NOTE: Do we ever not want the cache?
        Returns:
                a list of packages that match origdep
        """
        mydep = _dep_expand(origdep, mydb=self, settings=self.settings)
        return list(
            self._iter_match(mydep, self.cp_list(mydep.cp, use_cache=use_cache))
        )

    def _iter_match(self, atom, cpv_iter):
        cpv_iter = iter(match_from_list(atom, cpv_iter))
        if atom.repo:
            cpv_iter = self._iter_match_repo(atom, cpv_iter)
        if atom.slot:
            cpv_iter = self._iter_match_slot(atom, cpv_iter)
        if atom.unevaluated_atom.use:
            cpv_iter = self._iter_match_use(atom, cpv_iter)
        return cpv_iter

    def _pkg_str(self, cpv, repo):
        """
        This is used to contruct _pkg_str instances on-demand during
        matching. If cpv is a _pkg_str instance with slot attribute,
        then simply return it. Otherwise, fetch metadata and construct
        a _pkg_str instance. This may raise KeyError or InvalidData.
        """
        try:
            cpv.slot
        except AttributeError:
            pass
        else:
            return cpv

        metadata = dict(
            zip(
                self._pkg_str_aux_keys,
                self.aux_get(cpv, self._pkg_str_aux_keys, myrepo=repo),
            )
        )

        return _pkg_str(cpv, metadata=metadata, settings=self.settings, db=self)

    def _iter_match_repo(self, atom, cpv_iter):
        for cpv in cpv_iter:
            try:
                pkg_str = self._pkg_str(cpv, atom.repo)
            except (KeyError, InvalidData):
                pass
            else:
                if pkg_str.repo == atom.repo:
                    yield pkg_str

    def _iter_match_slot(self, atom, cpv_iter):
        for cpv in cpv_iter:
            try:
                pkg_str = self._pkg_str(cpv, atom.repo)
            except (KeyError, InvalidData):
                pass
            else:
                if _match_slot(atom, pkg_str):
                    yield pkg_str

    def _iter_match_use(self, atom, cpv_iter):
        """
        1) Check for required IUSE intersection (need implicit IUSE here).
        2) Check enabled/disabled flag states.
        """

        aux_keys = ["EAPI", "IUSE", "KEYWORDS", "SLOT", "USE", "repository"]
        for cpv in cpv_iter:
            try:
                metadata = dict(
                    zip(aux_keys, self.aux_get(cpv, aux_keys, myrepo=atom.repo))
                )
            except KeyError:
                continue

            try:
                cpv.slot
            except AttributeError:
                try:
                    cpv = _pkg_str(cpv, metadata=metadata, settings=self.settings)
                except InvalidData:
                    continue

            if not self._match_use(atom, cpv, metadata):
                continue

            yield cpv

    def _repoman_iuse_implicit_cnstr(self, pkg, metadata):
        """
        In repoman's version of _iuse_implicit_cnstr, account for modifications
        of the self.settings reference between calls.
        """
        eapi_attrs = _get_eapi_attrs(metadata["EAPI"])
        if eapi_attrs.iuse_effective:
            iuse_implicit_match = lambda flag: self.settings._iuse_effective_match(flag)
        else:
            iuse_implicit_match = lambda flag: self.settings._iuse_implicit_match(flag)
        return iuse_implicit_match

    def _iuse_implicit_cnstr(self, pkg, metadata):
        """
        Construct a callable that checks if a given USE flag should
        be considered to be a member of the implicit IUSE for the
        given package.

        @param pkg: package
        @type pkg: _pkg_str
        @param metadata: package metadata
        @type metadata: Mapping
        @return: a callable that accepts a single USE flag argument,
                and returns True only if the USE flag should be considered
                to be a member of the implicit IUSE for the given package.
        @rtype: callable
        """
        eapi_attrs = _get_eapi_attrs(metadata["EAPI"])
        if eapi_attrs.iuse_effective:
            iuse_implicit_match = self.settings._iuse_effective_match
        else:
            iuse_implicit_match = self.settings._iuse_implicit_match

        if not self._use_mutable and eapi_attrs.iuse_effective:
            # For built packages, it is desirable for the built USE setting to
            # be independent of the profile's current IUSE_IMPLICIT state, since
            # the profile's IUSE_IMPLICT setting may have diverged. Therefore,
            # any member of the built USE setting is considered to be a valid
            # member of IUSE_EFFECTIVE. Note that the binary package may be
            # remote, so it's only possible to rely on metadata that is available
            # in the remote Packages file, and the IUSE_IMPLICIT header in the
            # Packages file is vulnerable to mutation (see bug 640318).
            #
            # This behavior is only used for EAPIs that support IUSE_EFFECTIVE,
            # since built USE settings for earlier EAPIs may contain a large
            # number of irrelevant flags.
            prof_iuse = iuse_implicit_match
            enabled = frozenset(metadata["USE"].split()).__contains__
            iuse_implicit_match = lambda flag: prof_iuse(flag) or enabled(flag)

        return iuse_implicit_match

    def _match_use(self, atom, pkg, metadata, ignore_profile=False):
        iuse_implicit_match = self._iuse_implicit_cnstr(pkg, metadata)
        usealiases = self.settings._use_manager.getUseAliases(pkg)
        iuse = Package._iuse(
            None,
            metadata["IUSE"].split(),
            iuse_implicit_match,
            usealiases,
            metadata["EAPI"],
        )

        for x in atom.unevaluated_atom.use.required:
            if iuse.get_real_flag(x) is None:
                return False

        if atom.use is None:
            pass

        elif not self._use_mutable:
            # Use IUSE to validate USE settings for built packages,
            # in case the package manager that built this package
            # failed to do that for some reason (or in case of
            # data corruption). The enabled flags must be consistent
            # with implicit IUSE, in order to avoid potential
            # inconsistencies in USE dep matching (see bug #453400).
            use = frozenset(
                x for x in metadata["USE"].split() if iuse.get_real_flag(x) is not None
            )
            missing_enabled = frozenset(
                x for x in atom.use.missing_enabled if iuse.get_real_flag(x) is None
            )
            missing_disabled = frozenset(
                x for x in atom.use.missing_disabled if iuse.get_real_flag(x) is None
            )
            enabled = frozenset((iuse.get_real_flag(x) or x) for x in atom.use.enabled)
            disabled = frozenset(
                (iuse.get_real_flag(x) or x) for x in atom.use.disabled
            )

            if enabled:
                if any(x in enabled for x in missing_disabled):
                    return False
                need_enabled = enabled.difference(use)
                if need_enabled:
                    if any(x not in missing_enabled for x in need_enabled):
                        return False

            if disabled:
                if any(x in disabled for x in missing_enabled):
                    return False
                need_disabled = disabled.intersection(use)
                if need_disabled:
                    if any(x not in missing_disabled for x in need_disabled):
                        return False

        elif not self.settings.local_config:
            if not ignore_profile:
                # Check masked and forced flags for repoman.
                usemask = self.settings._getUseMask(
                    pkg, stable=self.settings._parent_stable
                )
                if any(
                    x in usemask and iuse.get_real_flag(x) is not None
                    for x in atom.use.enabled
                ):
                    return False

                useforce = self.settings._getUseForce(
                    pkg, stable=self.settings._parent_stable
                )
                if any(
                    x in useforce
                    and x not in usemask
                    and iuse.get_real_flag(x) is not None
                    for x in atom.use.disabled
                ):
                    return False

            # Check unsatisfied use-default deps
            if atom.use.enabled:
                missing_disabled = frozenset(
                    x
                    for x in atom.use.missing_disabled
                    if iuse.get_real_flag(x) is None
                )
                if any(x in atom.use.enabled for x in missing_disabled):
                    return False
            if atom.use.disabled:
                missing_enabled = frozenset(
                    x for x in atom.use.missing_enabled if iuse.get_real_flag(x) is None
                )
                if any(x in atom.use.disabled for x in missing_enabled):
                    return False

        return True

    def invalidentry(self, mypath):
        if "/" + MERGING_IDENTIFIER in mypath:
            if os.path.exists(mypath):
                writemsg(
                    colorize("BAD", _("INCOMPLETE MERGE:")) + " %s\n" % mypath,
                    noiselevel=-1,
                )
        else:
            writemsg("!!! Invalid db entry: %s\n" % mypath, noiselevel=-1)

    def update_ents(self, updates, onProgress=None, onUpdate=None):
        """
        Update metadata of all packages for package moves.
        @param updates: A list of move commands, or dict of {repo_name: list}
        @type updates: list or dict
        @param onProgress: A progress callback function
        @type onProgress: a callable that takes 2 integer arguments: maxval and curval
        @param onUpdate: A progress callback function called only
                for packages that are modified by updates.
        @type onUpdate: a callable that takes 2 integer arguments:
                maxval and curval
        """
        cpv_all = self.cpv_all()
        cpv_all.sort()
        maxval = len(cpv_all)
        aux_get = self.aux_get
        aux_update = self.aux_update
        update_keys = Package._dep_keys
        meta_keys = update_keys + self._pkg_str_aux_keys
        repo_dict = None
        if isinstance(updates, dict):
            repo_dict = updates
        if onUpdate:
            onUpdate(maxval, 0)
        if onProgress:
            onProgress(maxval, 0)
        for i, cpv in enumerate(cpv_all):
            try:
                metadata = dict(zip(meta_keys, aux_get(cpv, meta_keys)))
            except KeyError:
                continue
            try:
                pkg = _pkg_str(cpv, metadata=metadata, settings=self.settings)
            except InvalidData:
                continue
            metadata = dict((k, metadata[k]) for k in update_keys)
            if repo_dict is None:
                updates_list = updates
            else:
                try:
                    updates_list = repo_dict[pkg.repo]
                except KeyError:
                    try:
                        updates_list = repo_dict["DEFAULT"]
                    except KeyError:
                        continue

            if not updates_list:
                continue

            metadata_updates = portage.update_dbentries(
                updates_list, metadata, parent=pkg
            )
            if metadata_updates:
                aux_update(cpv, metadata_updates)
                if onUpdate:
                    onUpdate(maxval, i + 1)
            if onProgress:
                onProgress(maxval, i + 1)

    def move_slot_ent(self, mylist, repo_match=None):
        """This function takes a sequence:
        Args:
                mylist: a sequence of (atom, originalslot, newslot)
                repo_match: callable that takes single repo_name argument
                        and returns True if the update should be applied
        Returns:
                The number of slotmoves this function did
        """
        atom = mylist[1]
        origslot = mylist[2]
        newslot = mylist[3]

        try:
            atom.with_slot
        except AttributeError:
            atom = Atom(atom).with_slot(origslot)
        else:
            atom = atom.with_slot(origslot)

        origmatches = self.match(atom)
        moves = 0
        if not origmatches:
            return moves
        for mycpv in origmatches:
            try:
                mycpv = self._pkg_str(mycpv, atom.repo)
            except (KeyError, InvalidData):
                continue
            if repo_match is not None and not repo_match(mycpv.repo):
                continue
            moves += 1
            if (
                "/" not in newslot
                and mycpv.sub_slot
                and mycpv.sub_slot not in (mycpv.slot, newslot)
            ):
                newslot = "%s/%s" % (newslot, mycpv.sub_slot)
            mydata = {"SLOT": newslot + "\n"}
            self.aux_update(mycpv, mydata)
        return moves