aboutsummaryrefslogtreecommitdiff
blob: 799cd94aa3e94a4542737240d4f643ae179c3a4f (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
"""Profile specific support and addon."""

import os
import stat
from collections import defaultdict
from functools import partial
from itertools import chain

from pkgcore.ebuild import domain, misc
from pkgcore.ebuild import profiles as profiles_mod
from pkgcore.restrictions import packages, values
from snakeoil.cli import arghparse
from snakeoil.containers import ProtectedSet
from snakeoil.decorators import coroutine
from snakeoil.klass import jit_attr
from snakeoil.mappings import ImmutableDict
from snakeoil.osutils import pjoin

from .. import base
from ..base import PkgcheckUserException
from . import ArchesAddon, caches


class ProfileData:
    def __init__(
        self,
        repo,
        profile_name,
        key,
        provides,
        vfilter,
        iuse_effective,
        use,
        pkg_use,
        masked_use,
        forced_use,
        lookup_cache,
        insoluble,
        status,
        deprecated,
    ):
        self.repo = repo
        self.name = profile_name
        self.key = key
        self.provides_repo = provides
        self.provides_has_match = getattr(provides, "has_match", provides.match)
        self.iuse_effective = iuse_effective
        self.use = use
        self.pkg_use = pkg_use
        self.masked_use = masked_use
        self.forced_use = forced_use
        self.cache = lookup_cache
        self.insoluble = insoluble
        self.visible = vfilter.match
        self.status = status
        self.deprecated = deprecated

    def identify_use(self, pkg, known_flags):
        # note we're trying to be *really* careful about not creating
        # pointless intermediate sets unless required
        # kindly don't change that in any modifications, it adds up.
        enabled = known_flags.intersection(self.forced_use.pull_data(pkg))
        immutable = enabled.union(filter(known_flags.__contains__, self.masked_use.pull_data(pkg)))
        if force_disabled := self.masked_use.pull_data(pkg):
            enabled = enabled.difference(force_disabled)
        return immutable, enabled


class ProfileNode(profiles_mod.ProfileNode):
    """Re-inherited to disable instance caching."""


class ProfilesArgs(arghparse.CommaSeparatedNegations):
    """Parse profiles args for the ProfileAddon."""

    @staticmethod
    def norm_name(repo, s):
        """Expand status keywords and format paths."""
        if s in ("dev", "exp", "stable", "deprecated"):
            yield from repo.profiles.get_profiles(status=s)
        elif s == "all":
            yield from repo.profiles
        else:
            try:
                yield repo.profiles[os.path.normpath(s)]
            except KeyError:
                raise ValueError(f"nonexistent profile: {s!r}")

    def __call__(self, parser, namespace, values, option_string=None):
        disabled, enabled = self.parse_values(values)
        namespace.ignore_deprecated_profiles = "deprecated" not in enabled

        # Expand status keywords, e.g. 'stable' -> set of stable profiles, and
        # translate selections into profile objs.
        norm_name = partial(self.norm_name, namespace.target_repo)
        try:
            disabled = set().union(*map(norm_name, disabled))
            enabled = set().union(*map(norm_name, enabled))
        except ValueError as e:
            parser.error(str(e))

        # If no profiles are enabled, then all that are defined in
        # profiles.desc are scanned except ones that are explicitly disabled.
        if not enabled:
            enabled = set(namespace.target_repo.profiles)

        profiles = enabled.difference(disabled)
        setattr(namespace, self.dest, profiles)
        namespace.profiles = profiles


class ProfileAddon(caches.CachedAddon):
    """Addon supporting ebuild repository profiles."""

    required_addons = (ArchesAddon,)

    # non-profile dirs found in the profiles directory, generally only in
    # the gentoo repo, but could be in overlays as well
    non_profile_dirs = frozenset(["desc", "updates"])

    # cache registry
    cache = caches.CacheData(type="profiles", file="profiles.pickle", version=2)

    @classmethod
    def mangle_argparser(cls, parser):
        group = parser.add_argument_group("profiles")
        group.add_argument(
            "-p",
            "--profiles",
            metavar="PROFILE",
            dest="selected_profiles",
            action=arghparse.Delayed,
            target=ProfilesArgs,
            priority=101,
            help="comma separated list of profiles to enable/disable",
            docs="""
                Comma separated list of profiles to enable and disable for
                scanning. Any profiles specified in this fashion will be the
                only profiles that get scanned, skipping any disabled profiles.
                In addition, if no profiles are explicitly enabled, all
                profiles defined in the target repo's profiles.desc file will be
                scanned except those marked as experimental (exp).

                To specify disabled profiles prefix them with ``-`` which
                removes the from the list of profiles to be considered. Note
                that when starting the argument list with a disabled profile an
                equals sign must be used, e.g.  ``-p=-path/to/profile``,
                otherwise the disabled profile argument is treated as an
                option.

                The special keywords of ``stable``, ``dev``, ``exp``, and
                ``deprecated`` correspond to the lists of stable, development,
                experimental, and deprecated profiles, respectively. Therefore,
                to only scan all stable profiles pass the ``stable`` argument
                to --profiles. Additionally the keyword ``all`` can be used to
                scan all defined profiles in the target repo.
            """,
        )
        parser.bind_delayed_default(1001, "profiles")(cls._default_profiles)

    @staticmethod
    def _default_profiles(namespace, attr):
        """Determine set of profiles to enable by default."""
        # Disable experimental profiles by default if no profiles are
        # selected and no keywords or arches have been explicitly selected
        # that require them to operate properly.
        target_repo = namespace.target_repo
        profiles = set(target_repo.profiles)
        if not getattr(namespace, "exp_profiles_required", False):
            profiles -= set(ProfilesArgs.norm_name(target_repo, "exp"))
        setattr(namespace, attr, profiles)

    def __init__(self, *args, arches_addon):
        super().__init__(*args)
        self.global_insoluble = set()
        self.profile_filters = {}
        self.profile_evaluate_dict = {}

        self.arch_profiles = defaultdict(list)
        self.target_repo = self.options.target_repo
        ignore_deprecated = getattr(self.options, "ignore_deprecated_profiles", True)

        for p in sorted(self.options.profiles):
            if p.deprecated and ignore_deprecated:
                continue
            try:
                profile = self.target_repo.profiles.create_profile(p, load_profile_base=False)
            except profiles_mod.ProfileError as e:
                # Only throw errors if the profile was selected by the user, bad
                # repo profiles will be caught during repo metadata scans.
                if self.options.selected_profiles is not None:
                    raise PkgcheckUserException(f"invalid profile: {e.path!r}: {e.error}")
                continue
            self.arch_profiles[p.arch].append((profile, p))

    @coroutine
    def _profile_files(self):
        """Given a profile object, return its file set and most recent mtime."""
        cache = {}
        while True:
            profile = yield
            profile_mtime = 0
            profile_files = []
            for node in profile.stack:
                mtime, files = cache.get(node.path, (0, []))
                if not mtime:
                    for f in os.listdir(node.path):
                        p = pjoin(node.path, f)
                        st_obj = os.lstat(p)
                        if stat.S_ISREG(st_obj.st_mode):
                            files.append(p)
                            if st_obj.st_mtime > mtime:
                                mtime = st_obj.st_mtime
                    cache[node.path] = (mtime, files)
                if mtime > profile_mtime:
                    profile_mtime = mtime
                profile_files.extend(files)
            yield profile_mtime, frozenset(profile_files)

    @jit_attr
    def profile_data(self):
        """Mapping of profile age and file sets used to check cache viability."""
        data = {}
        gen_profile_data = self._profile_files()
        for profile_obj, profile in chain.from_iterable(self.arch_profiles.values()):
            mtime, files = gen_profile_data.send(profile_obj)
            data[profile] = (mtime, files)
            next(gen_profile_data)
        return ImmutableDict(data)

    def update_cache(self, force=False):
        """Update related cache and push updates to disk."""
        cached_profiles = defaultdict(dict)
        official_arches = self.target_repo.known_arches

        with base.ProgressManager(verbosity=self.options.verbosity) as progress:
            for repo in self.target_repo.trees:
                cache_file = self.cache_file(repo)
                # add profiles-base -> repo mapping to ease storage procedure
                cached_profiles[repo.config.profiles_base]["repo"] = repo
                if not force:
                    cache = self.load_cache(cache_file, fallback={})
                    cached_profiles[repo.config.profiles_base].update(cache)

                chunked_data_cache = {}

                for arch in sorted(self.options.arches):
                    stable_key, unstable_key = arch, f"~{arch}"
                    stable_r = packages.PackageRestriction(
                        "keywords", values.ContainmentMatch2((stable_key,))
                    )
                    unstable_r = packages.PackageRestriction(
                        "keywords",
                        values.ContainmentMatch2(
                            (
                                stable_key,
                                unstable_key,
                            )
                        ),
                    )

                    default_masked_use = tuple(set(x for x in official_arches if x != stable_key))

                    # padding for progress output
                    padding = max(len(x) for x in self.options.arches)

                    for profile_obj, profile in self.arch_profiles.get(arch, []):
                        files = self.profile_data.get(profile)
                        try:
                            cached_profile = cached_profiles[profile.base][profile.path]
                            if files != cached_profile["files"]:
                                # force refresh of outdated cache entry
                                raise KeyError

                            masks = cached_profile["masks"]
                            unmasks = cached_profile["unmasks"]
                            immutable_flags = cached_profile["immutable_flags"]
                            stable_immutable_flags = cached_profile["stable_immutable_flags"]
                            enabled_flags = cached_profile["enabled_flags"]
                            stable_enabled_flags = cached_profile["stable_enabled_flags"]
                            pkg_use = cached_profile["pkg_use"]
                            iuse_effective = cached_profile["iuse_effective"]
                            use = cached_profile["use"]
                            provides_repo = cached_profile["provides_repo"]
                        except KeyError:
                            try:
                                progress(
                                    f"{repo} -- updating profiles cache: {profile.arch:<{padding}}"
                                )

                                masks = profile_obj.masks
                                unmasks = profile_obj.unmasks

                                immutable_flags = profile_obj.masked_use.clone(unfreeze=True)
                                immutable_flags.add_bare_global((), default_masked_use)
                                immutable_flags.optimize(cache=chunked_data_cache)
                                immutable_flags.freeze()

                                stable_immutable_flags = profile_obj.stable_masked_use.clone(
                                    unfreeze=True
                                )
                                stable_immutable_flags.add_bare_global((), default_masked_use)
                                stable_immutable_flags.optimize(cache=chunked_data_cache)
                                stable_immutable_flags.freeze()

                                enabled_flags = profile_obj.forced_use.clone(unfreeze=True)
                                enabled_flags.add_bare_global((), (stable_key,))
                                enabled_flags.optimize(cache=chunked_data_cache)
                                enabled_flags.freeze()

                                stable_enabled_flags = profile_obj.stable_forced_use.clone(
                                    unfreeze=True
                                )
                                stable_enabled_flags.add_bare_global((), (stable_key,))
                                stable_enabled_flags.optimize(cache=chunked_data_cache)
                                stable_enabled_flags.freeze()

                                pkg_use = profile_obj.pkg_use
                                iuse_effective = profile_obj.iuse_effective
                                provides_repo = profile_obj.provides_repo

                                # finalize enabled USE flags
                                use = frozenset(
                                    misc.incremental_expansion(
                                        profile_obj.use, msg_prefix="while expanding USE"
                                    )
                                )
                            except profiles_mod.ProfileError:
                                # unsupported EAPI or other issue, profile checks will catch this
                                continue

                            cached_profiles[profile.base]["update"] = True
                            cached_profiles[profile.base][profile.path] = {
                                "files": files,
                                "masks": masks,
                                "unmasks": unmasks,
                                "immutable_flags": immutable_flags,
                                "stable_immutable_flags": stable_immutable_flags,
                                "enabled_flags": enabled_flags,
                                "stable_enabled_flags": stable_enabled_flags,
                                "pkg_use": pkg_use,
                                "iuse_effective": iuse_effective,
                                "use": use,
                                "provides_repo": provides_repo,
                            }

                        # used to interlink stable/unstable lookups so that if
                        # unstable says it's not visible, stable doesn't try
                        # if stable says something is visible, unstable doesn't try.
                        stable_cache = set()
                        unstable_insoluble = ProtectedSet(self.global_insoluble)

                        # few notes.  for filter, ensure keywords is last, on the
                        # offchance a non-metadata based restrict foregos having to
                        # access the metadata.
                        # note that the cache/insoluble are inversly paired;
                        # stable cache is usable for unstable, but not vice versa.
                        # unstable insoluble is usable for stable, but not vice versa
                        vfilter = domain.generate_filter(
                            self.target_repo.pkg_masks | masks, unmasks
                        )
                        self.profile_filters.setdefault(stable_key, []).append(
                            ProfileData(
                                repo.repo_id,
                                profile.path,
                                stable_key,
                                provides_repo,
                                packages.AndRestriction(vfilter, stable_r),
                                iuse_effective,
                                use,
                                pkg_use,
                                stable_immutable_flags,
                                stable_enabled_flags,
                                stable_cache,
                                ProtectedSet(unstable_insoluble),
                                profile.status,
                                profile.deprecated,
                            )
                        )

                        self.profile_filters.setdefault(unstable_key, []).append(
                            ProfileData(
                                repo.repo_id,
                                profile.path,
                                unstable_key,
                                provides_repo,
                                packages.AndRestriction(vfilter, unstable_r),
                                iuse_effective,
                                use,
                                pkg_use,
                                immutable_flags,
                                enabled_flags,
                                ProtectedSet(stable_cache),
                                unstable_insoluble,
                                profile.status,
                                profile.deprecated,
                            )
                        )

        # dump updated profile filters
        for k, v in cached_profiles.items():
            if v.pop("update", False):
                repo = v.pop("repo")
                cache_file = self.cache_file(repo)
                cache = caches.DictCache(cached_profiles[repo.config.profiles_base], self.cache)
                self.save_cache(cache, cache_file)

        for key, profile_list in self.profile_filters.items():
            similar = self.profile_evaluate_dict[key] = []
            for profile in profile_list:
                for existing in similar:
                    if (
                        existing[0].masked_use == profile.masked_use
                        and existing[0].forced_use == profile.forced_use
                    ):
                        existing.append(profile)
                        break
                else:
                    similar.append([profile])

    def identify_profiles(self, pkg):
        # yields groups of profiles; the 'groups' are grouped by the ability to share
        # the use processing across each of 'em.
        groups = []
        keywords = pkg.keywords
        unstable_keywords = (f"~{x}" for x in keywords if x[0] != "~")
        for key in chain(keywords, unstable_keywords):
            if profile_grps := self.profile_evaluate_dict.get(key):
                for profiles in profile_grps:
                    if group := [x for x in profiles if x.visible(pkg)]:
                        groups.append(group)
        return groups

    def __getitem__(self, key):
        """Return profiles matching a given keyword."""
        return self.profile_filters[key]

    def get(self, key, default=None):
        """Return profiles matching a given keyword with a fallback if none exist."""
        try:
            return self.profile_filters[key]
        except KeyError:
            return default

    def items(self):
        """Iterate over all keywords and profiles."""
        return self.profile_filters.items()

    def __iter__(self):
        """Iterate over all profile data objects."""
        return chain.from_iterable(self.profile_filters.values())

    def __len__(self):
        return len([x for x in self])