1212from pathlib import Path
1313from typing import TypeAlias , cast
1414
15+ from packaging .requirements import InvalidRequirement , Requirement
16+ from packaging .specifiers import InvalidSpecifier , SpecifierSet
17+ from packaging .version import InvalidVersion , Version
18+
1519from cuda .pathfinder ._binaries .find_nvidia_binary_utility import (
1620 find_nvidia_binary_utility as _find_nvidia_binary_utility ,
1721)
4953
5054ItemKind : TypeAlias = str
5155PackagedWith : TypeAlias = str
52- ConstraintOperator : TypeAlias = str
53- ConstraintArg : TypeAlias = int | str | tuple [str , int ] | None
56+ CtkVersionConstraintArg : TypeAlias = str | SpecifierSet | None
5457
5558_CTK_VERSION_RE = re .compile (r"^(?P<major>\d+)\.(?P<minor>\d+)" )
56- _REQUIRES_DIST_RE = re .compile (r"^\s*(?P<name>[A-Za-z0-9_.-]+)\s*(?P<specifier_text>[^;]*)(?:\s*;|$)" )
57- _VERSION_SPECIFIER_RE = re .compile (r"^\s*(?P<operator>==|<=|>=|<|>)\s*(?P<version>[0-9][A-Za-z0-9.+-]*?(?:\.\*)?)\s*$" )
59+ _CTK_VERSION_CONSTRAINT_ERROR = (
60+ "ctk_version must be None, a non-empty PEP 440 specifier string like '>=13.2,<14', "
61+ "or a packaging.specifiers.SpecifierSet."
62+ )
5863
5964_STATIC_LIBS_PACKAGED_WITH : dict [str , PackagedWith ] = {
6065 "cudadevrt" : "ctk" ,
@@ -89,33 +94,20 @@ class CtkVersion:
8994 def __str__ (self ) -> str :
9095 return f"{ self .major } .{ self .minor } "
9196
97+ def as_pep440_version (self ) -> Version :
98+ return Version (str (self ))
9299
93- @dataclass (frozen = True , slots = True )
94- class ComparisonConstraint :
95- operator : ConstraintOperator
96- value : int
97-
98- def matches (self , candidate : int ) -> bool :
99- if self .operator == "==" :
100- return candidate == self .value
101- if self .operator == "<" :
102- return candidate < self .value
103- if self .operator == "<=" :
104- return candidate <= self .value
105- if self .operator == ">" :
106- return candidate > self .value
107- if self .operator == ">=" :
108- return candidate >= self .value
109- raise AssertionError (f"Unsupported operator: { self .operator !r} " )
110100
111- def __str__ (self ) -> str :
112- return f"{ self .operator } { self .value } "
101+ @dataclass (frozen = True , slots = True )
102+ class CtkVersionConstraint :
103+ specifier : SpecifierSet
104+ text : str
113105
106+ def matches (self , candidate : CtkVersion ) -> bool :
107+ return bool (self .specifier .contains (candidate .as_pep440_version (), prereleases = True ))
114108
115- @dataclass (frozen = True , slots = True )
116- class VersionSpecifier :
117- operator : ConstraintOperator
118- version : str
109+ def __str__ (self ) -> str :
110+ return self .text
119111
120112
121113@dataclass (frozen = True , slots = True )
@@ -147,37 +139,30 @@ def require_compatible(self) -> None:
147139 raise CompatibilityCheckError (self .message )
148140
149141
150- def _coerce_constraint (name : str , raw_value : ConstraintArg ) -> ComparisonConstraint | None :
151- if raw_value is None :
152- return None
153- if isinstance (raw_value , int ):
154- return ComparisonConstraint ("==" , raw_value )
155- if isinstance (raw_value , tuple ):
156- if len (raw_value ) != 2 :
157- raise ValueError (f"{ name } tuple constraints must have exactly two elements." )
158- operator , value = raw_value
159- if operator not in ("==" , "<" , "<=" , ">" , ">=" ):
160- raise ValueError (f"{ name } has unsupported operator { operator !r} ." )
161- if not isinstance (value , int ):
162- raise ValueError (f"{ name } constraint value must be an integer." )
163- return ComparisonConstraint (operator , value )
164- if isinstance (raw_value , str ):
165- match = re .fullmatch (r"\s*(==|<|<=|>|>=)?\s*(\d+)\s*" , raw_value )
166- if match is None :
167- raise ValueError (f"{ name } must be an int, a (operator, value) tuple, or a string like '>=12'." )
168- operator = match .group (1 ) or "=="
169- value = int (match .group (2 ))
170- return ComparisonConstraint (operator , value )
171- raise ValueError (f"{ name } must be an int, a (operator, value) tuple, or a string like '>=12'." )
172-
173-
174142def _parse_ctk_version (cuda_version : str ) -> CtkVersion | None :
175143 match = _CTK_VERSION_RE .match (cuda_version )
176144 if match is None :
177145 return None
178146 return CtkVersion (major = int (match .group ("major" )), minor = int (match .group ("minor" )))
179147
180148
149+ def _coerce_ctk_version_constraint (raw_value : CtkVersionConstraintArg ) -> CtkVersionConstraint | None :
150+ if raw_value is None :
151+ return None
152+ if isinstance (raw_value , SpecifierSet ):
153+ return CtkVersionConstraint (specifier = raw_value , text = str (raw_value ))
154+ if isinstance (raw_value , str ):
155+ stripped = raw_value .strip ()
156+ if not stripped :
157+ raise ValueError (_CTK_VERSION_CONSTRAINT_ERROR )
158+ try :
159+ specifier = SpecifierSet (stripped )
160+ except InvalidSpecifier as exc :
161+ raise ValueError (_CTK_VERSION_CONSTRAINT_ERROR ) from exc
162+ return CtkVersionConstraint (specifier = specifier , text = stripped )
163+ raise ValueError (_CTK_VERSION_CONSTRAINT_ERROR )
164+
165+
181166def _normalize_distribution_name (name : str ) -> str :
182167 return re .sub (r"[-_.]+" , "-" , name ).lower ()
183168
@@ -190,63 +175,6 @@ def _distribution_name(dist: importlib.metadata.Distribution) -> str | None:
190175 return metadata .get ("Name" )
191176
192177
193- def _release_version_parts (version : str ) -> tuple [int , ...] | None :
194- match = re .match (r"^\d+(?:\.\d+)*" , version )
195- if match is None :
196- return None
197- return tuple (int (part ) for part in match .group (0 ).split ("." ))
198-
199-
200- def _compare_release_versions (lhs : tuple [int , ...], rhs : tuple [int , ...]) -> int :
201- max_len = max (len (lhs ), len (rhs ))
202- lhs_padded = lhs + (0 ,) * (max_len - len (lhs ))
203- rhs_padded = rhs + (0 ,) * (max_len - len (rhs ))
204- if lhs_padded < rhs_padded :
205- return - 1
206- if lhs_padded > rhs_padded :
207- return 1
208- return 0
209-
210-
211- def _parse_version_specifiers (specifier_text : str ) -> tuple [VersionSpecifier , ...]:
212- stripped = specifier_text .strip ()
213- if not stripped :
214- return ()
215- parsed : list [VersionSpecifier ] = []
216- for raw_clause in stripped .split ("," ):
217- match = _VERSION_SPECIFIER_RE .match (raw_clause )
218- if match is None :
219- return ()
220- parsed .append (VersionSpecifier (operator = match .group ("operator" ), version = match .group ("version" )))
221- return tuple (parsed )
222-
223-
224- def _version_satisfies_specifiers (version : str , specifiers : tuple [VersionSpecifier , ...]) -> bool :
225- if not specifiers :
226- return False
227- for specifier in specifiers :
228- if specifier .operator == "==" :
229- prefix = specifier .version .removesuffix (".*" )
230- if version == prefix or version .startswith (prefix + "." ):
231- continue
232- return False
233- candidate_parts = _release_version_parts (version )
234- required_parts = _release_version_parts (specifier .version )
235- if candidate_parts is None or required_parts is None :
236- return False
237- comparison = _compare_release_versions (candidate_parts , required_parts )
238- if specifier .operator == "<" and comparison < 0 :
239- continue
240- if specifier .operator == "<=" and comparison <= 0 :
241- continue
242- if specifier .operator == ">" and comparison > 0 :
243- continue
244- if specifier .operator == ">=" and comparison >= 0 :
245- continue
246- return False
247- return True
248-
249-
250178@functools .cache
251179def _owned_distribution_candidates (abs_path : str ) -> tuple [tuple [str , str ], ...]:
252180 normalized_abs_path = os .path .normpath (os .path .abspath (abs_path ))
@@ -263,41 +191,33 @@ def _owned_distribution_candidates(abs_path: str) -> tuple[tuple[str, str], ...]
263191
264192
265193@functools .cache
266- def _cuda_toolkit_requirement_maps () -> tuple [
267- tuple [str , CtkVersion , dict [str , tuple [tuple [VersionSpecifier , ...], ...]]], ...
268- ]:
269- results : list [tuple [str , CtkVersion , dict [str , tuple [tuple [VersionSpecifier , ...], ...]]]] = []
194+ def _cuda_toolkit_requirement_maps () -> tuple [tuple [str , CtkVersion , dict [str , tuple [SpecifierSet , ...]]], ...]:
195+ results : list [tuple [str , CtkVersion , dict [str , tuple [SpecifierSet , ...]]]] = []
270196 for dist in importlib .metadata .distributions ():
271197 dist_name = _distribution_name (dist )
272198 if _normalize_distribution_name (dist_name or "" ) != "cuda-toolkit" :
273199 continue
274200 ctk_version = _parse_ctk_version (dist .version )
275201 if ctk_version is None :
276202 continue
277- requirement_map : dict [str , set [tuple [VersionSpecifier , ...]]] = {}
278- for requirement in dist .requires or ():
279- match = _REQUIRES_DIST_RE .match (requirement )
280- if match is None :
203+ requirement_map : dict [str , set [str ]] = {}
204+ for requirement_text in dist .requires or ():
205+ try :
206+ requirement = Requirement (requirement_text )
207+ except InvalidRequirement :
281208 continue
282- req_name = _normalize_distribution_name (match .group ("name" ))
283- parsed_specifiers = _parse_version_specifiers (match .group ("specifier_text" ))
284- if not parsed_specifiers :
209+ specifier_text = str (requirement .specifier )
210+ if not specifier_text :
285211 continue
286- requirement_map .setdefault (req_name , set ()).add (parsed_specifiers )
212+ req_name = _normalize_distribution_name (requirement .name )
213+ requirement_map .setdefault (req_name , set ()).add (specifier_text )
287214 results .append (
288215 (
289216 dist .version ,
290217 ctk_version ,
291218 {
292- name : tuple (
293- sorted (
294- specifier_sets ,
295- key = lambda specifiers : tuple (
296- (specifier .operator , specifier .version ) for specifier in specifiers
297- ),
298- )
299- )
300- for name , specifier_sets in requirement_map .items ()
219+ name : tuple (SpecifierSet (specifier_text ) for specifier_text in sorted (specifier_set_texts ))
220+ for name , specifier_set_texts in requirement_map .items ()
301221 },
302222 )
303223 )
@@ -307,11 +227,16 @@ def _cuda_toolkit_requirement_maps() -> tuple[
307227def _wheel_metadata_for_abs_path (abs_path : str ) -> CtkMetadata | None :
308228 matched_versions : dict [CtkVersion , str ] = {}
309229 for owner_name , owner_version in _owned_distribution_candidates (abs_path ):
230+ try :
231+ owner_parsed_version = Version (owner_version )
232+ except InvalidVersion :
233+ continue
310234 normalized_owner_name = _normalize_distribution_name (owner_name )
311235 for toolkit_dist_version , ctk_version , requirement_map in _cuda_toolkit_requirement_maps ():
312236 requirement_specifier_sets = requirement_map .get (normalized_owner_name , ())
313237 if not any (
314- _version_satisfies_specifiers (owner_version , specifiers ) for specifiers in requirement_specifier_sets
238+ specifier_set .contains (owner_parsed_version , prereleases = True )
239+ for specifier_set in requirement_specifier_sets
315240 ):
316241 continue
317242 matched_versions [ctk_version ] = (
@@ -527,12 +452,10 @@ class CompatibilityGuardRails:
527452 def __init__ (
528453 self ,
529454 * ,
530- ctk_major : ConstraintArg = None ,
531- ctk_minor : ConstraintArg = None ,
455+ ctk_version : CtkVersionConstraintArg = None ,
532456 driver_cuda_version : DriverCudaVersion | None = None ,
533457 ) -> None :
534- self ._ctk_major_constraint = _coerce_constraint ("ctk_major" , ctk_major )
535- self ._ctk_minor_constraint = _coerce_constraint ("ctk_minor" , ctk_minor )
458+ self ._ctk_version_constraint = _coerce_ctk_version_constraint (ctk_version )
536459 self ._configured_driver_cuda_version = driver_cuda_version
537460 self ._driver_cuda_version = driver_cuda_version
538461 self ._resolved_items : list [ResolvedItem ] = []
@@ -567,15 +490,10 @@ def _enforce_ctk_metadata(self, item: ResolvedItem) -> None:
567490
568491 def _enforce_constraints (self , item : ResolvedItem ) -> None :
569492 assert item .ctk_version is not None
570- if self ._ctk_major_constraint is not None and not self ._ctk_major_constraint .matches (item .ctk_version .major ):
571- raise CompatibilityCheckError (
572- f"{ item .describe ()} resolves to CTK { item .ctk_version } , which does not satisfy "
573- f"ctk_major{ self ._ctk_major_constraint } ."
574- )
575- if self ._ctk_minor_constraint is not None and not self ._ctk_minor_constraint .matches (item .ctk_version .minor ):
493+ if self ._ctk_version_constraint is not None and not self ._ctk_version_constraint .matches (item .ctk_version ):
576494 raise CompatibilityCheckError (
577495 f"{ item .describe ()} resolves to CTK { item .ctk_version } , which does not satisfy "
578- f"ctk_minor { self ._ctk_minor_constraint } ."
496+ f"ctk_version { self ._ctk_version_constraint } ."
579497 )
580498
581499 def _anchor_item (self ) -> ResolvedItem | None :
0 commit comments