python: pin core libs release with sha-256

This commit is contained in:
shum
2026-10-12 10:42:23 +00:00
parent 6e25728ff4
commit 0a27d1a125
4 changed files with 69 additions and 20 deletions
@@ -7,6 +7,7 @@ from __future__ import annotations
import ctypes
import errno
import hashlib
import os
import platform
import sys
@@ -18,7 +19,7 @@ from ctypes import POINTER, c_char_p, c_int, c_uint8, c_void_p
from pathlib import Path
from typing import Any, Literal
from ._version import LIBS_VERSION
from ._version import LIBS_RELEASE, LIBS_SHA256
Backend = Literal["sqlite", "postgres"]
@@ -59,11 +60,14 @@ def _libname() -> str:
return _LIBNAME[sys.platform]
def _libs_url(backend: Backend) -> str:
def _libs_asset(backend: Backend) -> str:
suffix = "-postgres" if backend == "postgres" else ""
return f"simplex-chat-libs-{_platform_tag()}{suffix}.zip"
def _libs_url(backend: Backend) -> str:
return (
f"https://github.com/{_GITHUB_REPO}/releases/download/"
f"v{LIBS_VERSION}/simplex-chat-libs-{_platform_tag()}{suffix}.zip"
f"https://github.com/{_GITHUB_REPO}/releases/download/{LIBS_RELEASE}/{_libs_asset(backend)}"
)
@@ -84,7 +88,7 @@ def _resolve_libs_dir(backend: Backend) -> Path:
"postgres backend is only supported on linux-x86_64; "
f"current platform is {_platform_tag()}"
)
target = _cache_root() / f"v{LIBS_VERSION}" / backend
target = _cache_root() / LIBS_RELEASE / backend
if not (target / _libname()).exists():
_download(target, backend)
return target
@@ -125,16 +129,21 @@ def _download(target: Path, backend: Backend) -> None:
what we need and proceed. NEVER rmtree the target: that creates a TOCTOU
window where another process is reading/loading the file we're deleting.
"""
asset = _libs_asset(backend)
sha256 = LIBS_SHA256.get(asset)
if sha256 is None:
raise RuntimeError(f"no SHA-256 is pinned for {asset}")
target.parent.mkdir(parents=True, exist_ok=True)
url = _libs_url(backend)
print(
f"Downloading libsimplex ({_platform_tag()}, {backend}) v{LIBS_VERSION} from {url} ...",
f"Downloading libsimplex ({_platform_tag()}, {backend}) {LIBS_RELEASE} from {url} ...",
file=sys.stderr,
flush=True,
)
with tempfile.TemporaryDirectory(dir=target.parent) as tmp:
zip_path = Path(tmp) / "libs.zip"
_stream_to_file(url, zip_path, timeout=60.0)
_verify_sha256(zip_path, sha256, url)
with zipfile.ZipFile(zip_path) as zf:
zf.extractall(tmp)
# zip layout: <tmp>/libs/libsimplex.* + libHS*.*
@@ -158,6 +167,16 @@ def _download(target: Path, backend: Backend) -> None:
) from e
def _verify_sha256(file: Path, expected: str, url: str) -> None:
digest = hashlib.sha256()
with file.open("rb") as f:
while chunk := f.read(_DOWNLOAD_CHUNK):
digest.update(chunk)
actual = digest.hexdigest()
if actual != expected:
raise RuntimeError(f"SHA-256 of {url} is {actual}, expected {expected}")
_lock = threading.Lock()
_lib: ctypes.CDLL | None = None
_libc: ctypes.CDLL | None = None
@@ -1,9 +1,16 @@
"""Single source of truth for both the Python package version and the
simplex-chat-libs release tag we depend on.
"""Package version and the simplex-chat-libs release it loads.
Bump both together for normal releases. For wrapper-only fixes use a PEP 440
post-release: __version__ = "6.5.2.post1", LIBS_VERSION unchanged.
Written by scripts/ci/packages_release.py: the release PR bumps the version
and pins new library releases with the SHA-256 of their assets.
"""
__version__ = "7.1.0b6" # PEP 440 — read by hatchling for wheel metadata
LIBS_VERSION = "7.1.0-beta.6" # simplex-chat-libs release tag (no 'v' prefix)
LIBS_RELEASE = "core-7.1.0.11" # simplex-chat-libs release tag
LIBS_SHA256 = {
"simplex-chat-libs-linux-aarch64.zip": "d66aa1b30b9fa20225dad8ef75a9c605961e1c0adde2ba0283e2fcac4e15d260",
"simplex-chat-libs-linux-x86_64-postgres.zip": "925b3166f969b12082ff8ee659e5f2a9933fe40120deb80c9aa3002a5ba6c803",
"simplex-chat-libs-linux-x86_64.zip": "dabad0824e8813ea4a084fbf37e1ba4cf6dcea9cd8ccadc521fec200f246208a",
"simplex-chat-libs-macos-aarch64.zip": "aa74608b149fff7508a47eaac4284dcc6d189c6ed8b7c18428df9158bc176a61",
"simplex-chat-libs-macos-x86_64.zip": "a0e21457e1077be7f71b46c1c27452945b2cee2b6c50efb0004412ce5b7042ec",
"simplex-chat-libs-windows-x86_64.zip": "c01dad574daa101a1cbfd31ddbf2b3d0d986a4a42f97d789d2f878b2050b2b4b",
}
@@ -1,3 +1,4 @@
import hashlib
import zipfile
from pathlib import Path
@@ -5,7 +6,7 @@ import pytest
from simplex_chat import _native
from simplex_chat._native import _cache_root, _download, _resolve_libs_dir, lib_for
from simplex_chat._version import LIBS_VERSION
from simplex_chat._version import LIBS_RELEASE
def test_cache_root_linux(tmp_path, monkeypatch):
@@ -43,7 +44,7 @@ def test_resolve_downloads_when_missing(tmp_path, monkeypatch):
monkeypatch.setattr("simplex_chat._native._download", fake_download)
libs_dir = _resolve_libs_dir("sqlite")
assert libs_dir == tmp_path / "simplex-chat" / f"v{LIBS_VERSION}" / "sqlite"
assert libs_dir == tmp_path / "simplex-chat" / LIBS_RELEASE / "sqlite"
assert called["backend"] == "sqlite"
assert (libs_dir / "libsimplex.so").exists()
@@ -51,7 +52,7 @@ def test_resolve_downloads_when_missing(tmp_path, monkeypatch):
def test_resolve_uses_cache_on_second_call(tmp_path, monkeypatch):
monkeypatch.setenv("XDG_CACHE_HOME", str(tmp_path))
monkeypatch.setattr("sys.platform", "linux")
cached = tmp_path / "simplex-chat" / f"v{LIBS_VERSION}" / "sqlite"
cached = tmp_path / "simplex-chat" / LIBS_RELEASE / "sqlite"
cached.mkdir(parents=True)
(cached / "libsimplex.so").touch()
# Should NOT call _download — use the cached file.
@@ -68,9 +69,8 @@ def test_postgres_on_macos_rejected(monkeypatch):
_resolve_libs_dir("postgres")
def test_atomic_install(tmp_path, monkeypatch):
"""Build a fake libs zip, mock _stream_to_file, verify extraction + atomic rename."""
# Build zip: libs/libsimplex.so + libs/libHS-stub.so
def fake_libs_zip(tmp_path, monkeypatch, sha256=None) -> None:
"""Serve a zip with libs/libsimplex.so + libs/libHS-stub.so, pinned with `sha256` (its own by default)."""
src = tmp_path / "src" / "libs"
src.mkdir(parents=True)
(src / "libsimplex.so").write_text("fake-so")
@@ -85,15 +85,38 @@ def test_atomic_install(tmp_path, monkeypatch):
shutil.copy(zip_path, dest)
pinned = sha256 or hashlib.sha256(zip_path.read_bytes()).hexdigest()
monkeypatch.setattr("simplex_chat._native._stream_to_file", fake_stream)
monkeypatch.setattr("simplex_chat._native._platform_tag", lambda: "linux-x86_64")
monkeypatch.setattr(
"simplex_chat._native.LIBS_SHA256", {"simplex-chat-libs-linux-x86_64.zip": pinned}
)
def test_atomic_install(tmp_path, monkeypatch):
fake_libs_zip(tmp_path, monkeypatch)
target = tmp_path / "out"
_download(target, "sqlite")
assert (target / "libsimplex.so").read_text() == "fake-so"
assert (target / "libHS-stub.so").read_text() == "fake-hs"
def test_download_rejects_another_sha256(tmp_path, monkeypatch):
fake_libs_zip(tmp_path, monkeypatch, sha256="0" * 64)
target = tmp_path / "out"
with pytest.raises(RuntimeError, match=f"expected {'0' * 64}"):
_download(target, "sqlite")
assert sorted(p.name for p in tmp_path.iterdir()) == ["fake-libs.zip", "src"]
def test_download_requires_pinned_sha256(tmp_path, monkeypatch):
fake_libs_zip(tmp_path, monkeypatch)
with pytest.raises(
RuntimeError, match="no SHA-256 is pinned for simplex-chat-libs-linux-x86_64-postgres.zip"
):
_download(tmp_path / "out", "postgres")
def test_libc_on_windows_is_ucrt(monkeypatch):
loaded: list[str | None] = []
monkeypatch.setattr("sys.platform", "win32")
@@ -3,7 +3,7 @@ from unittest.mock import patch
import pytest
from simplex_chat._native import _libname, _libs_url, _platform_tag
from simplex_chat._version import LIBS_VERSION
from simplex_chat._version import LIBS_RELEASE
@patch("sys.platform", "linux")
@@ -45,7 +45,7 @@ def test_url_sqlite(_):
assert (
_libs_url("sqlite")
== "https://github.com/simplex-chat/simplex-chat-libs/releases/download/"
f"v{LIBS_VERSION}/simplex-chat-libs-linux-x86_64.zip"
f"{LIBS_RELEASE}/simplex-chat-libs-linux-x86_64.zip"
)
@@ -54,5 +54,5 @@ def test_url_postgres(_):
assert (
_libs_url("postgres")
== "https://github.com/simplex-chat/simplex-chat-libs/releases/download/"
f"v{LIBS_VERSION}/simplex-chat-libs-linux-x86_64-postgres.zip"
f"{LIBS_RELEASE}/simplex-chat-libs-linux-x86_64-postgres.zip"
)