Skip to content

Commit 477d275

Browse files
committed
Implement fetching patches from github
1 parent 5cdcc5f commit 477d275

File tree

1 file changed

+212
-45
lines changed

1 file changed

+212
-45
lines changed

graalpython/lib-graalpython/patches/pip-23.2.1.patch

Lines changed: 212 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,31 @@ index 02ba608..85c7c22 100644
3232
help="Don't periodically check PyPI to determine whether a new version "
3333
"of pip is available for download. Implied with --no-index.",
3434
)
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(
3560
diff --git a/pip/_internal/index/package_finder.py b/pip/_internal/index/package_finder.py
3661
index b6f8d57..6c37e0b 100644
3762
--- a/pip/_internal/index/package_finder.py
@@ -102,50 +127,76 @@ index a8cd133..20dd1e6 100644
102127
# file in .data maps to same location as file in wheel root).
103128
diff --git a/pip/_internal/utils/graalpy.py b/pip/_internal/utils/graalpy.py
104129
new file mode 100644
105-
index 0000000..2a67f4b
130+
index 0000000..375f66e
106131
--- /dev/null
107132
+++ 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
111136
+import os
112137
+import re
138+
+import tempfile
113139
+import zipfile
140+
+from contextlib import contextmanager
114141
+from pathlib import Path
115-
+from urllib.parse import urlparse
142+
+from tomllib import TOMLDecodeError
143+
+from urllib.parse import urlparse, urljoin, urlunparse
116144
+
117145
+from pip._internal.models.candidate import InstallationCandidate
118146
+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
120149
+from pip._vendor.packaging.specifiers import SpecifierSet
150+
+from pip._vendor.packaging.utils import canonicalize_name
121151
+from pip._vendor.packaging.version import VERSION_PATTERN
122152
+
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)
127160
+DISABLE_PATCHING = os.environ.get('PIP_GRAALPY_DISABLE_PATCHING', '').lower() in ('true', '1')
128161
+DISABLE_VERSION_SELECTION = os.environ.get('PIP_GRAALPY_DISABLE_VERSION_SELECTION', '').lower() in ('true', '1')
129162
+
163+
+GRAALPY_VERSION = os.environ.get('TEST_PIP_GRAALPY_VERSION', __graalpython__.get_graalvm_version())
164+
+
165+
+logger = logging.getLogger(__name__)
166+
+
130167
+
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))
133172
+
134173
+
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}")
142193
+
143194
+ def get_rules(self, name):
144-
+ if metadata := self._repository.get(normalize_name(name)):
195+
+ if metadata := self._repository.get(canonicalize_name(name)):
145196
+ return metadata.get('rules')
146197
+
147198
+ def get_add_sources(self, name):
148-
+ if metadata := self._repository.get(normalize_name(name)):
199+
+ if metadata := self._repository.get(canonicalize_name(name)):
149200
+ return metadata.get('add-sources')
150201
+
151202
+ def get_priority_for_version(self, name, version):
@@ -176,17 +227,134 @@ index 0000000..2a67f4b
176227
+ continue
177228
+ return rule
178229
+
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
181305
+
182306
+
183307
+__PATCH_REPOSITORY = None
184308
+
185309
+
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+
+
186354
+def get_patch_repository():
187355
+ global __PATCH_REPOSITORY
188356
+ if not __PATCH_REPOSITORY:
189-
+ __PATCH_REPOSITORY = PatchRepository(METADATA_PATH)
357+
+ __PATCH_REPOSITORY = create_patch_repository(PATCHES_URL)
190358
+ return __PATCH_REPOSITORY
191359
+
192360
+
@@ -204,8 +372,8 @@ index 0000000..2a67f4b
204372
+ name_ver_match = re.match(fr"^(?P<name>.*?)-(?P<version>{VERSION_PATTERN}).*?\.(?P<suffix>tar\.gz|tar|whl|zip)$",
205373
+ archive_name, re.VERBOSE | re.I)
206374
+ 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.")
209377
+ return
210378
+
211379
+ name = name_ver_match.group('name')
@@ -222,7 +390,7 @@ index 0000000..2a67f4b
222390
+
223391
+ autopatch_capi.auto_patch_tree(location)
224392
+
225-
+ print(f"Looking for GraalPy patches for {name}")
393+
+ logger.info(f"Looking for GraalPy patches for {name}")
226394
+ repository = get_patch_repository()
227395
+
228396
+ if is_wheel:
@@ -240,21 +408,23 @@ index 0000000..2a67f4b
240408
+ location = os.path.join(location, subdir)
241409
+ if rule:
242410
+ 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.")
253423
+ 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:")
256426
+ for version_spec in version_specs:
257-
+ print(f'{name} {version_spec}')
427+
+ logger.info(f'{name} {version_spec}')
258428
+
259429
+
260430
+def apply_graalpy_sort_order(sort_key_func):
@@ -294,9 +464,6 @@ index 0000000..2a67f4b
294464
+ return candidates
295465
+
296466
+
297-
+MARKER_NAME = 'GRAALPY_MARKER'
298-
+
299-
+
300467
+def mark_wheel(path):
301468
+ if DISABLE_PATCHING:
302469
+ return
@@ -307,14 +474,14 @@ index 0000000..2a67f4b
307474
+ dist_info = m.group(1)
308475
+ break
309476
+ 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}'
311478
+ with z.open(marker, 'w'):
312479
+ pass
313480
+
314481
+
315482
+def is_wheel_marked(path):
316483
+ 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())
318485
diff --git a/pip/_internal/utils/unpacking.py b/pip/_internal/utils/unpacking.py
319486
index 78b5c13..18a184c 100644
320487
--- a/pip/_internal/utils/unpacking.py

0 commit comments

Comments
 (0)