Skip to content

Commit b5df3e2

Browse files
committed
pip: Allow to specify per-module artifact policy
Closes: #525
1 parent 9d9ad80 commit b5df3e2

1 file changed

Lines changed: 137 additions & 57 deletions

File tree

pip/flatpak-pip-generator.py

Lines changed: 137 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from collections import OrderedDict
2727
from collections.abc import Iterator
2828
from contextlib import suppress
29-
from typing import Any, TextIO, Callable
29+
from typing import Any, TextIO, Callable, NoReturn
3030
import operator
3131
from packaging.version import Version
3232
from packaging.tags import Tag
@@ -37,6 +37,20 @@
3737
except ImportError:
3838
sys.exit("Please install the 'requirements-parser' module")
3939

40+
ARTIFACT_POLICIES = ("universal", "platform", "sdist")
41+
42+
43+
def parse_artifact_policy(s: str) -> tuple[str, str]:
44+
module, sep, policy = s.partition("=")
45+
module = module.strip()
46+
policy = policy.strip()
47+
48+
if not (sep and module and policy in ARTIFACT_POLICIES):
49+
raise argparse.ArgumentTypeError(f"Invalid artifact policy {s!r}")
50+
51+
return module, policy
52+
53+
4054
parser = argparse.ArgumentParser()
4155
parser.add_argument("packages", nargs="*")
4256
parser.add_argument(
@@ -127,10 +141,33 @@
127141
type=lambda s: [x.strip().lower() for x in s.split(",") if x.strip()],
128142
help="Comma-separated list of architectures for which platform wheels should be generated (default: x86_64,aarch64)",
129143
)
130-
144+
parser.add_argument(
145+
"--artifact-policy",
146+
dest="artifact_policies",
147+
metavar="MODULE=POLICY",
148+
action="append",
149+
default=[],
150+
type=parse_artifact_policy,
151+
help=argparse.SUPPRESS,
152+
)
131153

132154
opts = parser.parse_args()
133155

156+
157+
def normalize_name(name: str) -> str:
158+
return re.sub(r"[-_]+", "-", name.lower())
159+
160+
161+
artifact_policy_map: dict[str, str] = {}
162+
163+
for raw_name, policy in opts.artifact_policies:
164+
norm = normalize_name(raw_name.lower())
165+
if norm in artifact_policy_map and artifact_policy_map[norm] != policy:
166+
sys.exit(f"Conflicting artifact policies for {raw_name!r}")
167+
artifact_policy_map[norm] = policy
168+
169+
policy_platform_pkgs = {n for n, p in artifact_policy_map.items() if p == "platform"}
170+
134171
if opts.runtime:
135172
parts = opts.runtime.split("//", 1)
136173
if len(parts) != 2 or not parts[0] or not parts[1]:
@@ -239,7 +276,15 @@ def get_platform_tags_from_runtime(arch: str) -> set[Tag] | None:
239276

240277
SUPPORTED_TAG_SET: set[Tag] | None = None
241278

242-
if opts.prefer_wheels:
279+
effective_prefer_wheels = set(opts.prefer_wheels) | policy_platform_pkgs
280+
281+
if effective_prefer_wheels:
282+
if not opts.runtime:
283+
sys.exit(
284+
"--prefer-wheels or --artifact-policy with 'platform' policy"
285+
+ " requires --runtime to ensure correct platform wheel"
286+
+ " selection"
287+
)
243288
runtime_arch = get_runtime_arch()
244289
platform_tags = get_platform_tags_from_runtime(runtime_arch)
245290

@@ -264,10 +309,6 @@ def get_platform_tags_from_runtime(arch: str) -> set[Tag] | None:
264309
assert SUPPORTED_TAG_SET is not None
265310

266311

267-
def normalize_name(name: str) -> str:
268-
return re.sub(r"[-_]+", "-", name.lower())
269-
270-
271312
def make_source(
272313
file_info: dict[str, str | dict[str, str]], only_arches: list[str] | None = None
273314
) -> dict[str, str | list[str] | dict[str, str]]:
@@ -321,6 +362,7 @@ def resolve_package_sources(
321362
candidates: list[str],
322363
is_preferred: bool,
323364
known_hashes: set[str] | None = None,
365+
policy: str | None = None,
324366
) -> tuple[list[dict], list[str]]:
325367
sources_out: list[dict] = []
326368
pypi_files: list[dict] | None = None
@@ -361,6 +403,8 @@ def get_pypi_files() -> list[dict]:
361403
pypi_files = all_files
362404
return pypi_files
363405

406+
files = get_pypi_files()
407+
364408
def is_universal(filename: str) -> bool:
365409
return filename.endswith(".whl") and filename[:-4].split("-")[-1] == "any"
366410

@@ -447,7 +491,7 @@ def relaxed_compat(pytags: list[str], abitags: list[str], py_ver: int) -> bool:
447491

448492
def collect(compat_fn) -> list[dict]:
449493
result = []
450-
for f in get_pypi_files():
494+
for f in files:
451495
fn = f["filename"]
452496
if not fn.endswith(".whl"):
453497
continue
@@ -467,66 +511,96 @@ def collect(compat_fn) -> list[dict]:
467511
)
468512
return found
469513

470-
pypi_universal = next(
471-
(f for f in get_pypi_files() if is_universal(f["filename"])),
472-
None,
473-
)
514+
def format_error(msg: str) -> str:
515+
if known_hashes:
516+
return f"{msg} (constrained by hashes in requirements file)"
517+
return msg
518+
519+
def pkg_error(msg: str) -> str:
520+
return f"{name}: {format_error(msg)}"
521+
522+
def policy_error(msg: str) -> NoReturn:
523+
sys.exit(f"ERROR: artifact-policy '{policy}' for {name!r}: {format_error(msg)}")
524+
525+
def find_universal() -> dict[str, str | dict[str, str]] | None:
526+
return next((f for f in files if is_universal(f["filename"])), None)
527+
528+
def find_sdist() -> dict[str, str | dict[str, str]] | None:
529+
return next((f for f in files if not f["filename"].endswith(".whl")), None)
530+
531+
def resolve_platform_wheels(
532+
platform_policy: bool,
533+
) -> tuple[
534+
list[dict[str, str | list[str] | dict[str, str]]],
535+
list[str],
536+
]:
537+
assert SUPPORTED_TAG_SET is not None
538+
native_py_ver = runtime_python_ver(SUPPORTED_TAG_SET)
539+
540+
out = []
541+
errs = []
542+
543+
for arch in DEFAULT_WHEEL_ARCHES:
544+
arch_tags = get_tags_for_arch(arch)
545+
py_ver = runtime_python_ver(arch_tags) or native_py_ver
546+
547+
if py_ver is None:
548+
msg = f"Unable to determine Python version for arch '{arch}'"
549+
if platform_policy:
550+
policy_error(msg)
551+
errs.append(pkg_error(msg))
552+
continue
553+
554+
arch_candidates = arch_platform_candidates(arch, cast(int, py_ver))
555+
556+
if not arch_candidates:
557+
msg = f"No compatible platform wheel found for arch '{arch}'"
558+
if platform_policy:
559+
policy_error(msg)
560+
errs.append(pkg_error(msg))
561+
continue
562+
563+
wheel = max(
564+
arch_candidates,
565+
key=lambda f: wheel_priority(f["filename"], arch_tags),
566+
)
567+
out.append(make_source(wheel, only_arches=[arch]))
568+
569+
return out, errs
570+
571+
pypi_universal = find_universal()
572+
pypi_sdist = find_sdist()
573+
574+
if policy == "universal":
575+
if not pypi_universal:
576+
policy_error("No universal wheel found on PyPI")
577+
return [make_source(pypi_universal)], []
578+
579+
if policy == "sdist":
580+
if not pypi_sdist:
581+
policy_error("No sdist found on PyPI")
582+
return [make_source(pypi_sdist)], []
583+
584+
if policy == "platform":
585+
sources_out, _ = resolve_platform_wheels(platform_policy=True)
586+
return sources_out, []
587+
474588
if pypi_universal:
475589
return [make_source(pypi_universal)], []
476590

477591
if not is_preferred:
478-
pypi_sdist = next(
479-
(f for f in get_pypi_files() if not f["filename"].endswith(".whl")),
480-
None,
481-
)
482592
if pypi_sdist:
483593
return [make_source(pypi_sdist)], []
484594

485-
if any(is_platform_wheel(f["filename"]) for f in get_pypi_files()):
595+
if any(is_platform_wheel(f["filename"]) for f in files):
486596
return [], [f"__PLATFORM_ONLY__:{name}"]
487597

488598
if known_hashes:
489-
return [], [
490-
f"{name}: No suitable source found among artifacts matching the "
491-
"hashes in the requirements file."
492-
]
599+
return [], [pkg_error("No suitable source found on PyPI")]
493600
else:
494601
return [], [f"{name}: No suitable source found on PyPI"]
495602

496-
assert SUPPORTED_TAG_SET is not None
497-
native_py_ver = runtime_python_ver(SUPPORTED_TAG_SET)
498-
499-
errors: list[str] = []
500-
501-
for arch in DEFAULT_WHEEL_ARCHES:
502-
arch_tags = get_tags_for_arch(arch)
503-
py_ver = runtime_python_ver(arch_tags) or native_py_ver
504-
505-
if py_ver is None:
506-
errors.append(
507-
f"{name}: Unable to determine Python version for arch '{arch}'"
508-
)
509-
continue
510-
511-
arch_candidates = arch_platform_candidates(arch, cast(int, py_ver))
512-
513-
if not arch_candidates:
514-
if known_hashes:
515-
errors.append(
516-
f"{name}: No compatible platform wheel for arch '{arch}' among artifacts matching "
517-
"the hashes in the requirements file."
518-
)
519-
else:
520-
errors.append(
521-
f"{name}: No platform wheel found for arch '{arch}' on PyPI"
522-
)
523-
continue
524-
525-
wheel = max(
526-
arch_candidates, key=lambda f: wheel_priority(f["filename"], arch_tags)
527-
)
528-
sources_out.append(make_source(wheel, only_arches=[arch]))
529-
603+
sources_out, errors = resolve_platform_wheels(platform_policy=False)
530604
return sources_out, errors
531605

532606

@@ -940,7 +1014,7 @@ def to_ver(v: str) -> Version | None:
9401014
sources: dict[str, Any] = {}
9411015
unresolved_dependencies_errors = []
9421016
prefer_wheels_missing: list[str] = []
943-
prefer_set = {normalize_name(p) for p in opts.prefer_wheels}
1017+
prefer_set = {normalize_name(p) for p in effective_prefer_wheels}
9441018

9451019
tempdir_prefix = f"pip-generator-{output_package}"
9461020
with tempfile.TemporaryDirectory(prefix=tempdir_prefix) as tempdir:
@@ -1020,10 +1094,16 @@ def to_ver(v: str) -> Version | None:
10201094
name_cf = normalize_name(name)
10211095
version = get_file_version(candidates[0])
10221096
is_preferred = name_cf in prefer_set
1097+
pkg_policy = artifact_policy_map.get(name_cf)
10231098

10241099
known_hashes = req_hashes_by_pkg.get(name_cf) if use_hash else None
10251100
resolved, errors = resolve_package_sources(
1026-
name_cf, version, candidates, is_preferred, known_hashes
1101+
name_cf,
1102+
version,
1103+
candidates,
1104+
is_preferred,
1105+
known_hashes,
1106+
policy=pkg_policy,
10271107
)
10281108

10291109
for error in errors:

0 commit comments

Comments
 (0)