mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-10-16 15:21:03 +00:00
python: pin core libs release with sha-256
This commit is contained in:
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user