* feat(packaging): build thin .deb and .rpm with nfpm and attach them to releases Adds packaging/native: an nfpm config that produces a .deb and an .rpm from the release wheel. The packages stay thin - Python, GTK, PyGObject and the typelibs are distro Depends; only pywhispercpp and pynput are vendored because the first is unpackaged on both distros and the second is missing on Fedora. The Debian 12 build image and the nfpm download are pinned in tool_checksums.txt. just native-packages builds both in the pinned container, and native-smoke installs the built package per distro: debian:12 and ubuntu:24.04 exercise the .deb, fedora:42 the .rpm, then each runs vocalinux --version plus an import smoke of the app and the vendored extension. unified-pipeline.yml gates on the same three-distro matrix, and release.yml attaches both arches to the release and covers them in SHA256SUMS and the provenance manifest. Refs #600 * fix(packaging): preserve launcher args and document Wayland input group The launcher joined $* into an unquoted exec, re-splitting file paths that contain spaces before argparse saw them. Exec "$@" directly and re-quote each argument for the inner shell of 'sg input -c'. On Wayland the packages could not grant the 'input' group membership evdev hotkeys need; name the usermod step and its caveat in INSTALL.md and the release notes. * test(packaging): add return types to the native packaging tests --------- Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
275 lines
12 KiB
Makefile
275 lines
12 KiB
Makefile
# Vocalinux justfile
|
|
# Convenient commands for development
|
|
# Run `just` to list all recipes. Python tooling runs through `uv run` against
|
|
# .venv/ — run `just deps` after cloning (requires uv).
|
|
#
|
|
# Two environments, deliberately separate:
|
|
# .venv/ dev tooling, built by uv from .python-version (3.13). `just deps`
|
|
# pip-builds PyGObject from the lock and needs libgirepository-2.0-dev
|
|
# (Ubuntu 24.04+). Debian 12 cannot build 3.56; use `just install-dev`.
|
|
# venv/ what `just install` creates, always from the system Python so the
|
|
# distro PyGObject package is importable.
|
|
# install.sh ignores an activated .venv and rebuilds
|
|
# venv/ if another interpreter created it, so `just install` is safe to
|
|
# run from any shell. Override with SYSTEM_PYTHON=/usr/bin/python3.12.
|
|
|
|
# Extras and groups installed by `just deps`. `uv sync` prunes whatever the flags
|
|
# do not name — omitting `--group lint` really does uninstall the linters.
|
|
# Recipes that run tools use `uv run --no-sync` so they do not undo `just deps-all`
|
|
# (whisper/vosk/docs). They depend on `_tooling` for the same reason: with nothing
|
|
# left to create .venv/, `uv run --no-sync` on a fresh clone leaves an empty one
|
|
# behind and fails with `Failed to spawn: pytest`. CI lints with `--only-group lint`
|
|
# instead: that skips the project, whose pyaudio/PyGObject need system headers a
|
|
# lint runner has no reason to install.
|
|
DEV_EXTRAS := "--extra dev --extra vad --group lint"
|
|
|
|
# List available recipes
|
|
default:
|
|
@just --list
|
|
|
|
# Create .venv/ and keep it matching the lock. --inexact is what makes this safe
|
|
# to run ahead of every recipe: a plain `uv sync` removes extraneous packages, so
|
|
# it would uninstall whatever `just deps-all` installed — the very thing the
|
|
# --no-sync flags below exist to prevent. `version` skips this; it reads one
|
|
# string out of version.py and needs a bare interpreter, not a sync.
|
|
[private]
|
|
_tooling:
|
|
uv sync --inexact {{DEV_EXTRAS}}
|
|
|
|
# Install Vocalinux
|
|
install:
|
|
./install.sh
|
|
|
|
# Install in development mode
|
|
install-dev:
|
|
./install.sh --dev
|
|
|
|
# Install development dependencies into .venv/ (dev + vad extras)
|
|
deps:
|
|
uv sync {{DEV_EXTRAS}}
|
|
|
|
# Install every optional extra — whisper/vosk engines and docs (CUDA torch, multi-GB)
|
|
deps-all:
|
|
uv sync --all-extras --group lint
|
|
|
|
# Run test suite
|
|
test: _tooling
|
|
@echo "Running tests..."
|
|
uv run --no-sync pytest -v
|
|
|
|
# Run tests with coverage
|
|
test-cov: _tooling
|
|
@echo "Running tests with coverage..."
|
|
uv run --no-sync pytest --cov=src --cov-report=html --cov-report=term
|
|
@echo "Coverage report generated in htmlcov/"
|
|
|
|
# Run linters (flake8, black, isort)
|
|
lint: _tooling
|
|
@echo "Running flake8..."
|
|
uv run --no-sync flake8 src/ tests/ --count --select=E9,F63,F7,F82 --show-source --statistics
|
|
@echo "Checking black formatting..."
|
|
uv run --no-sync black --check --diff src/ tests/
|
|
@echo "Checking isort..."
|
|
uv run --no-sync isort --check-only --diff --profile black src/ tests/
|
|
|
|
# Auto-format code (black + isort)
|
|
format: _tooling
|
|
@echo "Formatting with black..."
|
|
uv run --no-sync black src/ tests/
|
|
@echo "Sorting imports with isort..."
|
|
uv run --no-sync isort --profile black src/ tests/
|
|
|
|
# Run type checking (mypy)
|
|
typecheck: _tooling
|
|
@echo "Running mypy..."
|
|
uv run --no-sync mypy src/
|
|
|
|
# Build distribution packages
|
|
build:
|
|
@echo "Building distribution packages..."
|
|
uv build
|
|
@echo "Built packages in dist/"
|
|
|
|
# Building on the host instead ships the host's glibc and GTK, which is how the
|
|
# published AppImage ended up unable to start on Debian 12 or Ubuntu 22.04.
|
|
#
|
|
# Build the AppImage as CI does, in the pinned base image (needs docker)
|
|
appimage: build
|
|
bash packaging/appimage/docker-build.sh dist/*.whl "$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py)" dist
|
|
|
|
# Start the built AppImage in a distro container, as the CI matrix does. Pick a
|
|
# distro older than the build image to test the glibc floor, or newer to test
|
|
# that the bundle does not break the host binaries it spawns.
|
|
#
|
|
# Boot the built AppImage on a distro, e.g. `just appimage-boot fedora:42`
|
|
appimage-boot distro="debian:12":
|
|
docker run --rm -v "$PWD/dist:/dist:ro" -v "$PWD/packaging/appimage:/pk:ro" {{distro}} bash /pk/boot-test.sh "/dist/$(basename "$(ls dist/*.AppImage)")"
|
|
|
|
# Build the .deb and .rpm in the pinned container, as release.yml does (needs
|
|
# docker). Thin packages: the distro supplies Python, GTK and the typelibs;
|
|
# only pywhispercpp and pynput are vendored.
|
|
native-packages: build
|
|
bash packaging/native/docker-build.sh dist/*.whl "$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py)" dist
|
|
|
|
# Install the built package in a matching distro container, as the CI matrix
|
|
# does: debian:12 / ubuntu:24.04 exercise the .deb, fedora:42 the .rpm.
|
|
native-smoke distro="debian:12":
|
|
docker run --rm -v "$PWD/dist:/out:ro" -v "$PWD/packaging/native:/pk:ro" {{distro}} bash /pk/smoke-test.sh
|
|
|
|
# Build the AUR package from this checkout on current Arch, as the CI gate
|
|
# does. Answers "does this commit build on Arch" — the tag tarball source= is
|
|
# swapped for a git archive of HEAD (needs docker).
|
|
#
|
|
# The checkout is mounted at its own path, and the main .git with it when this
|
|
# is a linked worktree: a worktree's .git is a pointer to a gitdir outside the
|
|
# tree, so git archive inside the container cannot see the objects without it.
|
|
aur-gate:
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
COMMON="$(cd "$(git rev-parse --git-common-dir)" && pwd)"
|
|
ARGS=(--rm -v "$PWD:$PWD:ro" -e REPO="$PWD")
|
|
if [ "$COMMON" != "$PWD/.git" ]; then
|
|
ARGS+=(-v "$COMMON:$COMMON:ro")
|
|
fi
|
|
docker run "${ARGS[@]}" archlinux:latest bash "$PWD/packaging/aur/build-test.sh"
|
|
|
|
# Run install.sh unattended in a distro container, as the CI gate does. Answers
|
|
# "does this commit install" (needs docker).
|
|
#
|
|
# The tree is taken via git archive of HEAD and extracted inside the container,
|
|
# so the venv local mode creates lands in the container's copy rather than in
|
|
# your working tree. Same worktree handling as aur-gate above.
|
|
#
|
|
# Usage: `just install-gate` for debian:12, or `just install-gate fedora:42`
|
|
install-gate distro="debian:12":
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
COMMON="$(cd "$(git rev-parse --git-common-dir)" && pwd)"
|
|
ARGS=(--rm -v "$PWD:$PWD:ro" -e REPO="$PWD")
|
|
if [ "$COMMON" != "$PWD/.git" ]; then
|
|
ARGS+=(-v "$COMMON:$COMMON:ro")
|
|
fi
|
|
docker run "${ARGS[@]}" {{distro}} bash "$PWD/scripts/install-test.sh"
|
|
|
|
# Check that a published release verifies as published: manifest, provenance,
|
|
# notes and PyPI digests. Needs gh, downloads nothing.
|
|
# Usage: `just verify-release` for the latest, or `just verify-release v0.17.0`
|
|
verify-release tag="":
|
|
python3 scripts/verify_release.py {{tag}}
|
|
|
|
# Regenerate uv.lock and the hash-pinned requirements/* exports.
|
|
# Bump the torch +cpu pin in requirements/whisper.in when you want a newer
|
|
# CPU build. OpenAI Whisper does not use torchaudio, whose CPU index can lag.
|
|
lock:
|
|
uv lock
|
|
uv export --only-group installer-build --no-emit-project -o requirements/installer-build.txt
|
|
uv export --no-dev --no-emit-project --no-emit-package pygobject -o requirements/runtime.txt
|
|
uv export --no-dev --extra vad --no-emit-project --no-emit-package pygobject -o requirements/vad.txt
|
|
# One export per selectable engine. install.sh installs each of these as an
|
|
# extra, so an extra without an export is an install path with nothing
|
|
# pinned: that is how parakeet, faster-whisper and vosk reached users.
|
|
# tests/test_dependency_exports.py enumerates the extras and fails when one
|
|
# has no export, rather than trusting this list to stay complete.
|
|
uv export --no-dev --extra vosk --no-emit-project --no-emit-package pygobject -o requirements/vosk.txt
|
|
uv export --no-dev --extra parakeet --no-emit-project --no-emit-package pygobject -o requirements/parakeet.txt
|
|
uv export --no-dev --extra faster-whisper --no-emit-project --no-emit-package pygobject -o requirements/faster-whisper.txt
|
|
# --group lint too: the linters live in a dependency group, not in the dev
|
|
# extra, so exporting the extra alone produced a file that reproduced
|
|
# neither `just deps` nor what CI lints with.
|
|
uv export --extra dev --group lint --no-emit-project --no-emit-package pygobject -o requirements/dev.txt
|
|
uv pip compile requirements/whisper.in --generate-hashes --emit-index-url \
|
|
--index-url https://pypi.org/simple \
|
|
--extra-index-url https://download.pytorch.org/whl/cpu \
|
|
--index-strategy unsafe-best-match --universal --python-version 3.11 \
|
|
-c requirements/runtime.txt -c requirements/installer-build.txt -o requirements/whisper.txt
|
|
# Compiled rather than exported: what the AppImage bundles on top of the
|
|
# lock, and what builds it, are pinned away from uv.lock on purpose --
|
|
# see requirements/appimage.in. --universal so one file covers both arches.
|
|
uv pip compile requirements/appimage.in --universal --no-deps --generate-hashes \
|
|
-o requirements/appimage.txt
|
|
uv pip compile requirements/appimage-tools.in --universal --no-deps --generate-hashes \
|
|
-o requirements/appimage-tools.txt
|
|
# The Flatpak is a fourth copy of the dependency set, and it drifted ten
|
|
# packages behind before anything compared it to source. Regenerate it here
|
|
# so a lock refresh cannot leave it behind again.
|
|
just flatpak-deps
|
|
|
|
# Fail if uv.lock is stale relative to pyproject.toml
|
|
lock-check:
|
|
uv lock --check
|
|
|
|
# Fail if any requirements/* export is behind uv.lock. Reads the `uv export`
|
|
# lines out of `lock` above and re-runs them, rather than keeping a second copy
|
|
# of that list. Offline: `uv export` reads the lock, so this needs no network.
|
|
export-check:
|
|
python3 scripts/check_exports.py
|
|
|
|
# Regenerate the shell package inventory consumed by install.sh. The committed
|
|
# output keeps the runtime installer independent of a YAML parser.
|
|
distro-packages: _tooling
|
|
uv run --no-sync python scripts/generate_distro_package_map.py
|
|
|
|
# Fail if install.d/package_map.sh is behind its YAML source.
|
|
distro-packages-check: _tooling
|
|
uv run --no-sync python scripts/generate_distro_package_map.py --check
|
|
|
|
# The export pins names, versions and digests; this looks up the URL that serves
|
|
# those bytes, so it needs PyPI. tests/test_flatpak_packaging.py checks the same
|
|
# invariant offline, which is what CI gates on.
|
|
# Point the Flatpak's dependency manifest at what uv.lock resolved.
|
|
flatpak-deps: _tooling
|
|
uv run --no-sync python scripts/sync_flatpak_deps.py
|
|
|
|
# Fail if the Flatpak manifest is behind requirements/runtime.txt.
|
|
flatpak-deps-check: _tooling
|
|
uv run --no-sync python scripts/sync_flatpak_deps.py --check
|
|
|
|
# Refresh the pinned model digests. whisper.cpp digests come from Hugging Face
|
|
# `lfs` metadata and cost no bandwidth; VOSK is pinned by the bytes we fetch, so
|
|
# every zip not already in the manifest is downloaded. A first run, or any run
|
|
# with --refresh, pulls ~21.9GB and takes ~30 minutes.
|
|
# Run after adding a model to vosk_model_info.py, whispercpp_model_info.py,
|
|
# parakeet_model_info.py or faster_whisper_model_info.py.
|
|
model-checksums: _tooling
|
|
uv run --no-sync python scripts/generate-model-checksums.py
|
|
|
|
# Remove build artifacts
|
|
clean:
|
|
@echo "Cleaning build artifacts..."
|
|
rm -rf build/
|
|
rm -rf dist/
|
|
rm -rf *.egg-info
|
|
rm -rf src/*.egg-info
|
|
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
|
find . -type f -name "*.pyc" -delete
|
|
find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true
|
|
find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true
|
|
rm -rf htmlcov/
|
|
rm -f .coverage
|
|
rm -f coverage.xml
|
|
@echo "Clean complete"
|
|
|
|
# Run the installed application
|
|
run:
|
|
vocalinux
|
|
|
|
# Run the installed application with debug logging
|
|
run-debug:
|
|
vocalinux --debug
|
|
|
|
# Run from source
|
|
run-source: _tooling
|
|
uv run --no-sync python -m vocalinux.main
|
|
|
|
# Run from source with debug logging
|
|
run-source-debug: _tooling
|
|
uv run --no-sync python -m vocalinux.main --debug
|
|
|
|
# Run pre-commit hooks on all files
|
|
pre-commit: _tooling
|
|
uv run --no-sync pre-commit run --all-files
|
|
|
|
# Print the current version
|
|
version:
|
|
@uv run --no-sync python -c "from src.vocalinux.version import __version__; print(__version__)"
|