Skip to content

Commit 220c220

Browse files
committed
[GR-58970] Add ability to ship new patches for a released version
PullRequest: graalpython/3517
2 parents 067f7ad + 084b57d commit 220c220

File tree

219 files changed

+1206
-896
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

219 files changed

+1206
-896
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ language runtime. The main focus is on user-observable behavior of the engine.
1414
* See [the documentation](https://github.com/oracle/graalpython/blob/master/docs/user/Interoperability.md#interacting-with-foreign-objects-from-python-scripts) for more information.
1515
* Remove support for running with Sulong managed both in embeddings as well as through the `graalpy-managed` launcher.
1616
* Rewrite wheelbuilder to be easier to use and contribute to. This version is now the same we run internally to build publishable wheels for some platforms we support, so the community can build the same wheels on their own hardware easily if desired.
17+
* `pip` is now able to fetch newer versions of GraalPy patches for third-party packages from `graalpython` GitHub repository, allowing us to add new patches to released versions.
18+
* The patch repository can be overridden using `PIP_GRAALPY_PATCHES_URL` environment variable, which can point to a local path or a URL. It can be disabled by setting it to an empty string.
1719

1820
## Version 24.1.0
1921
* GraalPy is now considered stable for pure Python workloads. While many workloads involving native extension modules work, we continue to consider them experimental. You can use the command-line option `--python.WarnExperimentalFeatures` to enable warnings for such modules at runtime. In Java embeddings the warnings are enabled by default and you can suppress them by setting the context option 'python.WarnExperimentalFeatures' to 'false'.

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

Lines changed: 91 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2023, 2023, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2023, 2024, Oracle and/or its affiliates. All rights reserved.
22
# DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
33
#
44
# The Universal Permissive License (UPL), Version 1.0
@@ -42,8 +42,12 @@
4242
import subprocess
4343
import sys
4444
import tempfile
45+
import threading
4546
import unittest
47+
from http.server import HTTPServer, SimpleHTTPRequestHandler
4648
from pathlib import Path
49+
from urllib.parse import urljoin
50+
from urllib.request import pathname2url
4751

4852
SETUP_PY_TEMPLATE = '''
4953
from setuptools import setup, find_packages
@@ -97,7 +101,7 @@ def setUp(self):
97101
shutil.copytree(self.venv_template_dir, self.venv_dir, symlinks=True)
98102
self.patch_dir = Path(tempfile.mkdtemp()).resolve()
99103
self.pip_env = os.environ.copy()
100-
self.pip_env['PIPLOADER_PATCHES_BASE_DIRS'] = str(self.patch_dir)
104+
self.pip_env['PIP_GRAALPY_PATCHES_URL'] = str(self.patch_dir)
101105
self.index_dir = Path(tempfile.mkdtemp()).resolve()
102106

103107
def tearDown(self):
@@ -106,18 +110,16 @@ def tearDown(self):
106110
shutil.rmtree(self.index_dir, ignore_errors=True)
107111

108112
def prepare_config(self, name, rules):
109-
package_dir = self.patch_dir / name
110-
package_dir.mkdir(exist_ok=True)
111113
toml_lines = []
112114
for rule in rules:
113-
toml_lines.append('[[rules]]')
115+
toml_lines.append(f'[[{name}.rules]]')
114116
for k, v in rule.items():
115117
if not k.startswith('$'):
116118
toml_lines.append(f'{k} = {v!r}')
117119
if patch := rule.get('patch'):
118-
with open(package_dir / patch, 'w') as f:
120+
with open(self.patch_dir / patch, 'w') as f:
119121
f.write(PATCH_TEMPLATE.format(rule.get('$patch-text', 'Patched')))
120-
with open(package_dir / 'metadata.toml', 'w') as f:
122+
with open(self.patch_dir / 'metadata.toml', 'w') as f:
121123
f.write('\n'.join(toml_lines))
122124

123125
def build_package(self, name, version):
@@ -147,27 +149,37 @@ def add_package_to_index(self, name, version, dist_type):
147149
package = self.build_package(name, version)[dist_type]
148150
shutil.copy(package, self.index_dir)
149151

150-
def run_venv_pip_install(self, package, extra_env=None):
152+
def run_venv_pip_install(self, package, extra_env=None, assert_stderr_matches=None):
151153
env = self.pip_env.copy()
152154
if extra_env:
153155
env.update(extra_env)
154-
out = subprocess.check_output([
155-
self.venv_python,
156-
'--experimental-options', '--python.EnableDebuggingBuiltins=true',
157-
'-m', 'pip', 'install', '--force-reinstall',
158-
'--find-links', self.index_dir, '--no-index', '--no-cache-dir',
159-
package],
160-
env=env, universal_newlines=True)
161-
assert 'Applying GraalPy patch failed for' not in out
162-
return re.findall(r'Successfully installed (\S+)', out)
156+
proc = subprocess.run(
157+
[
158+
str(self.venv_dir / 'bin' / 'pip'),
159+
'--isolated',
160+
'install',
161+
'--force-reinstall',
162+
'--find-links', self.index_dir,
163+
'--no-index',
164+
'--no-cache-dir',
165+
package,
166+
],
167+
check=True,
168+
capture_output=True,
169+
env=env,
170+
universal_newlines=True,
171+
)
172+
print(proc.stderr)
173+
assert 'Applying GraalPy patch failed for' not in proc.stderr
174+
if assert_stderr_matches:
175+
assert re.search(assert_stderr_matches, proc.stderr), \
176+
f"Didn't match expected stderr.\nExpected (regex): {assert_stderr_matches}\nActual:{proc.stderr}"
177+
return re.findall(r'Successfully installed (\S+)', proc.stdout)
163178

164179
def run_test_fun(self):
165180
code = "import patched_package; print(patched_package.test_fun())"
166181
return subprocess.check_output([self.venv_python, '-c', code], universal_newlines=True).strip()
167182

168-
def test_pip_launcher(self):
169-
subprocess.check_output([str(self.venv_dir / 'bin' / 'pip'), 'install', '--help'])
170-
171183
def test_wheel_unpatched_version(self):
172184
self.add_package_to_index('foo', '1.0.0', 'wheel')
173185
self.prepare_config('foo', [{
@@ -388,3 +400,62 @@ def test_name_with_dashes(self):
388400
}])
389401
assert self.run_venv_pip_install('package-with-dashes') == ['package-with-dashes-1.0.0']
390402
assert self.run_test_fun() == "Patched"
403+
404+
def check_installing_with_patch_repo(self, url_or_path: str, *, graalpy_version=None, should_be_skipped=False,
405+
assert_stderr_matches=None):
406+
self.pip_env['PIP_GRAALPY_PATCHES_URL'] = url_or_path
407+
if graalpy_version:
408+
self.pip_env['TEST_PIP_GRAALPY_VERSION'] = graalpy_version
409+
self.add_package_to_index('foo', '1.1.0', 'wheel')
410+
self.prepare_config('foo', [{'patch': 'foo.patch'}])
411+
assert self.run_venv_pip_install('foo', assert_stderr_matches=assert_stderr_matches) == ['foo-1.1.0']
412+
assert self.run_test_fun() == ("Unpatched" if should_be_skipped else "Patched")
413+
414+
def test_broken_patches_path(self):
415+
self.check_installing_with_patch_repo(
416+
'/tmp/not-there',
417+
should_be_skipped=True,
418+
assert_stderr_matches="WARNING: Failed to load GraalPy patch repository",
419+
)
420+
421+
def test_patches_file_url(self):
422+
self.check_installing_with_patch_repo(urljoin('file:', pathname2url(str(self.patch_dir.absolute()))))
423+
424+
@unittest.skipIf(
425+
__graalpython__.posix_module_backend() == 'java',
426+
"Server doesn't work properly under Java posix backend"
427+
)
428+
def test_patches_http_url(self):
429+
patch_dir = self.patch_dir
430+
431+
class Handler(SimpleHTTPRequestHandler):
432+
def __init__(self, *args, **kwargs):
433+
super().__init__(*args, directory=str(patch_dir), **kwargs)
434+
435+
try:
436+
with HTTPServer(('localhost', 0), Handler) as server:
437+
thread = threading.Thread(target=server.serve_forever)
438+
thread.start()
439+
try:
440+
self.check_installing_with_patch_repo(f'http://localhost:{server.server_port}')
441+
finally:
442+
server.shutdown()
443+
finally:
444+
thread.join()
445+
446+
def test_patches_repo_version_resolution(self):
447+
patch_dir_parent = self.patch_dir
448+
graalpy_version = '1.3.2'
449+
self.patch_dir = patch_dir_parent / graalpy_version
450+
self.patch_dir.mkdir()
451+
self.check_installing_with_patch_repo(str(patch_dir_parent / '<version>'), graalpy_version=graalpy_version)
452+
453+
def test_patches_repo_version_resolution_dev(self):
454+
patch_dir_parent = self.patch_dir
455+
self.patch_dir = patch_dir_parent / '1.3.2-dev'
456+
self.patch_dir.mkdir()
457+
self.check_installing_with_patch_repo(
458+
str(patch_dir_parent / '<version>'),
459+
graalpy_version='1.3.2-dev',
460+
should_be_skipped=True,
461+
)

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/PythonLanguage.java

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -183,20 +183,21 @@ public final class PythonLanguage extends TruffleLanguage<PythonContext> {
183183
public static final int MICRO = 7;
184184
public static final int RELEASE_LEVEL_ALPHA = 0xA;
185185
public static final int RELEASE_LEVEL_BETA = 0xB;
186-
public static final int RELEASE_LEVEL_GAMMA = 0xC;
186+
public static final int RELEASE_LEVEL_CANDIDATE = 0xC;
187187
public static final int RELEASE_LEVEL_FINAL = 0xF;
188-
public static final int RELEASE_LEVEL = RELEASE_LEVEL_ALPHA;
188+
public static final int RELEASE_LEVEL;
189189
public static final TruffleString RELEASE_LEVEL_STRING;
190190
public static final String FROZEN_FILENAME_PREFIX = "<frozen ";
191191
public static final String FROZEN_FILENAME_SUFFIX = ">";
192192

193193
/**
194194
* GraalVM version. Unfortunately, we cannot just use {@link Version#getCurrent} as it relies on
195-
* a GraalVM build, but we may run from Jar files directly during development. We generate the
196-
* version during the build that are checked against these constants.
195+
* a GraalVM build, but we may run outside GraalVM. We generate the version during the build
196+
* that are checked against these constants.
197197
*/
198198
public static final int GRAALVM_MAJOR;
199199
public static final int GRAALVM_MINOR;
200+
public static final int GRAALVM_MICRO;
200201
public static final String DEV_TAG;
201202

202203
/**
@@ -207,21 +208,6 @@ public final class PythonLanguage extends TruffleLanguage<PythonContext> {
207208
private static final int VERSION_BASE = '!';
208209

209210
static {
210-
switch (RELEASE_LEVEL) {
211-
case RELEASE_LEVEL_ALPHA:
212-
RELEASE_LEVEL_STRING = tsLiteral("alpha");
213-
break;
214-
case RELEASE_LEVEL_BETA:
215-
RELEASE_LEVEL_STRING = tsLiteral("beta");
216-
break;
217-
case RELEASE_LEVEL_GAMMA:
218-
RELEASE_LEVEL_STRING = tsLiteral("rc");
219-
break;
220-
case RELEASE_LEVEL_FINAL:
221-
default:
222-
RELEASE_LEVEL_STRING = tsLiteral("final");
223-
}
224-
225211
// The resource file is built by mx from "graalpy-versions" project using mx substitutions.
226212
// The actual values of the versions are computed by mx helper functions py_version_short,
227213
// graal_version_short, and dev_tag defined in mx_graalpython.py
@@ -238,7 +224,22 @@ public final class PythonLanguage extends TruffleLanguage<PythonContext> {
238224
}
239225
GRAALVM_MAJOR = is.read() - VERSION_BASE;
240226
GRAALVM_MINOR = is.read() - VERSION_BASE;
241-
is.read(); // skip GraalVM micro version
227+
GRAALVM_MICRO = is.read() - VERSION_BASE;
228+
RELEASE_LEVEL = is.read() - VERSION_BASE;
229+
switch (RELEASE_LEVEL) {
230+
case RELEASE_LEVEL_ALPHA:
231+
RELEASE_LEVEL_STRING = tsLiteral("alpha");
232+
break;
233+
case RELEASE_LEVEL_BETA:
234+
RELEASE_LEVEL_STRING = tsLiteral("beta");
235+
break;
236+
case RELEASE_LEVEL_CANDIDATE:
237+
RELEASE_LEVEL_STRING = tsLiteral("candidate");
238+
break;
239+
case RELEASE_LEVEL_FINAL:
240+
default:
241+
RELEASE_LEVEL_STRING = tsLiteral("final");
242+
}
242243
// see mx.graalpython/mx_graalpython.py:dev_tag
243244
byte[] rev = new byte[3 /* 'dev' */ + 10 /* revision */];
244245
if (is.read(rev) == rev.length) {
@@ -254,7 +255,7 @@ public final class PythonLanguage extends TruffleLanguage<PythonContext> {
254255
public static final int VERSION_HEX = MAJOR << 24 |
255256
MINOR << 16 |
256257
MICRO << 8 |
257-
RELEASE_LEVEL_ALPHA << 4 |
258+
RELEASE_LEVEL << 4 |
258259
RELEASE_SERIAL;
259260
public static final String VERSION = MAJOR + "." + MINOR + "." + MICRO;
260261
// Rarely updated version of the C API, we should take it from the imported CPython version

graalpython/com.oracle.graal.python/src/com/oracle/graal/python/builtins/modules/GraalPythonModuleBuiltins.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@
4040
*/
4141
package com.oracle.graal.python.builtins.modules;
4242

43+
import static com.oracle.graal.python.PythonLanguage.GRAALVM_MAJOR;
44+
import static com.oracle.graal.python.PythonLanguage.GRAALVM_MICRO;
45+
import static com.oracle.graal.python.PythonLanguage.GRAALVM_MINOR;
4346
import static com.oracle.graal.python.PythonLanguage.J_GRAALPYTHON_ID;
47+
import static com.oracle.graal.python.PythonLanguage.RELEASE_LEVEL;
48+
import static com.oracle.graal.python.PythonLanguage.RELEASE_LEVEL_FINAL;
4449
import static com.oracle.graal.python.nodes.BuiltinNames.J_EXTEND;
4550
import static com.oracle.graal.python.nodes.BuiltinNames.J___GRAALPYTHON__;
4651
import static com.oracle.graal.python.nodes.BuiltinNames.T_SHA3;
@@ -78,11 +83,6 @@
7883
import java.util.List;
7984
import java.util.logging.Level;
8085

81-
import com.oracle.truffle.api.interop.InvalidArrayIndexException;
82-
import com.oracle.truffle.api.interop.TruffleObject;
83-
import com.oracle.truffle.api.library.ExportLibrary;
84-
import com.oracle.truffle.api.library.ExportMessage;
85-
import org.graalvm.home.Version;
8686
import org.graalvm.nativeimage.ImageInfo;
8787

8888
import com.oracle.graal.python.PythonLanguage;
@@ -185,9 +185,13 @@
185185
import com.oracle.truffle.api.dsl.TypeSystemReference;
186186
import com.oracle.truffle.api.frame.VirtualFrame;
187187
import com.oracle.truffle.api.interop.InteropLibrary;
188+
import com.oracle.truffle.api.interop.InvalidArrayIndexException;
189+
import com.oracle.truffle.api.interop.TruffleObject;
188190
import com.oracle.truffle.api.interop.UnknownIdentifierException;
189191
import com.oracle.truffle.api.interop.UnsupportedMessageException;
190192
import com.oracle.truffle.api.library.CachedLibrary;
193+
import com.oracle.truffle.api.library.ExportLibrary;
194+
import com.oracle.truffle.api.library.ExportMessage;
191195
import com.oracle.truffle.api.nodes.LanguageInfo;
192196
import com.oracle.truffle.api.nodes.Node;
193197
import com.oracle.truffle.api.nodes.NodeUtil;
@@ -1056,11 +1060,18 @@ static PythonClass createType(VirtualFrame frame, TruffleString name, PTuple bas
10561060
@Builtin(name = "get_graalvm_version", minNumOfPositionalArgs = 0)
10571061
@GenerateNodeFactory
10581062
abstract static class GetGraalVmVersion extends PythonBuiltinNode {
1059-
@TruffleBoundary
1063+
private static final TruffleString VERSION_STRING;
1064+
static {
1065+
String version = String.format("%d.%d.%d", GRAALVM_MAJOR, GRAALVM_MINOR, GRAALVM_MICRO);
1066+
if (RELEASE_LEVEL != RELEASE_LEVEL_FINAL) {
1067+
version += "-dev";
1068+
}
1069+
VERSION_STRING = TruffleString.fromJavaStringUncached(version, TS_ENCODING);
1070+
}
1071+
10601072
@Specialization
10611073
TruffleString get() {
1062-
Version current = Version.getCurrent();
1063-
return TruffleString.fromJavaStringUncached(current.toString(), TS_ENCODING);
1074+
return VERSION_STRING;
10641075
}
10651076
}
10661077

graalpython/lib-graalpython/patches/Cython/metadata.toml

Lines changed: 0 additions & 14 deletions
This file was deleted.

graalpython/lib-graalpython/patches/MarkupSafe/metadata.toml

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)