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:
co-authored by
Jatin K Malik
parent
e53fc2ceb0
commit
1909f45fdf
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user