diff --git a/docs/DISTRO_COMPATIBILITY.md b/docs/DISTRO_COMPATIBILITY.md index a34f2df..227714b 100644 --- a/docs/DISTRO_COMPATIBILITY.md +++ b/docs/DISTRO_COMPATIBILITY.md @@ -198,8 +198,9 @@ sudo xbps-install -Sy python3-pip python3-gobject gtk+3 libayatana-appindicator ``` Note: `ydotool` is not packaged for Void, so the ydotool injection -fallback rung is unavailable there; the app uses IBus, the RemoteDesktop -portal, `wtype` or `xdotool` instead. +fallback rung is unavailable there. On niri, text is typed directly through +`wtype` with no permission prompt. IBus is not used on niri. On GNOME and +KDE the RemoteDesktop portal is tried first where it is available. ### Solus ```bash diff --git a/packaging/void/container-main.sh b/packaging/void/container-main.sh index 930bc94..d980cd1 100755 --- a/packaging/void/container-main.sh +++ b/packaging/void/container-main.sh @@ -61,9 +61,8 @@ voculinux) echo "== version and import smoke $PKGNAME" voculinux --version cat > /voculinux-smoke.py <<'PYEOF' -# The 0.18.5 sdist predates the rename: the importable top-level package -# and the distribution metadata still carry the old name. The first -# release cut after the rename moves these to voculinux. +# The importable package keeps its retained internal name (vocalinux); the +# distribution metadata is the fork's own name (voculinux). import importlib MODULES = ( @@ -81,7 +80,7 @@ for module in MODULES: print("import-smoke OK:", ", ".join(MODULES)) from importlib.metadata import version -print("vocalinux", version("vocalinux")) +print("voculinux", version("voculinux")) PYEOF python3 /voculinux-smoke.py ;; diff --git a/packaging/void/srcpkgs/voculinux/INSTALL.msg b/packaging/void/srcpkgs/voculinux/INSTALL.msg index ea96da2..fead780 100644 --- a/packaging/void/srcpkgs/voculinux/INSTALL.msg +++ b/packaging/void/srcpkgs/voculinux/INSTALL.msg @@ -6,16 +6,23 @@ then log out and back in, or the evdev hotkey backend cannot open /dev/input devices. Speech models are not in this package: they -download on first run into ~/.local/share/voculinux. +download on first run into ~/.local/share/voculinux +and are checked against pinned checksums. Optional engines are manual additions outside the guaranteed set: vosk, OpenAI whisper, Parakeet (sherpa-onnx), faster-whisper and onnxruntime (VAD) come from your own Python environment, not xbps. +Text injection: on niri there is no permission +prompt, and typing goes direct through wtype. On +GNOME and KDE the RemoteDesktop portal asks once +for keyboard access and remembers the answer where +the desktop supports it. If you deny it, dictation +falls back to the next typing method you have. + ydotool is not packaged for Void, so the ydotool -injection fallback rung is absent; the app uses -IBus, the RemoteDesktop portal, wtype or xdotool. +injection fallback rung is absent. Update through your package manager instead of the in-app release downloads: diff --git a/packaging/void/srcpkgs/voculinux/template b/packaging/void/srcpkgs/voculinux/template index 9871159..fdc331a 100644 --- a/packaging/void/srcpkgs/voculinux/template +++ b/packaging/void/srcpkgs/voculinux/template @@ -7,41 +7,32 @@ revision=1 build_style=python3-pep517 hostmakedepends="python3-setuptools" makedepends="python3-devel" -depends="python3 python3-gobject python3-cairo gtk+3 libayatana-appindicator ibus gobject-introspection python3-numpy python3-requests python3-pysocks python3-psutil python3-evdev python3-xlib python3-tqdm python3-platformdirs python3-PyAudio python3-pynput python3-pywhispercpp" +# Runtime helpers: pipewire (pw-record, pw-dump) for capture, wtype for +# direct typing on niri, wl-clipboard (wl-copy, wl-paste) for clipboard +# paths, xdotool for X11 and the XWayland fallback, pulseaudio-utils for +# pactl source listing. ibus is not a dependency: it is excluded on niri. +depends="python3 python3-gobject python3-cairo gtk+3 libayatana-appindicator gobject-introspection python3-numpy python3-requests python3-pysocks python3-psutil python3-evdev python3-xlib python3-tqdm python3-platformdirs python3-PyAudio python3-pynput python3-pywhispercpp pipewire wtype wl-clipboard xdotool pulseaudio-utils" short_desc="Free, offline voice dictation for Linux" -maintainer="Jatin K Malik " +maintainer="Xavier Karma " license="AGPL-3.0-only" -homepage="https://github.com/VocaHQ/vocalinux" -distfiles="https://github.com/VocaHQ/vocalinux/releases/download/v${version}/vocalinux-${version}.tar.gz" -checksum=1def4d8f0f14f2df2f2fee64ca29e6865e3d82e551f7d465c2a78ec6d4d72b1d -# The 0.18.5 release asset still carries the pre-rename archive root. -# xbps-src moves a single-top-dir archive to pkgname-version itself, so no -# wrksrc override is needed (wrksrc is deprecated and xlint rejects it). -# The first release cut after the rename ships the archive under the new -# name; that bump points distfiles at the renamed asset with its digest. +homepage="https://git.bongbetic.com/xavierk/voculinux" +distfiles="https://git.bongbetic.com/xavierk/voculinux/releases/download/v${version}/voculinux-${version}.tar.gz" +# Pending: set to the sha256 of voculinux-${version}.tar.gz once the fork's +# v${version} release exists (see packaging/void/README.md, "Checksum"). +# Until then xbps-src fails closed on this value; it never falls back to the +# upstream archive. +checksum=PENDING_FORK_RELEASE_SHA256 post_install() { - # The 0.18.5 sdist predates the rename: the entry and icons still - # carry the old names. Promote them at install time — the entry lands - # as voculinux.desktop with rewritten Name/Exec/Icon, old-prefixed - # icons land renamed (vinstall takes the installed name, as vman - # uses it). The first release cut after the rename ships the new - # names; that bump drops the renames. Entry points land on PATH - # through the wheel build; the marker is what the app reads to + # The fork's sdist already names the desktop entry, icons and launchers + # voculinux (pyproject entry points and the repo's voculinux.desktop), so + # the files install as they are. The marker is what the app reads to # present package-manager updates (see INSTALL.msg). vlicense LICENSE - sed -e 's/vocalinux/voculinux/g' -e 's/Vocalinux/Voculinux/g' \ - vocalinux.desktop > voculinux.desktop vinstall voculinux.desktop 644 usr/share/applications for icon in resources/icons/scalable/*.svg; do - vinstall "$icon" 644 usr/share/icons/hicolor/scalable/apps \ - "$(basename "$icon" | sed 's/^vocalinux/voculinux/')" + vinstall "$icon" 644 usr/share/icons/hicolor/scalable/apps done - # The wheel's entry points still carry the old names: promote the - # launchers (this runs before the provides hooks, so cmd:voculinux - # is what the package provides). - mv "${DESTDIR}/usr/bin/vocalinux" "${DESTDIR}/usr/bin/voculinux" - mv "${DESTDIR}/usr/bin/vocalinux-gui" "${DESTDIR}/usr/bin/voculinux-gui" vmkdir usr/share/voculinux touch "${DESTDIR}/usr/share/voculinux/packaged" } diff --git a/tests/test_void_app_template.py b/tests/test_void_app_template.py index 934462c..490c03a 100644 --- a/tests/test_void_app_template.py +++ b/tests/test_void_app_template.py @@ -60,10 +60,32 @@ EXTRA_VOID_DEPS = ( "python3", "gtk+3", "libayatana-appindicator", - "ibus", "gobject-introspection", ) +#: Command-line helpers the app runs at runtime, and the Void package each +#: comes from. The release gate needs them on a packaged install (ticket +#: #27; spec #16 "Void packaging"). ydotool is deliberately absent: it is not +#: packaged for Void, so its fallback rung does not exist there. +RUNTIME_HELPER_PACKAGES = { + "pipewire": "pw-record and pw-dump for PipeWire capture", + "wtype": "direct keysym typing, the first rung on niri", + "wl-clipboard": "wl-copy and wl-paste for the clipboard paths", + "xdotool": "X11 and XWayland injection", + "pulseaudio-utils": "pactl source listing", +} + +#: The checksum the template may carry before the fork's release exists. It is +#: explicit and fails closed in xbps-src; it is never the upstream digest. +PENDING_CHECKSUM = "PENDING_FORK_RELEASE_SHA256" + +#: sha256 of the upstream 0.18.5 archive. The template must never pin it: the +#: package is the fork's own release, not a copy of upstream's. +UPSTREAM_0_18_5_SHA256 = "1def4d8f0f14f2df2f2fee64ca29e6865e3d82e551f7d465c2a78ec6d4d72b1d" + +#: The fork's release host. Every distfile and the homepage live here. +FORK_RELEASE_HOST = "https://git.bongbetic.com/xavierk/voculinux" + #: Every package name the app template may reference, verified to exist in #: void-packages master on 2026-11-06 via the srcpkgs//template #: paths (python3-devel is a subpackage of the python3 template, as @@ -78,8 +100,12 @@ VERIFIED_VOID_PACKAGES = frozenset( "python3-cairo", "gtk+3", "libayatana-appindicator", - "ibus", "gobject-introspection", + "pipewire", + "wtype", + "wl-clipboard", + "xdotool", + "pulseaudio-utils", "python3-numpy", "python3-requests", "python3-pysocks", @@ -208,7 +234,7 @@ def test_template_metadata_shape() -> None: assert re.fullmatch(r".+ <[^@<>\s]+@[^@<>\s]+>", variables["maintainer"]) assert "noreply" not in variables["maintainer"] assert variables["license"] == "AGPL-3.0-only" - assert variables["homepage"] == "https://github.com/VocaHQ/vocalinux" + assert variables["homepage"] == FORK_RELEASE_HOST assert len(variables["short_desc"]) <= 72 assert re.fullmatch(r"[0-9][A-Za-z0-9.]*", variables["version"]) @@ -219,29 +245,35 @@ def test_template_version_matches_version_py() -> None: def test_distfiles_sources_the_canonical_release_by_checksum() -> None: - """One release-asset distfile, checksum-pinned; never a git snapshot.""" + """One fork release-asset distfile, checksum-verified; never upstream or git.""" variables = _template_vars() distfiles = variables["distfiles"] version = variables["version"] - assert ( - "https://github.com/VocaHQ/vocalinux/releases/download/v${version}/" in distfiles - ), "distfiles must use the canonical release distribution for this version" - assert distfiles.rstrip().endswith(".tar.gz"), "the release payload is the sdist tarball" + assert distfiles == ( + f"{FORK_RELEASE_HOST}/releases/download/v${{version}}/" + f"{variables['pkgname']}-${{version}}.tar.gz" + ), "distfiles must be the fork's own release sdist for this version" + assert "github.com" not in distfiles and "VocaHQ" not in distfiles, "no upstream archive" for moving in ("/master", "/main", "/archive/", "git+"): assert moving not in distfiles, f"distfiles must not track a moving ref: {moving}" - assert re.fullmatch(r"[0-9a-f]{64}", variables["checksum"]), "checksum must be sha256" - # The unpacked top-level directory must be where the template builds. - # xbps-src moves a single-top-dir archive to pkgname-version itself - # (common/hooks/do-extract/00-distfiles.sh), so the pre-rename archive - # root needs no override — and wrksrc is deprecated, xlint rejects it. - root = Path(distfiles.rsplit("/", 1)[1].replace("${version}", version)) - assert root.suffixes[-2:] == [".tar", ".gz"] - assert ( - root.name[: -len(".tar.gz")] != f"{variables['pkgname']}-{version}" - ), "the release asset is still rooted under the pre-rename archive name" + assert distfiles.rstrip().endswith(".tar.gz"), "the release payload is the sdist tarball" + # The sdist's top-level directory is pkgname-version, so xbps-src needs no + # wrksrc override (deprecated; xlint rejects it). + root = distfiles.rsplit("/", 1)[1].replace("${version}", version) + assert root == f"{variables['pkgname']}-{version}.tar.gz", "archive name must match pkgname" assert "wrksrc" not in variables, "deprecated wrksrc override: xlint fails the template" +def test_checksum_is_pinned_to_the_fork_release_or_explicitly_pending() -> None: + """A 64-hex digest once the fork release exists; until then, the explicit + pending marker. Never the upstream digest, never empty.""" + checksum = _template_vars()["checksum"] + assert checksum != UPSTREAM_0_18_5_SHA256, "the template must not pin the upstream archive" + assert ( + re.fullmatch(r"[0-9a-f]{64}", checksum) or checksum == PENDING_CHECKSUM + ), f"checksum must be a sha256 or the explicit pending marker, not {checksum!r}" + + def test_every_project_dependency_is_covered() -> None: """pyproject.toml runtime deps plus substrate must all resolve to depends.""" depends = set(_template_vars()["depends"].split()) @@ -297,22 +329,30 @@ def test_package_ships_no_speech_models() -> None: assert "model" not in code, "models are not package payload" -def test_post_install_installs_the_renamed_paths_from_the_old_sdist() -> None: - """The 0.18.5 sdist predates the rename, so post_install promotes the - old names at install time: the entry lands as voculinux.desktop with - rewritten Name/Exec/Icon, and old-prefixed icons land renamed.""" +def test_post_install_installs_the_fork_names_without_renaming() -> None: + """The fork's sdist already carries the new names, so post_install installs + them as they are: no sed rewrite and no mv of upstream launchers.""" body = _post_install_body() assert "vlicense LICENSE" in body - assert "vocalinux.desktop" in body, "the sdist entry still carries the old name" - assert "voculinux.desktop" in body and "usr/share/applications" in body - assert "s/Vocalinux/Voculinux/" in body, "the entry Name must be rewritten" - assert "s/vocalinux/voculinux/" in body, "the entry Exec/Icon must be rewritten" + assert "vinstall voculinux.desktop 644 usr/share/applications" in body assert "resources/icons/scalable" in body assert "usr/share/icons/hicolor/scalable/apps" in body - assert "s/^vocalinux/voculinux/" in body, "old-prefixed icons must land renamed" - assert "usr/bin/vocalinux" in body, "the sdist launchers still carry the old names" - assert "usr/bin/voculinux" in body, "the launchers must land under the new names" assert "usr/share/voculinux/packaged" in body + for upstream_step in ("sed ", " mv ", "vocalinux"): + assert ( + upstream_step not in body + ), f"post_install still renames upstream names: {upstream_step}" + assert "usr/bin/vocalinux" not in _template_text(), "no upstream launcher is promoted" + + +def test_runtime_helpers_are_declared_and_ibus_is_not() -> None: + """Capture, direct typing, the clipboard and X11 helpers come from xbps; + ibus is excluded because it is not an answer on niri (spec #16).""" + depends = set(_template_vars()["depends"].split()) + for package, why in RUNTIME_HELPER_PACKAGES.items(): + assert package in depends, f"depends must pull in {package} ({why})" + assert "ibus" not in depends, "ibus is excluded: it cannot inject on niri" + assert "ydotool" not in depends, "ydotool is not packaged for Void" def test_build_fetches_nothing_beyond_the_sdist() -> None: @@ -323,11 +363,11 @@ def test_build_fetches_nothing_beyond_the_sdist() -> None: def test_no_verification_disabling_options() -> None: - """Dependency verification stays on; distfiles stay checksum-pinned.""" + """Dependency verification stays on; distfiles stay checksum-bound.""" variables = _template_vars() for key in FORBIDDEN_VARS: assert key not in variables, f"template sets {key}" - assert re.fullmatch(r"[0-9a-f]{64}", variables["checksum"]) + assert "checksum" in variables, "the distfile must carry a checksum line" def test_template_style_is_lint_clean() -> None: @@ -348,6 +388,15 @@ def test_install_msg_covers_the_xbps_only_facts() -> None: for engine in ("vosk", "whisper", "parakeet", "faster-whisper", "onnxruntime"): assert engine in text, f"optional-engine status must name {engine}" assert "ydotool" in text, "the absent injection helper must be named" + assert "no permission" in text, "niri first run has no permission prompt" + assert "wtype" in text, "niri types direct through wtype" + assert "ibus" not in text, "IBus is excluded; the message must not point at it" + sentences = re.split(r"(?<=\.)\s+", " ".join(text.split())) + niri_sentences = [sentence for sentence in sentences if "niri" in sentence] + assert niri_sentences, "the message must describe the niri first run" + assert not any( + "portal" in sentence for sentence in niri_sentences + ), "the portal is never recommended on niri" def test_install_msg_line_length() -> None: