From 9694e3bba97b54f19a627b18ee6a99fef2116aab Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Sun, 10 May 2026 17:10:44 -0700 Subject: [PATCH 1/2] Update python 3.14 to 3.14.5 - Also adds dry run flag 'relenv versions --check' --- relenv/python-versions.json | 22 +++++- relenv/pyversions.py | 126 +++++++++++++++++++++---------- tests/test_pyversions_runtime.py | 23 ++++++ 3 files changed, 130 insertions(+), 41 deletions(-) diff --git a/relenv/python-versions.json b/relenv/python-versions.json index 7ceb98b9..7c859bf9 100644 --- a/relenv/python-versions.json +++ b/relenv/python-versions.json @@ -194,7 +194,8 @@ "3.14.3": "83eed62ba54742382542474db798717e6ee6b3f2", "3.14.2": "b21c499c9e0250c1bfabc29a08c160018d2f6f57", "3.14.1": "da8bd5ae7a346b80db64bac2dc2c9d9da3ca6eac", - "3.14.0": "8a1ae36a2c4212637401af93c8a7856d126156e3" + "3.14.0": "8a1ae36a2c4212637401af93c8a7856d126156e3", + "3.14.5": "550bd85f05ba3a75d710e716100db174f13601f6" }, "dependencies": { "perl": { @@ -290,6 +291,16 @@ "darwin", "win32" ] + }, + "3.53.1.0": { + "url": "https://sqlite.org/2026/sqlite-autoconf-{version}.tar.gz", + "sha256": "83e6b2020a034e9a7ad4a72feea59e1ad52f162e09cbd26735a3ffb98359fc4f", + "sqliteversion": "3530100", + "platforms": [ + "linux", + "darwin", + "win32" + ] } }, "xz": { @@ -499,6 +510,15 @@ "darwin", "win32" ] + }, + "2.8.1": { + "url": "https://github.com/libexpat/libexpat/releases/download/R_2_8_1/expat-{version}.tar.xz", + "sha256": "10b195ee78160a908388180a8fe3603d4e9a12f4755fbf5f3816b23a9d750da0", + "platforms": [ + "linux", + "darwin", + "win32" + ] } } } diff --git a/relenv/pyversions.py b/relenv/pyversions.py index 41e0d21f..51c9632b 100644 --- a/relenv/pyversions.py +++ b/relenv/pyversions.py @@ -995,16 +995,23 @@ def update_dependency_versions(path: pathlib.Path, deps_to_update: list[str] | N print(f"Updated {path}") -def create_pyversions(path: pathlib.Path) -> None: +def detect_python_versions() -> list[Version]: """ - Create python-versions.json file. + Detect available Python versions from python.org. """ url = "https://www.python.org/downloads/" content = fetch_url_content(url) matched = re.findall(r'Python.*', content) - cwd = os.getcwd() parsed_versions = sorted([_ref_version(_) for _ in matched], reverse=True) - versions = [_ for _ in parsed_versions if _.major >= 3] + return [_ for _ in parsed_versions if _.major >= 3] + + +def create_pyversions(path: pathlib.Path) -> None: + """ + Create python-versions.json file. + """ + versions = detect_python_versions() + cwd = os.getcwd() if path.exists(): all_data = json.loads(path.read_text()) @@ -1169,6 +1176,13 @@ def setup_parser( action="store_true", help="List versions", ) + subparser.add_argument( + "-c", + "--check", + default=False, + action="store_true", + help="Check for new python versions", + ) subparser.add_argument( "--version", default="3.14", @@ -1195,6 +1209,62 @@ def main(args: argparse.Namespace) -> None: """ packaged = pathlib.Path(__file__).parent / "python-versions.json" + # Detect terminal capabilities for fancy vs ASCII output + use_unicode = True + if sys.platform == "win32": + # Check if we're in a modern terminal that supports Unicode + import os + + # Windows Terminal and modern PowerShell support Unicode + wt_session = os.environ.get("WT_SESSION") + term_program = os.environ.get("TERM_PROGRAM") + if not wt_session and not term_program: + # Likely cmd.exe or old PowerShell, use ASCII + use_unicode = False + + if use_unicode: + ok_symbol = "✓" + update_symbol = "⚠" + new_symbol = "✗" + arrow = "→" + else: + ok_symbol = "[OK] " + update_symbol = "[UPDATE]" + new_symbol = "[NEW] " + arrow = "->" + + if args.check: + print("Checking for new python versions...\n") + + # Load current versions from JSON + with open(packaged) as f: + data = json.load(f) + + current_py = data.get("python", data) + py_updates = [] + py_up_to_date = [] + + py_detected = detect_python_versions() + for version in py_detected: + vstr = str(version) + if vstr in current_py: + print(f"{ok_symbol} Python {vstr:12} (up-to-date)") + py_up_to_date.append(vstr) + else: + print(f"{new_symbol} Python {vstr:12} (new version available)") + py_updates.append(vstr) + + # Summary + print(f"\n{'=' * 60}") + print(f"Summary: {len(py_up_to_date)} up-to-date, ", end="") + print(f" {len(py_updates)} new versions available") + + if py_updates: + print("\nTo update python versions, run:") + print(" python3 -m relenv versions --update") + + sys.exit(0) + # Handle dependency operations if args.check_deps: print("Checking for new dependency versions...\n") @@ -1204,32 +1274,8 @@ def main(args: argparse.Namespace) -> None: data = json.load(f) current_deps = data.get("dependencies", {}) - updates_available = [] - up_to_date = [] - - # Detect terminal capabilities for fancy vs ASCII output - use_unicode = True - if sys.platform == "win32": - # Check if we're in a modern terminal that supports Unicode - import os - - # Windows Terminal and modern PowerShell support Unicode - wt_session = os.environ.get("WT_SESSION") - term_program = os.environ.get("TERM_PROGRAM") - if not wt_session and not term_program: - # Likely cmd.exe or old PowerShell, use ASCII - use_unicode = False - - if use_unicode: - ok_symbol = "✓" - update_symbol = "⚠" - new_symbol = "✗" - arrow = "→" - else: - ok_symbol = "[OK] " - update_symbol = "[UPDATE]" - new_symbol = "[NEW] " - arrow = "->" + dep_updates = [] + dep_up_to_date = [] # Check each dependency checks = [ @@ -1253,15 +1299,15 @@ def main(args: argparse.Namespace) -> None: ] for dep_key, dep_name, detect_func in checks: - detected = detect_func() - if not detected: + dep_detected = detect_func() + if not dep_detected: continue # Handle SQLite's tuple return if dep_key == "sqlite": - latest_version = detected[0][0] # type: ignore[index] + latest_version = dep_detected[0][0] # type: ignore[index] else: - latest_version = detected[0] # type: ignore[index] + latest_version = dep_detected[0] # type: ignore[index] # Get current version from JSON current_version = None @@ -1273,20 +1319,20 @@ def main(args: argparse.Namespace) -> None: # Compare versions if current_version == latest_version: print(f"{ok_symbol} {dep_name:12} {current_version:15} (up-to-date)") - up_to_date.append(dep_name) + dep_up_to_date.append(dep_name) elif current_version: print(f"{update_symbol} {dep_name:12} {current_version:15} {arrow} {latest_version} (update available)") - updates_available.append((dep_name, current_version, latest_version)) + dep_updates.append((dep_name, current_version, latest_version)) else: print(f"{new_symbol} {dep_name:12} {'(not tracked)':15} {arrow} {latest_version}") - updates_available.append((dep_name, None, latest_version)) + dep_updates.append((dep_name, None, latest_version)) # Summary print(f"\n{'=' * 60}") - print(f"Summary: {len(up_to_date)} up-to-date, ", end="") - print(f"{len(updates_available)} updates available") + print(f"Summary: {len(dep_up_to_date)} up-to-date, ", end="") + print(f"{len(dep_updates)} updates available") - if updates_available: + if dep_updates: print("\nTo update dependencies, run:") print(" python3 -m relenv versions --update-deps") diff --git a/tests/test_pyversions_runtime.py b/tests/test_pyversions_runtime.py index e6c8afe9..52e924e7 100644 --- a/tests/test_pyversions_runtime.py +++ b/tests/test_pyversions_runtime.py @@ -192,6 +192,29 @@ def fake_fetch(url: str) -> str: assert versions[0] == "5.8.1" +def test_detect_python_versions(monkeypatch: pytest.MonkeyPatch) -> None: + """Test Python version detection from python.org.""" + mock_html = """ + + Python 3.14.5 + Python 3.13.2 + Python 2.7.18 + + """ + + def fake_fetch(url: str) -> str: + return mock_html + + monkeypatch.setattr(pyversions, "fetch_url_content", fake_fetch) + versions = pyversions.detect_python_versions() + assert isinstance(versions, list) + assert any(str(v) == "3.14.5" for v in versions) + assert any(str(v) == "3.13.2" for v in versions) + assert not any(str(v) == "2.7.18" for v in versions) # Should filter major < 3 + # Verify sorting (latest first) + assert str(versions[0]) == "3.14.5" + + def test_resolve_python_version_none_defaults_to_latest_310() -> None: """Test that None resolves to the latest 3.10 version.""" result = pyversions.resolve_python_version(None) From 505f725c23c7f21578d6b494a6f9307205eb0cad Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Mon, 11 May 2026 16:53:56 -0700 Subject: [PATCH 2/2] Fix for pyexpat --- relenv/build/darwin.py | 14 ++++++++++++++ relenv/build/linux.py | 14 ++++++++++++++ relenv/build/windows.py | 14 ++++++++++++++ relenv/python-versions.json | 2 +- 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/relenv/build/darwin.py b/relenv/build/darwin.py index 4016fcdf..ab6c8a4b 100644 --- a/relenv/build/darwin.py +++ b/relenv/build/darwin.py @@ -106,6 +106,20 @@ def update_expat(dirs: Dirs, env: MutableMapping[str, str]) -> None: for target_file in updated_files: os.utime(target_file, (now, now)) + # For expat >= 2.8.0, new entropy source files are required but not compiled + # by Python's build system. Include them directly in xmlparse.c. + xmlparse_c = expat_dir / "xmlparse.c" + if xmlparse_c.exists(): + with open(str(xmlparse_c), "a") as f: + f.write("\n/* Relenv: include new entropy sources for expat >= 2.8.0 */\n") + f.write('#if defined(_WIN32)\n#include "random_rand_s.c"\n#endif\n') + f.write('#if defined(HAVE_GETENTROPY)\n#include "random_getentropy.c"\n#endif\n') + f.write("#if defined(HAVE_GETRANDOM) || defined(HAVE_SYSCALL_GETRANDOM)\n") + f.write('#include "random_getrandom.c"\n#endif\n') + f.write('#if defined(HAVE_ARC4RANDOM_BUF)\n#include "random_arc4random_buf.c"\n#endif\n') + f.write('#if defined(HAVE_ARC4RANDOM)\n#include "random_arc4random.c"\n#endif\n') + f.write('#if !defined(_WIN32) && defined(XML_DEV_URANDOM)\n#include "random_dev_urandom.c"\n#endif\n') + # Update SBOM with correct checksums for updated expat files files_to_update = {} for target_file in updated_files: diff --git a/relenv/build/linux.py b/relenv/build/linux.py index 032bee0d..b4f6780e 100644 --- a/relenv/build/linux.py +++ b/relenv/build/linux.py @@ -463,6 +463,20 @@ def update_expat(dirs: Dirs, env: EnvMapping) -> None: for target_file in updated_files: os.utime(target_file, (now, now)) + # For expat >= 2.8.0, new entropy source files are required but not compiled + # by Python's build system. Include them directly in xmlparse.c. + xmlparse_c = expat_dir / "xmlparse.c" + if xmlparse_c.exists(): + with open(str(xmlparse_c), "a") as f: + f.write("\n/* Relenv: include new entropy sources for expat >= 2.8.0 */\n") + f.write('#if defined(_WIN32)\n#include "random_rand_s.c"\n#endif\n') + f.write('#if defined(HAVE_GETENTROPY)\n#include "random_getentropy.c"\n#endif\n') + f.write("#if defined(HAVE_GETRANDOM) || defined(HAVE_SYSCALL_GETRANDOM)\n") + f.write('#include "random_getrandom.c"\n#endif\n') + f.write('#if defined(HAVE_ARC4RANDOM_BUF)\n#include "random_arc4random_buf.c"\n#endif\n') + f.write('#if defined(HAVE_ARC4RANDOM)\n#include "random_arc4random.c"\n#endif\n') + f.write('#if !defined(_WIN32) && defined(XML_DEV_URANDOM)\n#include "random_dev_urandom.c"\n#endif\n') + # Update SBOM with correct checksums for updated expat files files_to_update = {} for target_file in updated_files: diff --git a/relenv/build/windows.py b/relenv/build/windows.py index 1c9fb210..d7bd61d4 100644 --- a/relenv/build/windows.py +++ b/relenv/build/windows.py @@ -348,6 +348,20 @@ def update_expat(dirs: Dirs, env: EnvMapping) -> None: for target_file in updated_files: os.utime(target_file, (now, now)) + # For expat >= 2.8.0, new entropy source files are required but not compiled + # by Python's build system. Include them directly in xmlparse.c. + xmlparse_c = expat_dir / "xmlparse.c" + if xmlparse_c.exists(): + with open(str(xmlparse_c), "a") as f: + f.write("\n/* Relenv: include new entropy sources for expat >= 2.8.0 */\n") + f.write('#if defined(_WIN32)\n#include "random_rand_s.c"\n#endif\n') + f.write('#if defined(HAVE_GETENTROPY)\n#include "random_getentropy.c"\n#endif\n') + f.write("#if defined(HAVE_GETRANDOM) || defined(HAVE_SYSCALL_GETRANDOM)\n") + f.write('#include "random_getrandom.c"\n#endif\n') + f.write('#if defined(HAVE_ARC4RANDOM_BUF)\n#include "random_arc4random_buf.c"\n#endif\n') + f.write('#if defined(HAVE_ARC4RANDOM)\n#include "random_arc4random.c"\n#endif\n') + f.write('#if !defined(_WIN32) && defined(XML_DEV_URANDOM)\n#include "random_dev_urandom.c"\n#endif\n') + # Update SBOM with correct checksums for updated expat files files_to_update = {f"Modules/expat/{f.name}": f for f in updated_files} if bash_refresh.exists(): diff --git a/relenv/python-versions.json b/relenv/python-versions.json index 7c859bf9..09559b2f 100644 --- a/relenv/python-versions.json +++ b/relenv/python-versions.json @@ -522,4 +522,4 @@ } } } -} \ No newline at end of file +}