Skip to content

Commit 4a6507f

Browse files
committed
[GR-44839] More flexible patching
PullRequest: graalpython/2683
2 parents a5e448e + a7ab5c0 commit 4a6507f

File tree

16 files changed

+524
-143
lines changed

16 files changed

+524
-143
lines changed

graalpy_virtualenv/graalpy_virtualenv/graalpy.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,15 @@ def run(self, creator):
180180
@contextmanager
181181
def get_pip_install_cmd(self, exe, for_py_version):
182182
cmd = [str(exe), "-m", "pip", "-q", "install", "--only-binary", ":all:", "--disable-pip-version-check"]
183-
if not self.download:
184-
cmd.append("--no-index")
185183
ensurepip_path = get_ensurepip_path(exe)
186184
for dist, version in self.distribution_to_versions().items():
187-
if ensurepip_path and version == 'bundle' and dist in ('pip', 'setuptools'):
188-
wheel = from_dir(dist, None, for_py_version, [ensurepip_path])
185+
if ensurepip_path and version == 'bundle':
186+
if dist in ('pip', 'setuptools'):
187+
wheel = from_dir(dist, None, for_py_version, [ensurepip_path])
188+
else:
189+
# For wheel, install just `wheel` our patching logic should pick the right version
190+
cmd.append(dist)
191+
continue
189192
else:
190193
wheel = get_wheel(
191194
distribution=dist,

graalpython/com.oracle.graal.python.test/src/tests/test_patched_pip.py

Lines changed: 335 additions & 27 deletions
Large diffs are not rendered by default.

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/runtime/PythonContext.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2067,13 +2067,6 @@ protected void perform(ThreadLocalAction.Access access) {
20672067
});
20682068
}
20692069
if (isOurThread) {
2070-
// Thread#stop is not supported on SVM
2071-
if (!ImageInfo.inImageCode()) {
2072-
if (thread.isAlive()) {
2073-
LOGGER.warning("could not join thread " + thread.getName() + ". Trying to stop it.");
2074-
}
2075-
thread.stop();
2076-
}
20772070
if (thread.isAlive()) {
20782071
LOGGER.warning("Could not stop thread " + thread.getName());
20792072
}

graalpython/lib-graalpython/modules/ginstall.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ def pkgconfig(**kwargs):
259259

260260
@pip_package()
261261
def wheel(**kwargs):
262-
install_from_pypi("wheel==0.33.4", **kwargs)
262+
install_from_pypi("wheel==0.38.*", **kwargs)
263263

264264
@pip_package()
265265
def protobuf(**kwargs):
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
From b1c948a07cfe2ffbb5e0954f0ee87dddd78e63ab Mon Sep 17 00:00:00 2001
2+
From: Michael Simacek <[email protected]>
3+
Date: Tue, 18 Jan 2022 15:54:16 +0100
4+
Subject: [PATCH] Adapt for GraalPython
5+
6+
---
7+
setup.py | 2 +-
8+
1 file changed, 1 insertion(+), 1 deletion(-)
9+
10+
diff --git a/setup.py b/setup.py
11+
index c6ee5bf..b398be9 100644
12+
--- a/setup.py
13+
+++ b/setup.py
14+
@@ -52,7 +52,7 @@ def show_message(*lines):
15+
print("=" * 74)
16+
17+
18+
-supports_speedups = platform.python_implementation() not in {"PyPy", "Jython"}
19+
+supports_speedups = platform.python_implementation() not in {"PyPy", "Jython", "GraalVM"}
20+
21+
if os.environ.get("CIBUILDWHEEL", "0") == "1" and supports_speedups:
22+
run_setup(True)
23+
--
24+
2.31.1
25+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Patch already upstreamed, keeping it around for older versions
2+
[[rules]]
3+
version = "< 2.1.0"
4+
patch = "MarkupSafe.patch"
5+
install-priority = 0 # Don't make pip install prefer this version
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
This directory contains patches applied by pip when installing packages. There is a directory for each package that
2+
contains patches and optionally a configuration file that can specify rules for matching patches to package versions and
3+
can also influence pip version selection mechanism.
4+
5+
Configuration files are named `metadata.toml` and can contain the following:
6+
7+
```toml
8+
# The file defines an array of tables (dicts) named `patches`. The patch selection process iterates it and picks the
9+
# first patch one that matches in version and dist type.
10+
# The next entry will apply to a wheel foo-1.0.0
11+
[[rules]]
12+
# Optional. Relative path to a patch file. May be omitted when the entry just specifies `install-priority`
13+
patch = 'foo-1.0.0.patch'
14+
# Optional. Version specifier according to https://peps.python.org/pep-0440/#version-specifiers. If omitted, it will
15+
# match any version
16+
version = '== 1.0.0'
17+
# Optional. Type of distribution artifact. One of `wheel` or `sdist`. Omit unless you want to have separate patches for
18+
# wheels and sdists.
19+
dist-type = 'wheel'
20+
# Optional. When applying a patch for a sdist that was created against a wheel, there can be a mismatch in the paths,
21+
# when the wheel was built from a subdirectory. When applying a patch on a sdist, this option will cause the patch
22+
# process to be run from given subdirectory. Has no effect when applying patches on wheels.
23+
subdir = 'src'
24+
# Optional. Can specify preference for or against this version when selecting which version to install. Defaults to 1.
25+
# When ordering all available versions in the index, each version gets a priority of the first entry it matches in this
26+
# file. If it doesn't match, it gets priority 0. Versions with higher priority are then prefered for installation. This
27+
# means that by default, versions with patches are prefered. Set the priority to 0 if you want the version not to be
28+
# prefered, for example when keeping an old patch that was accepted upstream in a newer version. Set the version to
29+
# a number greater than 1 if you want given version to be preferred to other entries. Additionally, if you set the
30+
# priority to 0, the version will not be shown in the suggestion list we display when we didn't find an applicable patch
31+
install-priority = 1
32+
33+
# The next entry will apply to all other artifacts of foo
34+
[[patches]]
35+
patch = 'foo.patch'
36+
```

graalpython/lib-graalpython/patches/pip/whl/pip-22.2.2.patch

Lines changed: 100 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,18 @@ index 1af8978..15fee7f 100644
5858
# file in .data maps to same location as file in wheel root).
5959
diff --git a/pip/_internal/utils/graalpy.py b/pip/_internal/utils/graalpy.py
6060
new 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
243253
diff --git a/pip/_internal/utils/unpacking.py b/pip/_internal/utils/unpacking.py
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[[rules]]
2+
# This version doesn't need a patch, but we want to pin it. Our virtualenv seeder pins setuptools and pip to the bundled
3+
# ones, so it makes sense to always pin wheel too to avoid it getting out of sync with setuptools.
4+
# TODO we should make 0.40 work
5+
version = '== 0.38.*'
6+
7+
[[rules]]
8+
version = '< 0.35'
9+
patch = 'wheel-pre-0.35.patch'
10+
subdir = 'src'

0 commit comments

Comments
 (0)