diff --git a/relenv/build/darwin.py b/relenv/build/darwin.py
index ab6c8a4b..c35fb80f 100644
--- a/relenv/build/darwin.py
+++ b/relenv/build/darwin.py
@@ -197,7 +197,7 @@ def build_python(env: MutableMapping[str, str], dirs: Dirs, logfp: IO[str]) -> N
xz_checksum = xz_info["sha256"]
else:
xz_version = "5.8.1"
- xz_url = "http://tukaani.org/xz/xz-{version}.tar.gz"
+ xz_url = "https://github.com/tukaani-project/xz/releases/download/v{version}/xz-{version}.tar.gz"
xz_checksum = "ed4d5589c4cfe84e1697bd02a9954b76af336931"
build.add(
diff --git a/relenv/build/linux.py b/relenv/build/linux.py
index 35f2206f..62b10c9d 100644
--- a/relenv/build/linux.py
+++ b/relenv/build/linux.py
@@ -689,7 +689,7 @@ def build_python(env: EnvMapping, dirs: Dirs, logfp: IO[str]) -> None:
else:
# Fallback to hardcoded values
xz_version = "5.8.1"
- xz_url = "http://tukaani.org/xz/xz-{version}.tar.gz"
+ xz_url = "https://github.com/tukaani-project/xz/releases/download/v{version}/xz-{version}.tar.gz"
xz_checksum = "ed4d5589c4cfe84e1697bd02a9954b76af336931"
build.add(
diff --git a/relenv/python-versions.json b/relenv/python-versions.json
index 1af410f4..d84959b1 100644
--- a/relenv/python-versions.json
+++ b/relenv/python-versions.json
@@ -367,7 +367,7 @@
},
"xz": {
"5.8.2": {
- "url": "http://tukaani.org/xz/xz-{version}.tar.gz",
+ "url": "https://github.com/tukaani-project/xz/releases/download/v{version}/xz-{version}.tar.gz",
"sha256": "ce09c50a5962786b83e5da389c90dd2c15ecd0980a258dd01f70f9e7ce58a8f1",
"platforms": [
"linux",
@@ -376,7 +376,7 @@
]
},
"5.8.3": {
- "url": "http://tukaani.org/xz/xz-{version}.tar.gz",
+ "url": "https://github.com/tukaani-project/xz/releases/download/v{version}/xz-{version}.tar.gz",
"sha256": "3d3a1b973af218114f4f889bbaa2f4c037deaae0c8e815eec381c3d546b974a0",
"platforms": [
"linux",
diff --git a/relenv/pyversions.py b/relenv/pyversions.py
index c9f210ab..a9844cc0 100644
--- a/relenv/pyversions.py
+++ b/relenv/pyversions.py
@@ -331,16 +331,36 @@ def detect_sqlite_versions() -> list[tuple[str, str]]:
def detect_xz_versions() -> list[str]:
"""
- Detect available XZ versions from tukaani.org.
+ Detect available XZ versions from the tukaani-project GitHub releases API.
+
+ Sourcing xz version metadata from tukaani.org is discouraged after
+ CVE-2024-3094 (the March 2024 xz-utils supply-chain backdoor). Both
+ release tarball hosting and the authoritative release listing moved
+ to https://github.com/tukaani-project/xz, so we consult that project's
+ Releases API and skip drafts / prereleases.
"""
- url = "https://tukaani.org/xz/"
+ url = "https://api.github.com/repos/tukaani-project/xz/releases"
content = fetch_url_content(url)
- # Find xz-X.Y.Z.tar.gz
- pattern = r"xz-(\d+\.\d+\.\d+)\.tar\.gz"
- matches = re.findall(pattern, content)
- # Deduplicate and sort
- versions = sorted(set(matches), key=lambda v: [int(x) for x in v.split(".")], reverse=True)
- return versions
+ try:
+ releases = json.loads(content)
+ except json.JSONDecodeError:
+ return []
+ versions: list[str] = []
+ for release in releases:
+ if not isinstance(release, dict):
+ continue
+ if release.get("draft") or release.get("prerelease"):
+ continue
+ tag = release.get("tag_name", "")
+ if not isinstance(tag, str):
+ continue
+ # Tags are like "v5.8.4"; accept plain "5.8.4" too for robustness.
+ if tag.startswith("v"):
+ tag = tag[1:]
+ if not re.fullmatch(r"\d+\.\d+\.\d+", tag):
+ continue
+ versions.append(tag)
+ return sorted(set(versions), key=lambda v: [int(x) for x in v.split(".")], reverse=True)
def detect_libffi_versions() -> list[str]:
@@ -691,13 +711,13 @@ def update_dependency_versions(path: pathlib.Path, deps_to_update: list[str] | N
if "xz" not in dependencies:
dependencies["xz"] = {}
if latest not in dependencies["xz"]:
- url = f"http://tukaani.org/xz/xz-{latest}.tar.gz"
+ url = f"https://github.com/tukaani-project/xz/releases/download/v{latest}/xz-{latest}.tar.gz"
print(f"Downloading {url}...")
download_path = download_url(url, cwd)
checksum = sha256_digest(download_path)
print(f"SHA-256: {checksum}")
dependencies["xz"][latest] = {
- "url": "http://tukaani.org/xz/xz-{version}.tar.gz",
+ "url": "https://github.com/tukaani-project/xz/releases/download/v{version}/xz-{version}.tar.gz",
"sha256": checksum,
"platforms": ["linux", "darwin", "win32"],
}
diff --git a/tests/test_pyversions_runtime.py b/tests/test_pyversions_runtime.py
index 52e924e7..50eebae8 100644
--- a/tests/test_pyversions_runtime.py
+++ b/tests/test_pyversions_runtime.py
@@ -170,24 +170,35 @@ def fake_fetch(url: str) -> str:
def test_detect_xz_versions(monkeypatch: pytest.MonkeyPatch) -> None:
- """Test XZ version detection from tukaani.org."""
- mock_html = """
-
- xz-5.8.1.tar.gz
- xz-5.8.0.tar.gz
- xz-5.6.3.tar.gz
-
- """
+ """Test XZ version detection from the tukaani-project GitHub releases API."""
+ import json
+
+ mock_releases = json.dumps(
+ [
+ {"tag_name": "v5.8.1", "draft": False, "prerelease": False},
+ {"tag_name": "v5.7.2beta", "draft": False, "prerelease": True},
+ {"tag_name": "v5.8.0", "draft": False, "prerelease": False},
+ {"tag_name": "v5.6.3", "draft": False, "prerelease": False},
+ {"tag_name": "v5.9.0-draft", "draft": True, "prerelease": False},
+ ]
+ )
+
+ captured_url: dict[str, str] = {}
def fake_fetch(url: str) -> str:
- return mock_html
+ captured_url["url"] = url
+ return mock_releases
monkeypatch.setattr(pyversions, "fetch_url_content", fake_fetch)
versions = pyversions.detect_xz_versions()
+ assert captured_url["url"] == "https://api.github.com/repos/tukaani-project/xz/releases"
assert isinstance(versions, list)
assert "5.8.1" in versions
assert "5.8.0" in versions
assert "5.6.3" in versions
+ # Draft and prerelease tags must be skipped
+ assert "5.7.2beta" not in versions
+ assert not any(v.startswith("5.9") for v in versions)
# Verify sorting (latest first)
assert versions[0] == "5.8.1"