2626from collections import OrderedDict
2727from collections .abc import Iterator
2828from contextlib import suppress
29- from typing import Any , TextIO , Callable
29+ from typing import Any , TextIO , Callable , NoReturn
3030import operator
3131from packaging .version import Version
3232from packaging .tags import Tag
3737except 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+
4054parser = argparse .ArgumentParser ()
4155parser .add_argument ("packages" , nargs = "*" )
4256parser .add_argument (
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
132154opts = 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+
134171if 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
240277SUPPORTED_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-
271312def 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:
9401014sources : dict [str , Any ] = {}
9411015unresolved_dependencies_errors = []
9421016prefer_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
9451019tempdir_prefix = f"pip-generator-{ output_package } "
9461020with 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