@@ -32,6 +32,31 @@ index 02ba608..85c7c22 100644
32
32
help="Don't periodically check PyPI to determine whether a new version "
33
33
"of pip is available for download. Implied with --no-index.",
34
34
)
35
+ diff --git a/pip/_internal/cli/req_command.py b/pip/_internal/cli/req_command.py
36
+ index 86070f1..2c8889a 100644
37
+ --- a/pip/_internal/cli/req_command.py
38
+ +++ b/pip/_internal/cli/req_command.py
39
+ @@ -68,6 +68,10 @@ def _create_truststore_ssl_context() -> Optional["SSLContext"]:
40
+ return truststore.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
41
+
42
+
43
+ + # GraalPy change: we need the session to fetch remote metadata, but it would be too difficult to pass it down all
44
+ + # the possible code paths
45
+ + _GRAALPY_SESSION = None
46
+ +
47
+ class SessionCommandMixin(CommandContextMixIn):
48
+
49
+ """
50
+ @@ -100,6 +104,9 @@ class SessionCommandMixin(CommandContextMixIn):
51
+ # automatically ContextManager[Any] and self._session becomes Any,
52
+ # then https://github.com/python/mypy/issues/7696 kicks in
53
+ assert self._session is not None
54
+ + # GraalPy change
55
+ + global _GRAALPY_SESSION
56
+ + _GRAALPY_SESSION = self._session
57
+ return self._session
58
+
59
+ def _build_session(
35
60
diff --git a/pip/_internal/index/package_finder.py b/pip/_internal/index/package_finder.py
36
61
index b6f8d57..6c37e0b 100644
37
62
--- a/pip/_internal/index/package_finder.py
@@ -102,50 +127,76 @@ index a8cd133..20dd1e6 100644
102
127
# file in .data maps to same location as file in wheel root).
103
128
diff --git a/pip/_internal/utils/graalpy.py b/pip/_internal/utils/graalpy.py
104
129
new file mode 100644
105
- index 0000000..2a67f4b
130
+ index 0000000..375f66e
106
131
--- /dev/null
107
132
+++ b/pip/_internal/utils/graalpy.py
108
- @@ -0,0 +1,209 @@
109
- + # ATTENTION: GraalPy uses existence of this module to verify that it is
110
- + # running a patched pip in pip_hook.py
133
+ @@ -0,0 +1,351 @@
134
+ + import abc
135
+ + import logging
111
136
+ import os
112
137
+ import re
138
+ + import tempfile
113
139
+ import zipfile
140
+ + from contextlib import contextmanager
114
141
+ from pathlib import Path
115
- + from urllib.parse import urlparse
142
+ + from tomllib import TOMLDecodeError
143
+ + from urllib.parse import urlparse, urljoin, urlunparse
116
144
+
117
145
+ from pip._internal.models.candidate import InstallationCandidate
118
146
+ from pip._internal.models.link import Link
119
- + from pip._vendor import tomli
147
+ + from pip._internal.utils.urls import url_to_path
148
+ + from pip._vendor import tomli, requests
120
149
+ from pip._vendor.packaging.specifiers import SpecifierSet
150
+ + from pip._vendor.packaging.utils import canonicalize_name
121
151
+ from pip._vendor.packaging.version import VERSION_PATTERN
122
152
+
123
- + METADATA_PATH = os.environ.get(
124
- + 'PIP_GRAALPY_METADATA',
125
- + os.path.join(__graalpython__.core_home, 'patches', 'metadata.toml'),
126
- + )
153
+ + MARKER_FILE_NAME = 'GRAALPY_MARKER'
154
+ + METADATA_FILENAME = 'metadata.toml'
155
+ + DEFAULT_PATCHES_PATH = Path(__graalpython__.core_home) / 'patches'
156
+ + VERSION_PARAMETER = '<version>'
157
+ + DEFAULT_PATCHES_URL = f'https://raw.githubusercontent.com/oracle/graalpython/refs/heads/github/patches/{VERSION_PARAMETER}/graalpython/lib-graalpython/patches/'
158
+ +
159
+ + PATCHES_URL = os.environ.get('PIP_GRAALPY_PATCHES_URL', DEFAULT_PATCHES_URL)
127
160
+ DISABLE_PATCHING = os.environ.get('PIP_GRAALPY_DISABLE_PATCHING', '').lower() in ('true', '1')
128
161
+ DISABLE_VERSION_SELECTION = os.environ.get('PIP_GRAALPY_DISABLE_VERSION_SELECTION', '').lower() in ('true', '1')
129
162
+
163
+ + GRAALPY_VERSION = os.environ.get('TEST_PIP_GRAALPY_VERSION', __graalpython__.get_graalvm_version())
164
+ +
165
+ + logger = logging.getLogger(__name__)
166
+ +
130
167
+
131
- + def normalize_name(name):
132
- + return re.sub('[-_.]+', '-', name).lower()
168
+ + def url_for_file(patches_url, filename):
169
+ + scheme, netloc, path, params, query, fragment = urlparse(patches_url)
170
+ + path = urljoin(path, filename)
171
+ + return urlunparse((scheme, netloc, path, params, query, fragment))
133
172
+
134
173
+
135
- + class PatchRepository:
136
- + def __init__(self, metadata_path):
137
- + self._repository = {}
138
- + self.metadata_path = Path(metadata_path)
139
- + if self.metadata_path.exists():
140
- + with open(self.metadata_path, 'rb') as f:
141
- + self._repository = {normalize_name(name): metadata for name, metadata in tomli.load(f).items()}
174
+ + class RepositoryException(Exception):
175
+ + pass
176
+ +
177
+ +
178
+ + class RepositoryNotFoundException(RepositoryException):
179
+ + pass
180
+ +
181
+ +
182
+ + class AbstractPatchRepository(metaclass=abc.ABCMeta):
183
+ + def __init__(self, metadata: dict):
184
+ + self._repository = metadata
185
+ +
186
+ + @staticmethod
187
+ + def metadata_from_string(metadata_content) -> dict:
188
+ + try:
189
+ + parsed_metadata = tomli.loads(metadata_content)
190
+ + return {canonicalize_name(name): data for name, data in parsed_metadata.items()}
191
+ + except TOMLDecodeError as e:
192
+ + raise RepositoryException(f"'{METADATA_FILENAME}' cannot be parsed: {e}")
142
193
+
143
194
+ def get_rules(self, name):
144
- + if metadata := self._repository.get(normalize_name (name)):
195
+ + if metadata := self._repository.get(canonicalize_name (name)):
145
196
+ return metadata.get('rules')
146
197
+
147
198
+ def get_add_sources(self, name):
148
- + if metadata := self._repository.get(normalize_name (name)):
199
+ + if metadata := self._repository.get(canonicalize_name (name)):
149
200
+ return metadata.get('add-sources')
150
201
+
151
202
+ def get_priority_for_version(self, name, version):
@@ -176,17 +227,134 @@ index 0000000..2a67f4b
176
227
+ continue
177
228
+ return rule
178
229
+
179
- + def resolve_patch(self, patch_path):
180
- + return self.metadata_path.parent / patch_path
230
+ + @abc.abstractmethod
231
+ + def resolve_patch(self, patch_name: str):
232
+ + pass
233
+ +
234
+ +
235
+ + class EmptyRepository(AbstractPatchRepository):
236
+ + def __init__(self):
237
+ + super().__init__({})
238
+ +
239
+ + def resolve_patch(self, patch_name: str):
240
+ + raise AssertionError("Invalid call")
241
+ +
242
+ +
243
+ + class LocalPatchRepository(AbstractPatchRepository):
244
+ + def __init__(self, patches_path: Path, repository_data: dict):
245
+ + super().__init__(repository_data)
246
+ + self.patches_path = patches_path
247
+ + logger.debug("Loaded GraalPy patch repository from %s", patches_path)
248
+ +
249
+ + @classmethod
250
+ + def from_path(cls, patches_path: Path):
251
+ + try:
252
+ + with open(patches_path / METADATA_FILENAME) as f:
253
+ + metadata_content = f.read()
254
+ + except FileNotFoundError:
255
+ + raise RepositoryNotFoundException(f"'{METADATA_FILENAME}' doesn't exist")
256
+ + except OSError as e:
257
+ + raise RepositoryException(f"'{METADATA_FILENAME}' cannot be read: {e}")
258
+ + return cls(patches_path, cls.metadata_from_string(metadata_content))
259
+ +
260
+ + @contextmanager
261
+ + def resolve_patch(self, patch_name: str):
262
+ + yield self.patches_path / patch_name
263
+ +
264
+ +
265
+ + class RemotePatchRepository(AbstractPatchRepository):
266
+ + def __init__(self, patches_url: str, repository_data: dict):
267
+ + super().__init__(repository_data)
268
+ + self.patches_url = patches_url
269
+ + logger.debug("Loaded GraalPy patch repository from %s", patches_url)
270
+ +
271
+ + @staticmethod
272
+ + def get_session():
273
+ + from pip._internal.cli.req_command import _GRAALPY_SESSION
274
+ + return _GRAALPY_SESSION or requests.Session()
275
+ +
276
+ + @classmethod
277
+ + def from_url(cls, patches_url: str):
278
+ + try:
279
+ + url = url_for_file(patches_url, METADATA_FILENAME)
280
+ + response = cls.get_session().get(url)
281
+ + if response.status_code == 404:
282
+ + raise RepositoryNotFoundException(f"'{METADATA_FILENAME} not found at url: {url}")
283
+ + response.raise_for_status()
284
+ + metadata_content = response.content.decode('utf-8')
285
+ + except RepositoryNotFoundException:
286
+ + raise
287
+ + except Exception as e:
288
+ + raise RepositoryException(f"'{METADATA_FILENAME} cannot be retrieved': {e}")
289
+ + return cls(patches_url, cls.metadata_from_string(metadata_content))
290
+ +
291
+ + @contextmanager
292
+ + def resolve_patch(self, patch_name: str):
293
+ + try:
294
+ + response = self.get_session().get(url_for_file(self.patches_url, patch_name))
295
+ + response.raise_for_status()
296
+ + except requests.RequestException as e:
297
+ + logger.warning("Failed to download GraalPy patch '%s': %s", patch_name, e)
298
+ + yield None
299
+ + else:
300
+ + with tempfile.TemporaryDirectory() as tempdir:
301
+ + patch_file = Path(tempdir) / patch_name
302
+ + with open(patch_file, 'wb') as f:
303
+ + f.write(response.content)
304
+ + yield patch_file
181
305
+
182
306
+
183
307
+ __PATCH_REPOSITORY = None
184
308
+
185
309
+
310
+ + def repository_from_url_or_path(url_or_path):
311
+ + if '://' not in url_or_path:
312
+ + return LocalPatchRepository.from_path(Path(url_or_path))
313
+ + elif url_or_path.startswith('file:'):
314
+ + patches_path = Path(url_to_path(url_or_path))
315
+ + return LocalPatchRepository.from_path(patches_path)
316
+ + else:
317
+ + patches_url = url_or_path
318
+ + if not patches_url.endswith('/'):
319
+ + patches_url += '/'
320
+ + return RemotePatchRepository.from_url(patches_url)
321
+ +
322
+ +
323
+ + def create_patch_repository(patches_url):
324
+ + if patches_url:
325
+ + if VERSION_PARAMETER in patches_url:
326
+ + if not GRAALPY_VERSION.endswith('-dev'):
327
+ + try_version = GRAALPY_VERSION
328
+ + while '.' in try_version:
329
+ + url = patches_url.replace(VERSION_PARAMETER, try_version)
330
+ + try:
331
+ + return repository_from_url_or_path(url)
332
+ + except RepositoryNotFoundException:
333
+ + logger.debug("No patch repository found for version %s at %s", try_version, url)
334
+ + except RepositoryException as e:
335
+ + logger.warning("Failed to load GraalPy patch repository from %s: %s", url, e)
336
+ + logger.warning("Falling back to internal GraalPy patch repository")
337
+ + break
338
+ + try_version = try_version.rsplit('.', 1)[0]
339
+ + else:
340
+ + logger.debug("Skipping versioned GraalPy patch repository on snapshot build")
341
+ + else:
342
+ + try:
343
+ + return repository_from_url_or_path(patches_url)
344
+ + except RepositoryException as e:
345
+ + logger.warning("Failed to load GraalPy patch repository from %s: %s", patches_url, e)
346
+ + logger.warning("Falling back to internal GraalPy patch repository")
347
+ + try:
348
+ + return LocalPatchRepository.from_path(DEFAULT_PATCHES_PATH)
349
+ + except RepositoryException as e:
350
+ + logger.warning("Failed to load internal GraalPy patch repository: %s", e)
351
+ + return EmptyRepository()
352
+ +
353
+ +
186
354
+ def get_patch_repository():
187
355
+ global __PATCH_REPOSITORY
188
356
+ if not __PATCH_REPOSITORY:
189
- + __PATCH_REPOSITORY = PatchRepository(METADATA_PATH )
357
+ + __PATCH_REPOSITORY = create_patch_repository(PATCHES_URL )
190
358
+ return __PATCH_REPOSITORY
191
359
+
192
360
+
@@ -204,8 +372,8 @@ index 0000000..2a67f4b
204
372
+ name_ver_match = re.match(fr"^(?P<name>.*?)-(?P<version>{VERSION_PATTERN}).*?\.(?P<suffix>tar\.gz|tar|whl|zip)$",
205
373
+ archive_name, re.VERBOSE | re.I)
206
374
+ if not name_ver_match:
207
- + print (f"GraalPy warning: could not parse package name, version, or format from {archive_name!r}.\n"
208
- + "Could not determine if any GraalPy specific patches need to be applied.")
375
+ + logger.warning (f"GraalPy warning: could not parse package name, version, or format from {archive_name!r}.\n"
376
+ + "Could not determine if any GraalPy specific patches need to be applied.")
209
377
+ return
210
378
+
211
379
+ name = name_ver_match.group('name')
@@ -222,7 +390,7 @@ index 0000000..2a67f4b
222
390
+
223
391
+ autopatch_capi.auto_patch_tree(location)
224
392
+
225
- + print (f"Looking for GraalPy patches for {name}")
393
+ + logger.info (f"Looking for GraalPy patches for {name}")
226
394
+ repository = get_patch_repository()
227
395
+
228
396
+ if is_wheel:
@@ -240,21 +408,23 @@ index 0000000..2a67f4b
240
408
+ location = os.path.join(location, subdir)
241
409
+ if rule:
242
410
+ if patch := rule.get('patch'):
243
- + patch = repository.resolve_patch(patch)
244
- + print(f"Patching package {name} using {patch}")
245
- + exe = '.exe' if os.name == 'nt' else ''
246
- + try:
247
- + subprocess.run([f"patch{exe}", "-f", "-d", location, "-p1", "-i", str(patch)], check=True)
248
- + except FileNotFoundError:
249
- + print(
250
- + "WARNING: GraalPy needs the 'patch' utility to apply compatibility patches. Please install it using your system's package manager.")
251
- + except subprocess.CalledProcessError:
252
- + print(f"Applying GraalPy patch failed for {name}. The package may still work.")
411
+ + with repository.resolve_patch(patch) as patch_path:
412
+ + if not patch_path:
413
+ + return
414
+ + logger.info(f"Patching package {name} using {patch}")
415
+ + exe = '.exe' if os.name == 'nt' else ''
416
+ + try:
417
+ + subprocess.run([f"patch{exe}", "-f", "-d", location, "-p1", "-i", str(patch_path)], check=True)
418
+ + except FileNotFoundError:
419
+ + logger.warning(
420
+ + "WARNING: GraalPy needs the 'patch' utility to apply compatibility patches. Please install it using your system's package manager.")
421
+ + except subprocess.CalledProcessError:
422
+ + logger.warning(f"Applying GraalPy patch failed for {name}. The package may still work.")
253
423
+ elif version_specs := repository.get_suggested_version_specs(name):
254
- + print ("We have patches to make this package work on GraalVM for some version(s).")
255
- + print ("If installing or running fails, consider using one of the versions that we have patches for:")
424
+ + logger.info ("We have patches to make this package work on GraalVM for some version(s).")
425
+ + logger.info ("If installing or running fails, consider using one of the versions that we have patches for:")
256
426
+ for version_spec in version_specs:
257
- + print (f'{name} {version_spec}')
427
+ + logger.info (f'{name} {version_spec}')
258
428
+
259
429
+
260
430
+ def apply_graalpy_sort_order(sort_key_func):
@@ -294,9 +464,6 @@ index 0000000..2a67f4b
294
464
+ return candidates
295
465
+
296
466
+
297
- + MARKER_NAME = 'GRAALPY_MARKER'
298
- +
299
- +
300
467
+ def mark_wheel(path):
301
468
+ if DISABLE_PATCHING:
302
469
+ return
@@ -307,14 +474,14 @@ index 0000000..2a67f4b
307
474
+ dist_info = m.group(1)
308
475
+ break
309
476
+ assert dist_info, "Cannot find .dist_info in built wheel"
310
- + marker = f'{dist_info}/{MARKER_NAME }'
477
+ + marker = f'{dist_info}/{MARKER_FILE_NAME }'
311
478
+ with z.open(marker, 'w'):
312
479
+ pass
313
480
+
314
481
+
315
482
+ def is_wheel_marked(path):
316
483
+ with zipfile.ZipFile(path) as z:
317
- + return any(re.match(rf'[^/]+.dist-info/{MARKER_NAME }$', f) for f in z.namelist())
484
+ + return any(re.match(rf'[^/]+.dist-info/{MARKER_FILE_NAME }$', f) for f in z.namelist())
318
485
diff --git a/pip/_internal/utils/unpacking.py b/pip/_internal/utils/unpacking.py
319
486
index 78b5c13..18a184c 100644
320
487
--- a/pip/_internal/utils/unpacking.py
0 commit comments