@@ -58,16 +58,18 @@ index 1af8978..15fee7f 100644
5858 # file in .data maps to same location as file in wheel root).
5959diff --git a/pip/_internal/utils/graalpy.py b/pip/_internal/utils/graalpy.py
6060new file mode 100644
61- index 0000000..59a5cdb
61+ index 0000000..dd8f441
6262--- /dev/null
6363+++ b/pip/_internal/utils/graalpy.py
64- @@ -0,0 +1,178 @@
64+ @@ -0,0 +1,188 @@
6565+ # ATTENTION: GraalPy uses existence of this module to verify that it is
6666+ # running a patched pip in pip_hook.py
6767+ import os
6868+ import re
6969+ from pathlib import Path
7070+
71+ + from pip._vendor import tomli
72+ + from pip._vendor.packaging.specifiers import Specifier
7173+ from pip._vendor.packaging.version import VERSION_PATTERN
7274+
7375+ PATCHES_BASE_DIRS = [os.path.join(__graalpython__.core_home, "patches")]
@@ -92,57 +94,73 @@ index 0000000..59a5cdb
9294+ for package_dir in Path(base_dir).iterdir():
9395+ denormalized_name = package_dir.name
9496+ normalized_name = normalize_name(denormalized_name)
95- + entry = {}
96- + for dist_type in ('whl', 'sdist'):
97- + typedir = package_dir / dist_type
98- + if typedir.is_dir():
99- + versions = {}
100- + subentry = {'versions': versions}
101- + for file in typedir.iterdir():
102- + if file.suffix == '.patch':
103- + if file.stem == denormalized_name:
104- + versions[None] = file
105- + elif (version := file.stem.removeprefix(f'{denormalized_name}-')) != file.stem:
106- + versions[version] = file
107- + elif file.suffix == '.dir':
108- + subentry['dir'] = file
109- + entry[dist_type] = subentry
110- + self._repository[normalized_name] = entry
111- +
112- + def _deep_get(self, *args):
113- + res = self._repository
114- + for arg in args:
115- + res = res.get(arg)
116- + if not res:
117- + return None
118- + return res
119- +
120- + def get_patch_versions(self, name, dist_types=('whl', 'sdist')):
97+ + metadata = {}
98+ + if (metadata_path := package_dir / 'metadata.toml').is_file():
99+ + with open(metadata_path, 'rb') as f:
100+ + metadata = tomli.load(f)
101+ + metadata.setdefault('rules', [])
102+ + for rule in metadata['rules']:
103+ + if 'patch' in rule:
104+ + rule['patch'] = package_dir / rule['patch']
105+ + else:
106+ + # TODO legacy structure, simplify when we get rid of ginstall
107+ + metadata['rules'] = []
108+ + for dist_type in ('whl', 'sdist'):
109+ + typedir = package_dir / dist_type
110+ + if typedir.is_dir():
111+ + files = sorted(typedir.iterdir(), key=lambda f: len(f.name), reverse=True)
112+ + for file in files:
113+ + if file.suffix == '.patch':
114+ + if file.stem == denormalized_name:
115+ + metadata['rules'].append({
116+ + 'patch': str(file),
117+ + 'type': dist_type,
118+ + })
119+ + elif (version := file.stem.removeprefix(f'{denormalized_name}-')) != file.stem:
120+ + metadata['rules'].append({
121+ + 'version': f'== {version}.*',
122+ + 'patch': file,
123+ + 'type': dist_type,
124+ + })
125+ + for file in files:
126+ + if file.suffix == '.dir':
127+ + with open(file) as f:
128+ + subdir = f.read().strip()
129+ + for rule in metadata['rules']:
130+ + rule['subdir'] = subdir
131+ + self._repository[normalized_name] = metadata
132+ +
133+ + def get_rules(self, name):
134+ + if metadata := self._repository.get(normalize_name(name)):
135+ + return metadata['rules']
136+ +
137+ + def get_priority_for_version(self, name, version):
138+ + if rules := self.get_rules(name):
139+ + for rule in rules:
140+ + if self.rule_matches_version(rule, version):
141+ + return rule.get('install-priority', 1)
142+ + return 0
143+ +
144+ + @staticmethod
145+ + def rule_matches_version(rule, version):
146+ + return not rule.get('version') or Specifier(rule['version']).contains(version)
147+ +
148+ + def get_suggested_version_specs(self, name):
121149+ versions = set()
122- + for dist_type in dist_types:
123- + versions |= set(self._deep_get(normalize_name(name), dist_type, 'versions') or {})
150+ + if rules := self.get_rules(name):
151+ + for rule in rules:
152+ + if 'patch' in rule and rule.get('install-priority', 1) > 0 and (version := rule.get('version')):
153+ + versions.add(version)
124154+ return versions
125155+
126- + def get_patch(self, name, requested_version, dist_type):
127- + versions = self._deep_get(normalize_name(name), dist_type, 'versions')
128- + if not versions:
129- + return None
130- + while True:
131- + if patch := versions.get(requested_version):
132- + return patch
133- + if not requested_version:
134- + return None
135- + split = requested_version.rsplit('.', 1)
136- + if len(split) == 2:
137- + requested_version = split[0]
138- + else:
139- + requested_version = None
140- +
141- + def get_patch_subdir(self, name):
142- + dir_file = self._deep_get(normalize_name(name), 'whl', 'dir')
143- + if dir_file:
144- + with open(dir_file) as f:
145- + return f.read().strip()
156+ + def get_matching_rule(self, name, requested_version, dist_type):
157+ + if metadata := self.get_rules(name):
158+ + for rule in metadata:
159+ + if rule.get('dist-type', dist_type) != dist_type:
160+ + continue
161+ + if not self.rule_matches_version(rule, requested_version):
162+ + continue
163+ + return rule
146164+
147165+
148166+ __PATCH_REPOSITORY = None
@@ -155,6 +173,9 @@ index 0000000..59a5cdb
155173+ return __PATCH_REPOSITORY
156174+
157175+
176+ + __already_patched = set()
177+ +
178+ +
158179+ def apply_graalpy_patches(filename, location):
159180+ """
160181+ Applies any GraalPy patches to package extracted from 'filename' into 'location'.
@@ -184,60 +205,49 @@ index 0000000..59a5cdb
184205+ if is_wheel and is_bundled_wheel(location, name):
185206+ return
186207+
208+ + # When we patch a sdist, pip may call us again to process the wheel produced from it
209+ + if (name, version) in __already_patched:
210+ + return
211+ +
187212+ print(f"Looking for GraalPy patches for {name}")
188213+ repository = get_patch_repository()
189214+
190215+ if is_wheel:
191- + query_dist_types = ('whl',)
192216+ # patches intended for binary distribution:
193- + patch = repository.get_patch (name, version, 'whl ')
217+ + rule = repository.get_matching_rule (name, version, 'wheel ')
194218+ else:
195- + query_dist_types = ('sdist', 'whl')
196219+ # patches intended for source distribution if applicable
197- + patch = repository.get_patch(name, version, 'sdist')
198- + if not patch:
199- + patch = repository.get_patch(name, version, 'whl')
200- + if subdir := repository.get_patch_subdir(name):
201- + # we may need to change wd if we are actually patching a source distribution
202- + # with a patch intended for a binary distribution, because in the source
203- + # distribution the actual deployed sources may be in a subdirectory (typically "src")
204- + location = os.path.join(location, subdir)
205- + if patch:
206- + print(f"Patching package {name} using {patch}")
207- + try:
208- + subprocess.run(["patch", "-f", "-d", location, "-p1", "-i", str(patch)], check=True)
209- + except FileNotFoundError:
210- + print(
211- + "WARNING: GraalPy needs the 'patch' utility to apply compatibility patches. Please install it using your system's package manager.")
212- + except subprocess.CalledProcessError:
213- + print(f"Applying GraalPy patch failed for {name}. The package may still work.")
214- + elif versions := repository.get_patch_versions(name, dist_types=query_dist_types):
220+ + rule = repository.get_matching_rule(name, version, 'sdist')
221+ + if not rule:
222+ + rule = repository.get_matching_rule(name, version, 'wheel')
223+ + if rule and (subdir := rule.get('subdir')):
224+ + # we may need to change wd if we are actually patching a source distribution
225+ + # with a patch intended for a binary distribution, because in the source
226+ + # distribution the actual deployed sources may be in a subdirectory (typically "src")
227+ + location = os.path.join(location, subdir)
228+ + if rule:
229+ + if patch := rule.get('patch'):
230+ + print(f"Patching package {name} using {patch}")
231+ + try:
232+ + subprocess.run(["patch", "-f", "-d", location, "-p1", "-i", str(patch)], check=True)
233+ + except FileNotFoundError:
234+ + print(
235+ + "WARNING: GraalPy needs the 'patch' utility to apply compatibility patches. Please install it using your system's package manager.")
236+ + except subprocess.CalledProcessError:
237+ + print(f"Applying GraalPy patch failed for {name}. The package may still work.")
238+ + __already_patched.add((name, version))
239+ + elif version_specs := repository.get_suggested_version_specs(name):
215240+ print("We have patches to make this package work on GraalVM for some version(s).")
216241+ print("If installing or running fails, consider using one of the versions that we have patches for:")
217- + for version in versions:
218- + print(f'- {version}')
219- +
220- +
221- + def version_match(v1, v2):
222- + if v2 is None:
223- + # Unversioned patch matches everything
224- + return True
225- + for c1, c2 in zip(v1.split('.'), v2.split('.')):
226- + if c1 != c2:
227- + return False
228- + return True
242+ + for version_spec in version_specs:
243+ + print(f'{name} {version_spec}')
229244+
230245+
231246+ def apply_graalpy_sort_order(sort_key_func):
232247+ def wrapper(self, candidate):
233248+ default_sort_key = sort_key_func(self, candidate)
234- + name = candidate.name
235- + version = str(candidate.version)
236- + patched_version_candidates = get_patch_repository().get_patch_versions(name)
237- + for patched_version in patched_version_candidates:
238- + if version_match(version, patched_version):
239- + return (1,) + default_sort_key
240- + return (0,) + default_sort_key
249+ + priority = get_patch_repository().get_priority_for_version(candidate.name, str(candidate.version))
250+ + return priority, default_sort_key
241251+
242252+ return wrapper
243253diff --git a/pip/_internal/utils/unpacking.py b/pip/_internal/utils/unpacking.py
0 commit comments