fix(app): drop upstream references from user-facing source
The missing-dependency guidance now names the Void xbps package and this fork's install doc, instead of piping the upstream install script to bash (and drops the pipx line, since pipx is not a published channel). The IBus daemon hint points at the fork's docs; on niri it points at NIRI.md, because IBus is excluded there. The IBus component homepage is this fork's repository. VocaGateway keeps its upstream clone URL: Settings clones that separate project at its pinned tag at runtime. The URL is now recorded as a credit, and the gateway guide says the project is upstream's. The rename sweep no longer lets upstream slugs through under src/. Each upstream reference in source is a recorded line, matched exactly and once. Refs #16
This commit is contained in:
@@ -6,6 +6,9 @@ optional power-user path. It is **not** on-device processing: microphone audio
|
||||
still leaves the desktop client and is transcribed by the gateway process on
|
||||
your machine (or LAN).
|
||||
|
||||
VocaGateway is a separate upstream project by VocaHQ, not part of Voculinux.
|
||||
Voculinux does not publish it; the Settings action clones the pinned upstream tag.
|
||||
|
||||
The default speech engine remains **whisper.cpp** on-device. Enabling a local
|
||||
gateway never changes that default by itself. Use **Use this Gateway** only when
|
||||
you want Voculinux to talk to the gateway over the existing Remote API settings
|
||||
|
||||
@@ -4,6 +4,9 @@ This is an opt-in path for power users who want to run
|
||||
https://github.com/VocaHQ/vocagateway on the same Linux box via podman
|
||||
(or docker). It is not on-device processing: audio still leaves the
|
||||
desktop client and goes to the local gateway container.
|
||||
|
||||
VocaGateway is a separate upstream project, not part of Voculinux. This fork
|
||||
does not publish it; the helpers clone its pinned upstream tag.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
@@ -13,6 +13,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
# Keep pin explicit: never float on latest.
|
||||
GATEWAY_RELEASE_TAG = "v0.1.0"
|
||||
# VocaGateway is a separate upstream project, not part of Voculinux, and this fork
|
||||
# does not publish it. The URL is upstream's on purpose: the Settings action clones
|
||||
# this pinned tag from it. Do not point it at a fork.
|
||||
GATEWAY_GIT_URL = "https://github.com/VocaHQ/vocagateway.git"
|
||||
COMPOSE_PROJECT = "vocagateway"
|
||||
DEFAULT_PORT = 8765
|
||||
|
||||
@@ -256,14 +256,12 @@ def check_dependencies():
|
||||
"typelib-1_0-Notify-0_7 libnotify4"
|
||||
)
|
||||
logger.error("")
|
||||
logger.error(" Void Linux (signed xbps package, pulls in GTK and PyGObject):")
|
||||
logger.error(" Register the repository once, as described in:")
|
||||
logger.error(
|
||||
"For pipx users: Install system packages BEFORE running 'pipx install voculinux'"
|
||||
)
|
||||
logger.error("")
|
||||
logger.error("For the best experience, use the recommended installer:")
|
||||
logger.error(
|
||||
" curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh | bash"
|
||||
" https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs/INSTALL.md"
|
||||
)
|
||||
logger.error(" sudo xbps-install -Su voculinux")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@@ -106,7 +106,7 @@ except (ImportError, ValueError) as e:
|
||||
|
||||
# File paths for communication ($XDG_DATA_HOME, defaults to ~/.local/share).
|
||||
# Identity strings come from vocalinux/app_identity.py (see the import above);
|
||||
# the homepage below is the retained upstream URL, never renamed.
|
||||
# the homepage below is this fork's repository, not upstream's.
|
||||
VOCULINUX_IBUS_DIR = Path(xdg_data_home()) / IBUS_DIR_NAME
|
||||
SOCKET_PATH = VOCULINUX_IBUS_DIR / "inject.sock"
|
||||
PID_FILE = VOCULINUX_IBUS_DIR / "engine.pid"
|
||||
@@ -125,7 +125,7 @@ _COMPONENT_META = {
|
||||
"version": "1.0",
|
||||
"license": "AGPL-3.0",
|
||||
"author": APP_NAME,
|
||||
"homepage": "https://github.com/VocaHQ/vocalinux",
|
||||
"homepage": "https://git.bongbetic.com/xavierk/voculinux",
|
||||
"textdomain": IBUS_TEXTDOMAIN,
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,21 @@ def _kde_wayland_ibus_hint() -> str:
|
||||
)
|
||||
|
||||
|
||||
#: This fork's docs on Gitea. The upstream wiki describes a different project.
|
||||
_FORK_DOCS_URL = "https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs"
|
||||
|
||||
|
||||
def _ibus_daemon_missing_hint() -> str:
|
||||
"""Return where to look when the IBus daemon is not running.
|
||||
|
||||
niri excludes IBus (see ``TextInjector._wayland_compositor_bridges_ibus``), so
|
||||
there the hint names niri's notes rather than an IBus setup guide.
|
||||
"""
|
||||
if _is_niri_session():
|
||||
return f"IBus is excluded on niri; see {_FORK_DOCS_URL}/NIRI.md"
|
||||
return f"For IBus setup, see: {_FORK_DOCS_URL}/TROUBLESHOOTING.md"
|
||||
|
||||
|
||||
class DesktopEnvironment(Enum):
|
||||
"""Enum representing the desktop environment."""
|
||||
|
||||
@@ -977,8 +992,8 @@ class TextInjector:
|
||||
elif not force_ibus and not is_ibus_daemon_running():
|
||||
logger.info(
|
||||
"IBus daemon not running. This is normal on some desktop environments "
|
||||
"(e.g., KDE Plasma). Using alternative text injection method. "
|
||||
"For IBus setup, see: https://github.com/VocaHQ/vocalinux/wiki/IBus-Setup"
|
||||
"(e.g., KDE Plasma). Using alternative text injection method. %s",
|
||||
_ibus_daemon_missing_hint(),
|
||||
)
|
||||
# Some Wayland compositors (COSMIC, Sway, Hyprland, ...) do not deliver
|
||||
# IBus commits to native Wayland apps, so IBus would silently drop the
|
||||
|
||||
@@ -806,6 +806,10 @@ class TestVocalinuxEngineApplication(unittest.TestCase):
|
||||
|
||||
component_kwargs = mock_component_cls.call_args.kwargs
|
||||
self.assertIn("--ibus", component_kwargs["command_line"])
|
||||
# The component's homepage is this fork's repository, not upstream's.
|
||||
self.assertEqual(
|
||||
component_kwargs["homepage"], "https://git.bongbetic.com/xavierk/voculinux"
|
||||
)
|
||||
|
||||
@patch("vocalinux.text_injection.ibus_engine.VocalinuxEngine._start_socket_server")
|
||||
@patch("vocalinux.text_injection.ibus_engine.IBus.Factory.new")
|
||||
|
||||
@@ -224,6 +224,25 @@ class TestCheckDependencies(unittest.TestCase):
|
||||
)
|
||||
mock_logger.error.assert_any_call(" sudo apt install gnome-shell-extension-appindicator")
|
||||
|
||||
def test_check_dependencies_points_void_users_at_the_xbps_package(self):
|
||||
"""Voculinux ships only as a Void xbps package: the hint names it, no upstream script."""
|
||||
from vocalinux.main import check_dependencies
|
||||
|
||||
with patch("vocalinux.main.logger") as mock_logger:
|
||||
with patch("builtins.__import__", side_effect=ImportError("No module")):
|
||||
result = check_dependencies()
|
||||
|
||||
assert result is False
|
||||
logged = [call.args[0] for call in mock_logger.error.call_args_list if call.args]
|
||||
assert " sudo xbps-install -Su voculinux" in logged
|
||||
assert (
|
||||
" https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs/INSTALL.md"
|
||||
in logged
|
||||
)
|
||||
assert not any(
|
||||
marker in line for line in logged for marker in ("curl", "| bash", "pipx", "VocaHQ")
|
||||
)
|
||||
|
||||
|
||||
class TestCheckDisplayAvailable(unittest.TestCase):
|
||||
"""Tests for check_display_available() function."""
|
||||
|
||||
@@ -26,12 +26,17 @@ install message. On those, upstream references are not allowlisted by pattern
|
||||
at all. An upstream URL, handle or host is a regression unless the line is
|
||||
exactly the one credit line recorded for that file.
|
||||
|
||||
Under ``src/`` the slug and label rules are not allowlisted either. Every
|
||||
upstream reference in source is a recorded line (``SRC_UPSTREAM_CREDITS``),
|
||||
matched exactly and once, so a new one fails the source scan.
|
||||
|
||||
``tests/test_rename_sweep.py`` also checks the allowlist itself, so a loosened
|
||||
rule fails here before it lets an old name onto a user-visible surface.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from collections import Counter
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
SRC_PACKAGE = os.path.join(REPO_ROOT, "src", "vocalinux")
|
||||
@@ -116,6 +121,34 @@ FORK_SURFACES = frozenset(
|
||||
"packaging/void/srcpkgs/voculinux/INSTALL.msg",
|
||||
}
|
||||
)
|
||||
#: Upstream references under ``src/``, by the release checklist's pattern. Each
|
||||
#: hit must be a recorded line in SRC_UPSTREAM_CREDITS, not a pattern match.
|
||||
SRC_UPSTREAM_REFERENCE = re.compile(
|
||||
r"github\.com/(?:VocaHQ|jatinkrmalik)|vocahq|raw\.githubusercontent", re.IGNORECASE
|
||||
)
|
||||
#: Every upstream reference under src/, as stripped lines per file. The scan
|
||||
#: counts occurrences, so a duplicated or stale entry fails too.
|
||||
SRC_UPSTREAM_CREDITS: dict[str, frozenset[str]] = {
|
||||
# About-page credit (also the fork-surface credit above).
|
||||
"src/vocalinux/ui/settings_about.py": FORK_SURFACE_CREDITS[
|
||||
"src/vocalinux/ui/settings_about.py"
|
||||
],
|
||||
# Provenance record in RETAINED_SURFACES.
|
||||
"src/vocalinux/app_identity.py": frozenset(
|
||||
{'"https://github.com/VocaHQ/vocalinux (upstream URLs and provenance)",'}
|
||||
),
|
||||
# Docstring: the upstream repository is never queried.
|
||||
"src/vocalinux/utils/update_checker.py": frozenset(
|
||||
{"upstream VocaHQ repository is never queried, and packaged installs never query"}
|
||||
),
|
||||
# VocaGateway: a separate upstream project, cloned at its pinned tag.
|
||||
"src/vocalinux/gateway_embed/paths_embed.py": frozenset(
|
||||
{'GATEWAY_GIT_URL = "https://github.com/VocaHQ/vocagateway.git"'}
|
||||
),
|
||||
"src/vocalinux/gateway_embed/__init__.py": frozenset(
|
||||
{"https://github.com/VocaHQ/vocagateway on the same Linux box via podman"}
|
||||
),
|
||||
}
|
||||
#: Upstream hosts, handles and URL forms. Plain words such as "Flathub" are not
|
||||
#: listed, so a sentence that says a channel is not published stays legal.
|
||||
UPSTREAM_REFERENCE = re.compile(
|
||||
@@ -229,7 +262,12 @@ def _allowed(path: str, line: str) -> bool:
|
||||
if path == WEB_REBRAND_TEST and line.strip() in WEB_REBRAND_SCANNER_LINES:
|
||||
# The web rebrand test names the old word only to scan for it.
|
||||
return True
|
||||
if _UPSTREAM_SLUG.search(line) or _UPSTREAM_LABEL.search(line):
|
||||
if line.strip() in SRC_UPSTREAM_CREDITS.get(path, frozenset()):
|
||||
return True
|
||||
if not path.startswith("src/") and (
|
||||
_UPSTREAM_SLUG.search(line) or _UPSTREAM_LABEL.search(line)
|
||||
):
|
||||
# Upstream provenance on tests, docs, web and packaging. Source is pinned line by line.
|
||||
return True
|
||||
if _MODULE_PATH.search(line) or _DOTTED_MODULE.search(line):
|
||||
return True
|
||||
@@ -305,6 +343,45 @@ def test_fork_surfaces_carry_their_credit_exactly_once() -> None:
|
||||
assert lines.count(credit) == 1, f"{rel}: credit line must appear exactly once"
|
||||
|
||||
|
||||
def test_src_upstream_references_are_exactly_the_recorded_credits() -> None:
|
||||
"""Under src/, an upstream reference is allowed only on its recorded line, once each."""
|
||||
found: Counter[tuple[str, str]] = Counter()
|
||||
for rel, full in _iter_files():
|
||||
if not rel.startswith("src/") or any(
|
||||
part.endswith(SKIP_SUFFIXES) for part in rel.split("/")
|
||||
):
|
||||
continue
|
||||
try:
|
||||
with open(full, encoding="utf-8") as handle:
|
||||
text = handle.read()
|
||||
except (UnicodeDecodeError, ValueError):
|
||||
continue
|
||||
for line in text.splitlines():
|
||||
if SRC_UPSTREAM_REFERENCE.search(line):
|
||||
found[(rel, line.strip())] += 1
|
||||
expected: Counter[tuple[str, str]] = Counter(
|
||||
(rel, line) for rel, lines in SRC_UPSTREAM_CREDITS.items() for line in lines
|
||||
)
|
||||
unrecorded = sorted(f"{rel}: {line}" for rel, line in (found - expected).elements())
|
||||
missing = sorted(f"{rel}: {line}" for rel, line in (expected - found).elements())
|
||||
assert not unrecorded and not missing, (
|
||||
"src/ upstream references must match SRC_UPSTREAM_CREDITS exactly, once each.\n"
|
||||
f"unrecorded: {unrecorded}\nmissing: {missing}"
|
||||
)
|
||||
|
||||
|
||||
def test_gateway_docs_state_vocagateway_is_upstream() -> None:
|
||||
"""The gateway guide says VocaGateway is upstream's project, not Voculinux's."""
|
||||
path = os.path.join(REPO_ROOT, "docs", "GATEWAY_EMBED.md")
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
text = " ".join(handle.read().split())
|
||||
sentence = (
|
||||
"VocaGateway is a separate upstream project by VocaHQ, not part of Voculinux. "
|
||||
"Voculinux does not publish it; the Settings action clones the pinned upstream tag."
|
||||
)
|
||||
assert sentence in text
|
||||
|
||||
|
||||
def test_owned_web_files_carry_new_name() -> None:
|
||||
"""The two web rename surfaces name the fork (URLs aside)."""
|
||||
for rel in sorted(WEB_OWNED):
|
||||
@@ -337,6 +414,18 @@ def test_allowlist_denies_user_facing_old_names() -> None:
|
||||
("src/vocalinux/ui/tray_indicator.py", 'ICON = "vocalinux-microphone"'),
|
||||
("packaging/void/srcpkgs/voculinux/template", "pkgname=vocalinux"),
|
||||
("docs/INSTALL.md", "Open vocalinux.desktop from the menu."),
|
||||
(
|
||||
"src/vocalinux/main.py",
|
||||
'" curl -fsSL https://raw.githubusercontent.com/VocaHQ/vocalinux/main/install.sh | bash"',
|
||||
),
|
||||
(
|
||||
"src/vocalinux/text_injection/text_injector.py",
|
||||
'"For IBus setup, see: https://github.com/VocaHQ/vocalinux/wiki/IBus-Setup"',
|
||||
),
|
||||
(
|
||||
"src/vocalinux/text_injection/ibus_engine.py",
|
||||
'"homepage": "https://github.com/VocaHQ/vocalinux",',
|
||||
),
|
||||
]
|
||||
for path, line in denied:
|
||||
assert not _allowed(path, line), f"{path}: {line!r} must not be allowlisted"
|
||||
@@ -358,6 +447,10 @@ def test_allowlist_keeps_the_retained_surfaces() -> None:
|
||||
("tests/test_app_identity.py", "from vocalinux import app_identity"),
|
||||
("packaging/void/container-release.sh", 'grep -E "usr/share/applications/vocalinux"'),
|
||||
("install.sh", 'local PKG_NAME="vocalinux"'),
|
||||
(
|
||||
"src/vocalinux/app_identity.py",
|
||||
'"https://github.com/VocaHQ/vocalinux (upstream URLs and provenance)",',
|
||||
),
|
||||
]
|
||||
for path, line in kept:
|
||||
assert _allowed(path, line), f"{path}: {line!r} is a retained surface"
|
||||
|
||||
@@ -19,6 +19,7 @@ from unittest.mock import MagicMock, mock_open, patch
|
||||
from vocalinux.text_injection.text_injector import (
|
||||
DesktopEnvironment,
|
||||
TextInjector,
|
||||
_ibus_daemon_missing_hint,
|
||||
_warn_if_ydotool_globally_enabled,
|
||||
_ydotool_install_guidance,
|
||||
)
|
||||
@@ -3410,6 +3411,34 @@ class TestCompositorIBusBridging(unittest.TestCase):
|
||||
self.assertEqual(injector.wayland_tool, "portal")
|
||||
|
||||
|
||||
class TestIBusDaemonMissingHint(unittest.TestCase):
|
||||
"""The "IBus daemon not running" hint points at fork docs; niri gets its own notes."""
|
||||
|
||||
_SESSION_VARS = ("NIRI_SOCKET", "XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP", "DESKTOP_SESSION")
|
||||
_FORK_DOCS = "https://git.bongbetic.com/xavierk/voculinux/src/branch/main/docs"
|
||||
|
||||
def _hint_for(self, **session: str) -> str:
|
||||
env = {name: "" for name in self._SESSION_VARS} | session
|
||||
with mock.patch.dict(os.environ, env):
|
||||
return _ibus_daemon_missing_hint()
|
||||
|
||||
def test_niri_session_names_niri_notes_not_ibus_setup(self):
|
||||
self.assertEqual(
|
||||
self._hint_for(XDG_CURRENT_DESKTOP="niri"),
|
||||
f"IBus is excluded on niri; see {self._FORK_DOCS}/NIRI.md",
|
||||
)
|
||||
|
||||
def test_niri_socket_alone_counts_as_niri(self):
|
||||
hint = self._hint_for(NIRI_SOCKET="/run/user/1000/niri.wayland-1.sock")
|
||||
self.assertEqual(hint, f"IBus is excluded on niri; see {self._FORK_DOCS}/NIRI.md")
|
||||
|
||||
def test_other_sessions_point_at_the_fork_ibus_docs(self):
|
||||
self.assertEqual(
|
||||
self._hint_for(XDG_CURRENT_DESKTOP="GNOME"),
|
||||
f"For IBus setup, see: {self._FORK_DOCS}/TROUBLESHOOTING.md",
|
||||
)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _fake_config(config: Any) -> Iterator[None]:
|
||||
"""Pretend config.json holds ``config``; ``None`` means no file at all.
|
||||
|
||||
Reference in New Issue
Block a user