|
7 | 7 | from importlib.resources import read_text |
8 | 8 | from itertools import islice |
9 | 9 | from pathlib import Path |
| 10 | +from typing import NamedTuple |
10 | 11 |
|
11 | 12 | from pkgcore.bugzilla.apikey import BugzillaApiKey |
12 | 13 | from pkgcore.restrictions import boolean, packages, values |
13 | 14 | from pkgcore.restrictions.required_use import find_constraint_satisfaction, iter_flags |
14 | 15 | from pkgcore.util import commandline |
15 | 16 | from pkgcore.util import packages as pkgutils |
16 | 17 | from snakeoil.cli import arghparse |
| 18 | +from snakeoil.cli.exceptions import UserException |
| 19 | +from snakeoil.strings import pluralism |
17 | 20 |
|
18 | 21 | from ..cli import ArgumentParser |
19 | 22 |
|
|
70 | 73 | still be changed from defaults. |
71 | 74 | """, |
72 | 75 | ) |
| 76 | +use_opts.add_argument( |
| 77 | + "--enable-prefixes", |
| 78 | + default=[], |
| 79 | + action=arghparse.CommaSeparatedValuesAppend, |
| 80 | + help="USE flags prefixes that will always be enabled", |
| 81 | + docs=""" |
| 82 | + Comma separated USE flags prefixes which are always enabled, in every |
| 83 | + USE combination and in every mode. |
| 84 | +
|
| 85 | + Flags the profile masks cannot be enabled, and are kept disabled with |
| 86 | + a warning. ``test`` is never affected, use ``--test`` for it. |
| 87 | + """, |
| 88 | +) |
| 89 | +use_opts.add_argument( |
| 90 | + "--disable-prefixes", |
| 91 | + default=[], |
| 92 | + action=arghparse.CommaSeparatedValuesAppend, |
| 93 | + help="USE flags prefixes that will never be enabled", |
| 94 | + docs=""" |
| 95 | + Comma separated USE flags prefixes which are never enabled, in every |
| 96 | + USE combination and in every mode. This is useful for expensive or |
| 97 | + known broken USE flags such as ``lto`` and ``pgo``. |
| 98 | +
|
| 99 | + Flags the profile forces on cannot be disabled, and are kept enabled |
| 100 | + with a warning. ``test`` is never affected, use ``--test`` for it. |
| 101 | +
|
| 102 | + If ``REQUIRED_USE`` admits no combination once these options are |
| 103 | + applied, the package cannot be tested and an error is raised. |
| 104 | + """, |
| 105 | +) |
73 | 106 | random_use_opts = use_opts.add_mutually_exclusive_group() |
74 | 107 | random_use_opts.add_argument( |
75 | 108 | "--use-default", |
@@ -278,13 +311,57 @@ def _groupby_use_expand( |
278 | 311 | return use_flags, use_expand_dict |
279 | 312 |
|
280 | 313 |
|
281 | | -def _build_job(namespace, pkg, is_test: bool): |
282 | | - use_expand_prefixes = tuple(s.lower() + "_" for s in namespace.domain.profile.use_expand) |
283 | | - immutable, enabled, _disabled = namespace.domain.get_package_use_unconfigured(pkg) |
| 314 | +class UnsatisfiableUse(UserException): |
| 315 | + """No USE combination is testable for a package.""" |
284 | 316 |
|
| 317 | + |
| 318 | +class _PkgUse(NamedTuple): |
| 319 | + """USE flag state of a package, shared by all of its jobs.""" |
| 320 | + |
| 321 | + immutable: frozenset[str] |
| 322 | + enabled: frozenset[str] |
| 323 | + iuse: frozenset[str] |
| 324 | + force_on: frozenset[str] |
| 325 | + force_off: frozenset[str] |
| 326 | + |
| 327 | + |
| 328 | +def _resolve_use(namespace, pkg, err): |
| 329 | + """Determine a package's USE flag state, honoring the prefix options.""" |
| 330 | + immutable, enabled, masked = namespace.domain.get_package_use_unconfigured(pkg) |
285 | 331 | iuse = frozenset(pkg.iuse_stripped) |
286 | | - force_true = immutable.union(("test",) if is_test else ()) |
287 | | - force_false = ("test",) if not is_test else () |
| 332 | + |
| 333 | + def matching(prefixes): |
| 334 | + # "test" is driven by --test alone, never by the prefix options |
| 335 | + return frozenset(use for use in iuse if use.startswith(tuple(prefixes))) - {"test"} |
| 336 | + |
| 337 | + force_on, force_off = matching(namespace.enable_prefixes), matching(namespace.disable_prefixes) |
| 338 | + if both := force_on.intersection(force_off): |
| 339 | + raise UnsatisfiableUse( |
| 340 | + f"{pkg.cpvstr}: USE flag{pluralism(both)} matched by both --enable-prefixes " |
| 341 | + f"and --disable-prefixes: {', '.join(sorted(both))}" |
| 342 | + ) |
| 343 | + |
| 344 | + # the profile has the last word, its forced and masked flags can't be flipped |
| 345 | + for option, state, flags, profile_flags in ( |
| 346 | + ("--disable-prefixes", "forced", force_off, immutable), |
| 347 | + ("--enable-prefixes", "masked", force_on, masked), |
| 348 | + ): |
| 349 | + if conflict := flags.intersection(profile_flags): |
| 350 | + err.warn( |
| 351 | + f"{pkg.cpvstr}: ignoring {option} for profile {state} USE " |
| 352 | + f"flag{pluralism(conflict)}: {', '.join(sorted(conflict))}" |
| 353 | + ) |
| 354 | + return _PkgUse( |
| 355 | + immutable, enabled, iuse, force_on.difference(masked), force_off.difference(immutable) |
| 356 | + ) |
| 357 | + |
| 358 | + |
| 359 | +def _build_job(namespace, pkg, pkg_use: _PkgUse, is_test: bool): |
| 360 | + use_expand_prefixes = tuple(s.lower() + "_" for s in namespace.domain.profile.use_expand) |
| 361 | + immutable, enabled, iuse = pkg_use.immutable, pkg_use.enabled, pkg_use.iuse |
| 362 | + |
| 363 | + force_true = immutable.union(pkg_use.force_on, ("test",) if is_test else ()) |
| 364 | + force_false = pkg_use.force_off.union(("test",) if not is_test else ()) |
288 | 365 |
|
289 | 366 | if namespace.random_use == "d": |
290 | 367 | prefer_true = enabled |
@@ -348,19 +425,27 @@ def resolve_unconstrained(): |
348 | 425 | produced += 1 |
349 | 426 | if produced >= wanted: |
350 | 427 | return |
| 428 | + if not produced: |
| 429 | + forced = sorted(pkg_use.force_on) + [f"-{use}" for use in sorted(pkg_use.force_off)] |
| 430 | + raise UnsatisfiableUse( |
| 431 | + f"{pkg.cpvstr}: no USE combination satisfies REQUIRED_USE" |
| 432 | + + (f" with {', '.join(forced)}" if forced else "") |
| 433 | + ) |
351 | 434 | # replay the combinations if REQUIRED_USE admits fewer than asked for, |
352 | 435 | # redrawing the unconstrained flags each pass |
353 | 436 | if exhausted or not randomize: |
354 | 437 | return |
355 | 438 |
|
356 | 439 |
|
357 | | -def _build_jobs(namespace, pkgs): |
| 440 | +def _build_jobs(namespace, pkgs, err): |
358 | 441 | for pkg in pkgs: |
359 | | - for flags in islice(_build_job(namespace, pkg, False), namespace.use_combos): |
| 442 | + pkg_use = _resolve_use(namespace, pkg, err) |
| 443 | + for flags in islice(_build_job(namespace, pkg, pkg_use, False), namespace.use_combos): |
360 | 444 | yield pkg.versioned_atom, False, flags |
361 | 445 |
|
362 | 446 | if namespace.test: |
363 | | - yield pkg.versioned_atom, True, next(iter(_build_job(namespace, pkg, True))) |
| 447 | + flags = next(iter(_build_job(namespace, pkg, pkg_use, True))) |
| 448 | + yield pkg.versioned_atom, True, flags |
364 | 449 |
|
365 | 450 |
|
366 | 451 | def _create_config_dir(directory: Path): |
@@ -430,7 +515,7 @@ def main(options, out, err): |
430 | 515 | from jinja2 import Template |
431 | 516 |
|
432 | 517 | script = Template(template, trim_blocks=True, lstrip_blocks=True).render( |
433 | | - jobs=list(_build_jobs(options, pkgs)), |
| 518 | + jobs=list(_build_jobs(options, pkgs, err)), |
434 | 519 | report_file=job_name + ".report", |
435 | 520 | job_name=job_name, |
436 | 521 | log_dir=options.logs_dir, |
|
0 commit comments