@@ -58,16 +58,18 @@ index 1af8978..15fee7f 100644
58
58
# file in .data maps to same location as file in wheel root).
59
59
diff --git a/pip/_internal/utils/graalpy.py b/pip/_internal/utils/graalpy.py
60
60
new file mode 100644
61
- index 0000000..59a5cdb
61
+ index 0000000..dd8f441
62
62
--- /dev/null
63
63
+++ b/pip/_internal/utils/graalpy.py
64
- @@ -0,0 +1,178 @@
64
+ @@ -0,0 +1,188 @@
65
65
+ # ATTENTION: GraalPy uses existence of this module to verify that it is
66
66
+ # running a patched pip in pip_hook.py
67
67
+ import os
68
68
+ import re
69
69
+ from pathlib import Path
70
70
+
71
+ + from pip._vendor import tomli
72
+ + from pip._vendor.packaging.specifiers import Specifier
71
73
+ from pip._vendor.packaging.version import VERSION_PATTERN
72
74
+
73
75
+ PATCHES_BASE_DIRS = [os.path.join(__graalpython__.core_home, "patches")]
@@ -92,57 +94,73 @@ index 0000000..59a5cdb
92
94
+ for package_dir in Path(base_dir).iterdir():
93
95
+ denormalized_name = package_dir.name
94
96
+ 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):
121
149
+ 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)
124
154
+ return versions
125
155
+
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
146
164
+
147
165
+
148
166
+ __PATCH_REPOSITORY = None
@@ -155,6 +173,9 @@ index 0000000..59a5cdb
155
173
+ return __PATCH_REPOSITORY
156
174
+
157
175
+
176
+ + __already_patched = set()
177
+ +
178
+ +
158
179
+ def apply_graalpy_patches(filename, location):
159
180
+ """
160
181
+ Applies any GraalPy patches to package extracted from 'filename' into 'location'.
@@ -184,60 +205,49 @@ index 0000000..59a5cdb
184
205
+ if is_wheel and is_bundled_wheel(location, name):
185
206
+ return
186
207
+
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
+ +
187
212
+ print(f"Looking for GraalPy patches for {name}")
188
213
+ repository = get_patch_repository()
189
214
+
190
215
+ if is_wheel:
191
- + query_dist_types = ('whl',)
192
216
+ # patches intended for binary distribution:
193
- + patch = repository.get_patch (name, version, 'whl ')
217
+ + rule = repository.get_matching_rule (name, version, 'wheel ')
194
218
+ else:
195
- + query_dist_types = ('sdist', 'whl')
196
219
+ # 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):
215
240
+ print("We have patches to make this package work on GraalVM for some version(s).")
216
241
+ 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}')
229
244
+
230
245
+
231
246
+ def apply_graalpy_sort_order(sort_key_func):
232
247
+ def wrapper(self, candidate):
233
248
+ 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
241
251
+
242
252
+ return wrapper
243
253
diff --git a/pip/_internal/utils/unpacking.py b/pip/_internal/utils/unpacking.py
0 commit comments