fix(flatpak): bundle onnxruntime for Silero VAD (#1002)

* fix(flatpak): bundle onnxruntime for Silero VAD

Bundle the [vad] extra in the Flatpak and select the matching CPython wheel for each architecture.

Fixes #969

* docs(flatpak): keep the runtime.txt history in the sync script docstring

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-10-21 00:27:05 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent e53fc2ceb0
commit 1909f45fdf
7 changed files with 277 additions and 54 deletions
+2 -2
View File
@@ -84,7 +84,7 @@ just lock # regenerate uv.lock, requirements/*.txt and the Flatpak dep
just lock-check # fail if uv.lock is stale vs pyproject.toml
just distro-packages # regenerate install.d/package_map.sh from YAML
just distro-packages-check # fail if the generated package map is stale
just flatpak-deps # regenerate the Flatpak dep manifest from requirements/runtime.txt (needs PyPI)
just flatpak-deps # regenerate the Flatpak dep manifest from runtime + [vad] (needs PyPI)
just flatpak-deps-check # fail if that manifest is behind the export
just model-checksums # refresh pinned model digests after adding a model
just appimage # build the AppImage in its pinned base image (needs docker)
@@ -132,7 +132,7 @@ Website: `web/AGENTS.md`, `web/PRODUCT.md`, `web/DESIGN.md`. Do not duplicate si
| AppImage build inputs | Base image, tooling, interpreter, shaderc and the Vulkan headers are pinned in `packaging/appimage/tool_checksums.txt`. Build with `just appimage` (docker) — building on the host ships the host's glibc, which is what kept the AppImage off Debian 12 |
| AppImage boot matrix | `packaging/appimage/boot-test.sh` runs the finished AppImage in distro containers, and the matrix in `unified-pipeline.yml` must keep distros both older and newer than the build image: the old ones prove the glibc floor, the new ones catch a bundle that breaks the host binaries it spawns. A distro added there needs its package recipe in the script — `tests/test_appimage_packaging.py` checks both |
| Native packages (.deb / .rpm) | Thin packages built by nfpm in the pinned Debian 12 image (`packaging/native/tool_checksums.txt` pins the image and the nfpm releases). Python, GTK and PyGObject come from the distro; only `pywhispercpp` and `pynput` are vendored under `/usr/lib/vocalinux/vendor` (unpackaged on Debian/Fedora). `packaging/native/smoke-test.sh` installs the package per-distro and runs `vocalinux --version` plus an import smoke — `tests/test_native_packaging.py` keeps the deps lists, vendored set and CI matrix consistent offline |
| Flatpak deps | `packaging/flatpak/python3-dependencies.yaml` is generated from `requirements/runtime.txt` by `scripts/sync_flatpak_deps.py` (`just flatpak-deps`, which `just lock` runs itself). It resolves each artifact by the digest uv already recorded, so the Flatpak downloads the bytes in `uv.lock`. The project installs there with `pip3 install --no-deps`, so nothing inside the build evaluates `pyproject.toml` — `tests/test_flatpak_packaging.py` checks the invariant offline |
| Flatpak deps | `packaging/flatpak/python3-dependencies.yaml` is generated from `requirements/vad.txt` (runtime dependencies plus the `[vad]` extra) by `scripts/sync_flatpak_deps.py` (`just flatpak-deps`, which `just lock` runs itself). It resolves each artifact by the digest uv already recorded, so the Flatpak downloads the bytes in `uv.lock`; `onnxruntime` is pinned to CPython wheels for each Flatpak architecture and the GNOME 50 Python version. The project installs there with `pip3 install --no-deps`, so nothing inside the build evaluates `pyproject.toml` — `tests/test_flatpak_packaging.py` checks the invariant offline |
| Speech models | Verified against digests pinned in `src/vocalinux/utils/model_checksums.txt` before install, by both `install.sh` and the runtime downloaders. **Fails closed** — an unpinned model is refused. Regenerate with `just model-checksums` (never by hand); `tests/test_model_checksums.py` fails if it falls behind. whisper.cpp URLs use a pinned Hugging Face commit, never `main` |
Optional extras: `vosk`, `whisper`, `vad`, `dev`.
+1 -1
View File
@@ -237,7 +237,7 @@ distro-packages-check: _tooling
flatpak-deps: _tooling
uv run --no-sync python scripts/sync_flatpak_deps.py
# Fail if the Flatpak manifest is behind requirements/runtime.txt.
# Fail if the Flatpak manifest is behind requirements/vad.txt (runtime + [vad]).
flatpak-deps-check: _tooling
uv run --no-sync python scripts/sync_flatpak_deps.py --check
+11 -7
View File
@@ -42,9 +42,10 @@ contributors (below).
## Current Scope
The manifest ships the default `whisper_cpp` engine only. VOSK is omitted because
the PyPI package is wheel-only (no sdist), so a Flathub-ready VOSK build would
need to compile VOSK and its native deps from source.
The manifest ships the default `whisper_cpp` engine and Silero VAD
(`onnxruntime`). VOSK is omitted because the PyPI package is wheel-only (no
sdist), so a Flathub-ready VOSK build would need to compile VOSK and its native
deps from source.
Global keyboard shortcuts use **evdev** (`/dev/input`) on Wayland and
XWayland. On native X11, they use the X11 **pynput** listener through
@@ -127,9 +128,10 @@ flatpak-builder --run build-dir packaging/flatpak/com.vocalinux.Vocalinux.yml \
## Python Dependencies
`python3-dependencies.yaml` is generated from `requirements/runtime.txt`, the
hash-pinned export `just lock` writes out of `uv.lock`. Refresh it whenever the
dependency set changes:
`python3-dependencies.yaml` is generated from `requirements/vad.txt`, the
hash-pinned export `just lock` writes out of `uv.lock`. This includes the
runtime dependencies plus the `[vad]` extra. Refresh it whenever the dependency
set changes:
```bash
just flatpak-deps # rewrite the manifest (needs PyPI)
@@ -142,7 +144,9 @@ no extra step.
The script resolves each package's URL by looking up the digest uv already
recorded, so the bytes this build downloads are the bytes in `uv.lock`, not
merely the same version number. It takes a universal wheel where one exists and
the sdist otherwise; an ABI-tagged wheel would pin the build to one SDK Python.
the sdist otherwise. `onnxruntime` has no sdist, so its CPython-ABI wheel is
pinned to GNOME 50's Python version and selected separately for each Flatpak
architecture.
This replaced a `flatpak-pip-generator` invocation whose package list was typed
out on the command line. Nothing regenerated it and no test compared it to
+25 -1
View File
@@ -1,4 +1,4 @@
# Generated by scripts/sync_flatpak_deps.py from requirements/runtime.txt, the
# Generated by scripts/sync_flatpak_deps.py from requirements/vad.txt, the
# hash-pinned export `just lock` writes out of uv.lock. Run `just flatpak-deps`
# after any dependency change; `just flatpak-deps-check` and
# tests/test_flatpak_packaging.py fail when this file falls behind.
@@ -144,6 +144,30 @@ modules:
--prefix=${FLATPAK_DEST} "numpy" --no-build-isolation
sources:
- *id008
- name: python3-onnxruntime
buildsystem: simple
build-commands:
- pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "onnxruntime" --no-build-isolation
sources:
- type: file
url: https://files.pythonhosted.org/packages/83/2c/d8eb945d2a372149df9705a8d5c8d7c6c46c987c5446dbcea9e1ea7f6556/onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_x86_64.whl
sha256: e2128f31f449e922c62dbe5d8b6b7b079f0bcaf2d56a102fa203cb6e5bb5ab19
only-arches: [x86_64]
- type: file
url: https://files.pythonhosted.org/packages/c9/17/b9ad04051a8c4f504852ce0e8e10f9a6b2f1a331eedcdcc503df776dd0ea/onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_aarch64.whl
sha256: d67673c5367727860922c5262d724472f1b5539fb7ccf4c81a638f9b71719803
only-arches: [aarch64]
- type: file
url: https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl
sha256: 7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4
- type: file
url: https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl
sha256: d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c
- type: file
url: https://files.pythonhosted.org/packages/19/c7/5f7c636ec43e0c545e28d1f1db71990108306f7bdcb89f069ba97e428e7f/protobuf-7.35.1-py3-none-any.whl
sha256: 4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9
- *id008
- name: python3-pyaudio
buildsystem: simple
build-commands:
+88 -30
View File
@@ -8,12 +8,16 @@ afterwards and no test compared it to anything, so it drifted: by 2026-09-10 ten
of its fifteen shared packages were behind `requirements/runtime.txt`, including
pywhispercpp, which sat at 1.4.1 under the project's own `>=1.5.0` floor. The
Flatpak installs the project with `pip3 install --no-deps`, so that constraint
is never evaluated and nothing there could notice.
is never evaluated and nothing there could notice. It now reads
`requirements/vad.txt`, the runtime dependencies plus the `[vad]` extra, so
Silero VAD's onnxruntime ships too (#969).
This script closes the loop the other way round: it reads the hash-pinned export
that `just lock` produces and rewrites every source in the manifest to the
artifact whose digest uv already recorded. The bytes the Flatpak downloads are
then the bytes in `uv.lock`, verified by hash rather than by convention.
then the bytes in `uv.lock`, verified by hash rather than by convention. The
`onnxruntime` sources are selected as CPython-ABI wheels for each Flatpak
architecture because PyPI publishes no sdist for it.
It edits in place rather than regenerating, because two things in that file are
hand-written and no generator produces them: pywhispercpp's Vulkan build with
@@ -41,15 +45,14 @@ from packaging.markers import Marker
from packaging.utils import canonicalize_name
REPO_ROOT = Path(__file__).resolve().parents[1]
EXPORT = REPO_ROOT / "requirements" / "runtime.txt"
EXPORT = REPO_ROOT / "requirements" / "vad.txt"
DEPS_YAML = REPO_ROOT / "packaging" / "flatpak" / "python3-dependencies.yaml"
MANIFEST = REPO_ROOT / "packaging" / "flatpak" / "com.vocalinux.Vocalinux.yml"
#: The environment the Flatpak build resolves for. `flatpak remote-info flathub
#: org.gnome.Sdk//50 --show-metadata` reports a freedesktop 25.08 base, which
#: ships Python 3.13. Only numpy reads this today: the lock splits it at
#: `python_full_version >= '3.12'`. Everything else is marked on sys_platform
#: alone. Bump it when the manifest's runtime-version moves.
#: The environment the Flatpak build resolves for. GNOME 50 ships Python 3.13.
#: numpy's lock entry splits at `python_full_version >= '3.12'`; onnxruntime's
#: CPython wheel tag also uses this Python version. Bump it when the manifest's
#: runtime-version moves.
TARGET_ENV = {
"sys_platform": "linux",
"platform_system": "Linux",
@@ -67,9 +70,16 @@ TARGET_ENV = {
#: `just lock` builds with --no-emit-package pygobject for the same reason.
RUNTIME_PROVIDED = {"pycairo"}
#: onnxruntime has no sdist on PyPI, and building it from source is out of reach
#: in the sandbox. These come as CPython-ABI wheels, one per Flatpak arch, tagged
#: for TARGET_ENV's python_version; a runtime Python bump makes pip --no-index
#: fail loudly at build time rather than silently.
BINARY_ONLY = {"onnxruntime"}
FLATPAK_ARCHES = ("x86_64", "aarch64")
#: Build backends for the sdists the manifest builds offline under
#: --no-build-isolation. They are not runtime dependencies, so they never appear
#: in requirements/runtime.txt and stay pinned here by hand.
#: in requirements/vad.txt and stay pinned here by hand.
BUILD_BACKENDS = {"meson-python", "pyproject-metadata"}
PYPI_JSON = "https://pypi.org/pypi/{name}/{version}/json"
@@ -116,31 +126,53 @@ def pypi_artifacts(name: str, version: str) -> list[dict]:
return json.load(response)["urls"]
def choose_artifact(name: str, version: str, digests: set[str]) -> tuple[str, str]:
def choose_artifact(
name: str, version: str, digests: set[str], arch: str | None = None
) -> tuple[str, str]:
"""The one file the Flatpak should download, as (url, sha256).
Prefer a universal wheel, fall back to the sdist. That is the rule the
original generator followed and it holds for every package here: the pure
Python ones ship `py3-none-any`, and the compiled ones (numpy, evdev,
PyAudio, psutil, pywhispercpp) ship no universal wheel, so the sandbox
builds them from source against the SDK's own interpreter. Never an
ABI-tagged wheel, whose cp tag would pin us to one SDK Python.
Binary-only packages use an architecture-specific CPython wheel. For every
other package, prefer a universal wheel and fall back to the sdist so the
sandbox builds compiled dependencies against the SDK's interpreter.
"""
artifacts = pypi_artifacts(name, version)
wheels = [
a
for a in artifacts
if a["packagetype"] == "bdist_wheel" and a["filename"].endswith("-none-any.whl")
]
sdists = [a for a in artifacts if a["packagetype"] == "sdist"]
candidates = wheels or sdists
if name in BINARY_ONLY:
if arch not in FLATPAK_ARCHES:
raise SystemExit(
f"{name} {version}: expected one of the Flatpak architectures "
f"{', '.join(FLATPAK_ARCHES)}, got {arch!r}"
)
python_tag = TARGET_ENV["python_version"].replace(".", "")
wheel_pattern = re.compile(
rf"-cp{python_tag}-cp{python_tag}-manylinux_.+_{re.escape(arch)}\.whl$"
)
candidates = [
artifact
for artifact in artifacts
if artifact["packagetype"] == "bdist_wheel"
and wheel_pattern.search(artifact["filename"])
]
if not candidates:
raise SystemExit(
f"{name} {version} publishes no CPython cp{python_tag} manylinux "
f"wheel for {arch}; update the Flatpak Python runtime or package"
)
else:
wheels = [
artifact
for artifact in artifacts
if artifact["packagetype"] == "bdist_wheel"
and artifact["filename"].endswith("-none-any.whl")
]
sdists = [artifact for artifact in artifacts if artifact["packagetype"] == "sdist"]
candidates = wheels or sdists
if not candidates:
raise SystemExit(
f"{name} {version} publishes neither a universal wheel nor an sdist; "
"the Flatpak cannot build it without an ABI-tagged wheel"
)
# A release can publish more than one universal wheel (PySocks 1.7.1 ships
# py27 and py3 builds); take the first whose digest uv actually locked.
# A release can publish more than one matching wheel; take the first whose
# digest uv actually locked.
picked = next((a for a in candidates if a["digests"]["sha256"] in digests), None)
if picked is None:
first = candidates[0]
@@ -189,9 +221,22 @@ def main() -> int:
text = DEPS_YAML.read_text(encoding="utf-8")
lines = text.splitlines(keepends=True)
in_manifest = {artifact_name(name) for _, name, _ in manifest_sources(text)}
sources = manifest_sources(text)
in_manifest = {artifact_name(name) for _, name, _ in sources}
stale = sorted(in_manifest - set(locked) - BUILD_BACKENDS)
missing = sorted(set(locked) - in_manifest - RUNTIME_PROVIDED)
binary_arches = {name: set() for name in BINARY_ONLY}
for _, filename, _ in sources:
name = artifact_name(filename)
if name in BINARY_ONLY:
arch_match = re.search(r"_(x86_64|aarch64)\.whl$", filename)
if arch_match:
binary_arches[name].add(arch_match.group(1))
for name in BINARY_ONLY & set(locked):
missing.extend(
f"{name} ({arch})" for arch in FLATPAK_ARCHES if arch not in binary_arches[name]
)
missing.sort()
if stale:
print(
f"{DEPS_YAML.name} builds packages the lock does not resolve: "
@@ -213,14 +258,20 @@ def main() -> int:
return 1
changes: list[str] = []
for index, filename, old_sha in manifest_sources(text):
for index, filename, old_sha in sources:
name = artifact_name(filename)
if name in BUILD_BACKENDS:
# Absent from the lock by definition: nothing imports them at
# runtime, they only let the sandbox build the sdists offline.
continue
version, digests = locked[name]
url, sha = choose_artifact(name, version, digests)
arch = None
if name in BINARY_ONLY:
arch_match = re.search(r"_(x86_64|aarch64)\.whl$", filename)
if not arch_match:
raise SystemExit(f"{name}: cannot determine a Flatpak architecture from {filename}")
arch = arch_match.group(1)
url, sha = choose_artifact(name, version, digests, arch)
if sha == old_sha:
continue
indent = re.match(r"^(\s*)", lines[index]).group(1)
@@ -265,11 +316,18 @@ def main() -> int:
)
if updated == text and manifest_updated == manifest_text:
print("Flatpak dependencies already match requirements/runtime.txt")
print(
"Flatpak dependencies already match requirements/vad.txt "
"(runtime dependencies plus the [vad] extra)"
)
return 0
if args.check:
print("Flatpak dependencies are behind requirements/runtime.txt:", file=sys.stderr)
print(
"Flatpak dependencies are behind requirements/vad.txt "
"(runtime dependencies plus the [vad] extra):",
file=sys.stderr,
)
for change in changes:
print(change, file=sys.stderr)
if manifest_updated != manifest_text:
+71 -13
View File
@@ -24,6 +24,7 @@ from pathlib import Path
import pytest
from packaging.markers import Marker
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.utils import canonicalize_name
from packaging.version import Version
@@ -43,7 +44,7 @@ PORTABLE_GGML_OPTIONS = (
REPO_ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = REPO_ROOT / "pyproject.toml"
EXPORT = REPO_ROOT / "requirements" / "runtime.txt"
EXPORT = REPO_ROOT / "requirements" / "vad.txt"
FLATPAK_DIR = REPO_ROOT / "packaging" / "flatpak"
DEPS_YAML = FLATPAK_DIR / "python3-dependencies.yaml"
MANIFEST = FLATPAK_DIR / "com.vocalinux.Vocalinux.yml"
@@ -70,6 +71,8 @@ RUNTIME_PROVIDED = {"pycairo"}
#: Build backends for the sdists the sandbox compiles under
#: --no-build-isolation. Never imported at runtime, so never in the export.
BUILD_BACKENDS = {"meson-python", "pyproject-metadata"}
BINARY_ONLY = {"onnxruntime"}
FLATPAK_ARCHES = ("x86_64", "aarch64")
def _locked() -> dict:
@@ -95,7 +98,7 @@ def _locked() -> dict:
return selected
def _flatpak_sources() -> list:
def _flatpak_sources() -> list[dict[str, str | None]]:
"""Every PyPI artifact the Flatpak downloads: name, version, digest, filename."""
lines = DEPS_YAML.read_text(encoding="utf-8").splitlines()
sources = []
@@ -106,6 +109,11 @@ def _flatpak_sources() -> list:
filename = url.group(1)
digest = re.match(r"^\s*sha256: ([0-9a-f]{64})\s*$", lines[index + 1])
assert digest, f"{DEPS_YAML.name}:{index + 2}: PyPI source with no sha256 under it"
arch_match = (
re.match(r"^\s*only-arches: \[([a-z0-9_]+)\]\s*$", lines[index + 2])
if index + 2 < len(lines)
else None
)
if filename.endswith(".whl"):
name, version = filename.split("-")[0], filename.split("-")[1]
else:
@@ -117,6 +125,7 @@ def _flatpak_sources() -> list:
"version": version,
"sha256": digest.group(1),
"filename": filename,
"only_arches": arch_match.group(1) if arch_match else None,
}
)
assert sources, "no PyPI sources found; the parser and the file disagree"
@@ -150,7 +159,7 @@ def test_every_source_is_the_version_the_lock_resolved(source: dict[str, str]) -
)
assert source["version"] == locked["version"], (
f"{source['name']}: Flatpak builds {source['version']},"
f" requirements/runtime.txt pins {locked['version']}."
f" requirements/vad.txt pins {locked['version']}."
" Run `just flatpak-deps`."
)
@@ -164,11 +173,11 @@ def test_every_digest_is_one_uv_recorded(source: dict[str, str]) -> None:
hashes = _locked()[source["name"]]["hashes"]
assert source["sha256"] in hashes, (
f"{source['filename']} carries a digest absent from"
" requirements/runtime.txt. Run `just flatpak-deps`."
" requirements/vad.txt. Run `just flatpak-deps`."
)
def test_the_flatpak_carries_every_runtime_dependency() -> None:
def test_the_flatpak_carries_every_exported_dependency() -> None:
"""A missing one fails at first launch, not at build time, because the
project is installed with --no-deps and nothing resolves the gap."""
missing = sorted(set(_locked()) - set(_flatpak_versions()) - RUNTIME_PROVIDED)
@@ -178,6 +187,49 @@ def test_the_flatpak_carries_every_runtime_dependency() -> None:
)
def test_every_vad_extra_package_is_built_by_the_flatpak() -> None:
"""Keep optional VAD dependencies in the Flatpak export (#969)."""
with PYPROJECT.open("rb") as handle:
vad_dependencies = tomllib.load(handle)["project"]["optional-dependencies"]["vad"]
vad_packages = {
canonicalize_name(Requirement(dependency).name) for dependency in vad_dependencies
}
missing = sorted(vad_packages - set(_flatpak_versions()))
assert not missing, f"the Flatpak does not build [vad] packages: {', '.join(missing)}"
def test_the_silero_model_is_included_as_package_data() -> None:
"""The Flatpak app install must ship the model alongside Silero VAD."""
with PYPROJECT.open("rb") as handle:
package_data = tomllib.load(handle)["tool"]["setuptools"]["package-data"]
assert "data/*.onnx" in package_data["vocalinux.speech_recognition"]
assert (
REPO_ROOT / "src" / "vocalinux" / "speech_recognition" / "data" / "silero_vad.onnx"
).is_file()
def test_each_binary_only_package_has_one_wheel_per_flatpak_architecture() -> None:
"""Each binary-only wheel is restricted to its matching Flatpak architecture."""
sources = _flatpak_sources()
for name in BINARY_ONLY:
package_sources = [
source
for source in sources
if source["name"] == name and source["filename"] is not None
]
for arch in FLATPAK_ARCHES:
arch_sources = [
source for source in package_sources if source["filename"].endswith(f"_{arch}.whl")
]
assert (
len(arch_sources) == 1
), f"{name} needs exactly one wheel for {arch}, found {len(arch_sources)}"
source = arch_sources[0]
assert (
source["only_arches"] == arch
), f"{source['filename']} must carry only-arches: [{arch}]"
def test_the_flatpak_builds_nothing_the_lock_does_not_resolve() -> None:
"""#705 deleted pydub, lxml, tqdm and python-xlib from `pyproject.toml`.
@@ -266,18 +318,24 @@ def test_the_manifest_env_agrees_with_the_module() -> None:
@pytest.mark.parametrize("source", _flatpak_sources(), ids=lambda s: s["name"])
def test_no_source_is_pinned_to_one_python_abi(source: dict[str, str]) -> None:
"""Universal wheel or sdist, never a cp-tagged wheel.
"""Keep universal wheels or sdists except binary-only runtime wheels.
The SDK's Python minor version is not ours to choose and moves with the
GNOME runtime. An abi3 or cp313 wheel builds today and stops resolving the
day the runtime bumps, with nothing in this repo changed.
Binary-only wheels must match the CPython ABI of GNOME 50. An unrelated ABI
tag would make a runtime Python bump fail during the offline build.
"""
if not source["filename"].endswith(".whl"):
return
assert source["filename"].endswith("-none-any.whl"), (
f"{source['filename']} is pinned to one interpreter ABI; take the sdist"
" and let the sandbox build it against the SDK's own Python"
)
if source["name"] in BINARY_ONLY:
python_tag = TARGET_ENV["python_version"].replace(".", "")
assert re.search(
rf"-cp{python_tag}-cp{python_tag}-manylinux_.+_(x86_64|aarch64)\.whl$",
source["filename"],
), f"{source['filename']} is not a wheel for the Flatpak's CPython runtime"
else:
assert source["filename"].endswith("-none-any.whl"), (
f"{source['filename']} is pinned to one interpreter ABI; take the sdist"
" and let the sandbox build it against the SDK's own Python"
)
def _on_event_block(event: str) -> str:
+79
View File
@@ -0,0 +1,79 @@
"""Unit tests for the Flatpak dependency artifact selector."""
import importlib.util
from pathlib import Path
from typing import Any
import pytest
SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "sync_flatpak_deps.py"
SPEC = importlib.util.spec_from_file_location("sync_flatpak_deps", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
SYNC: Any = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(SYNC)
def _wheel(filename: str, digest: str) -> dict[str, object]:
"""Return a small PyPI wheel record for artifact selection tests."""
return {
"packagetype": "bdist_wheel",
"filename": filename,
"digests": {"sha256": digest},
"url": f"https://example.invalid/{filename}",
}
def _install_artifacts(monkeypatch: pytest.MonkeyPatch, artifacts: list[dict[str, object]]) -> None:
"""Replace the PyPI lookup with deterministic artifact records."""
def fake_pypi_artifacts(name: str, version: str) -> list[dict[str, object]]:
return artifacts
monkeypatch.setattr(SYNC, "pypi_artifacts", fake_pypi_artifacts)
def test_binary_only_selection_uses_matching_cpython_architecture_wheel(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Select the locked cp313 x86_64 wheel, not other ABI or arch variants."""
artifacts = [
_wheel("onnxruntime-1.29.0-cp313-cp313t-manylinux_2_28_x86_64.whl", "a" * 64),
_wheel("onnxruntime-1.29.0-cp312-cp312-manylinux_2_28_x86_64.whl", "b" * 64),
_wheel("onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_aarch64.whl", "c" * 64),
_wheel("onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_x86_64.whl", "d" * 64),
]
_install_artifacts(monkeypatch, artifacts)
selected = SYNC.choose_artifact(
"onnxruntime",
"1.29.0",
{"a" * 64, "b" * 64, "c" * 64, "d" * 64},
arch="x86_64",
)
assert selected == (artifacts[-1]["url"], "d" * 64)
def test_binary_only_selection_fails_when_matching_wheel_digest_is_not_locked(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Do not select a compatible wheel unless uv recorded its digest."""
wheel = _wheel("onnxruntime-1.29.0-cp313-cp313-manylinux_2_28_x86_64.whl", "e" * 64)
_install_artifacts(monkeypatch, [wheel])
with pytest.raises(SystemExit, match="not among the hashes uv locked"):
SYNC.choose_artifact("onnxruntime", "1.29.0", {"f" * 64}, arch="x86_64")
def test_non_binary_selection_prefers_universal_wheel(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Non-binary dependencies retain the universal-wheel preference."""
source = _wheel("sample-1.0.tar.gz", "1" * 64)
source["packagetype"] = "sdist"
universal = _wheel("sample-1.0-py3-none-any.whl", "2" * 64)
_install_artifacts(monkeypatch, [source, universal])
selected = SYNC.choose_artifact("sample", "1.0", {"1" * 64, "2" * 64})
assert selected == (universal["url"], "2" * 64)