feat(identity): rename user-visible identity to voculinux with one-time legacy import

The fork takes its own name everywhere a user can see it: binaries
(voculinux, voculinux-gui), desktop entry, autostart, tray, D-Bus
namespace (com.voculinux.Voculinux, no alias for the old name),
config/data/cache dirs, single-instance lock, IBus engine identity,
tray icons, resource prefixes, VOCULINUX_* environment names, and the
update-checker user agent. The internal Python module stays vocalinux
and the distribution metadata keeps the upstream URLs as provenance.

On first run a previous Vocalinux install's config.json, custom
dictionary, and portal restore token are copied once into the new
locations (new config always wins, no merge, no write-back); models
re-resolve through the pinned-checksum download path. An offline
naming-sweep test proves no old-name strings remain on user-visible
surfaces outside the allowlist.

Refs #18
This commit is contained in:
soubarnak
2026-11-06 01:29:39 +05:30
parent c565e6d946
commit ea7be5f111
251 changed files with 2449 additions and 1991 deletions
+22 -22
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Build vocalinux_<version>_<arch>.deb and vocalinux-<version>.<arch>.rpm.
# Build voculinux_<version>_<arch>.deb and voculinux-<version>.<arch>.rpm.
#
# Usage: build.sh <path-to-vocalinux-wheel> <version> [output-dir]
# Usage: build.sh <path-to-voculinux-wheel> <version> [output-dir]
# version release version, e.g. 0.18.0 or 0.19.0-beta
#
# CI and releases run this inside docker-build.sh's pinned Debian 12 image;
@@ -63,18 +63,18 @@ NFPM="$TOOLS/nfpm"
PAYLOAD="$TOOLS/payload"
DOC="$TOOLS/pkg-doc"
install -d \
"$PAYLOAD/usr/lib/vocalinux/app" \
"$PAYLOAD/usr/lib/vocalinux/vendor" \
"$PAYLOAD/usr/lib/voculinux/app" \
"$PAYLOAD/usr/lib/voculinux/vendor" \
"$PAYLOAD/usr/bin" \
"$PAYLOAD/usr/share/applications" \
"$PAYLOAD/usr/share/icons/hicolor/scalable/apps" \
"$DOC"
# --- application payload ------------------------------------------------
# The wheel is a zip; -m zipfile lays out vocalinux/ and its dist-info
# The wheel is a zip; -m zipfile lays out voculinux/ and its dist-info
# exactly where pip --target would, without pip's EXTERNALLY-MANAGED refusal
# on Debian 12.
python3 -m zipfile -e "$WHEEL" "$PAYLOAD/usr/lib/vocalinux/app/"
python3 -m zipfile -e "$WHEEL" "$PAYLOAD/usr/lib/voculinux/app/"
# --- vendored wheels -----------------------------------------------------
# Download each supported CPython's pywhispercpp wheel — it has no abi3 tag,
@@ -99,7 +99,7 @@ for PYV in $VENDOR_PYS; do
--python-version "$PYV" -r "$PINS_VENDOR" -d "$TOOLS/wheels/$PYV" >/dev/null
done
for VENDOR_WHEEL in "$TOOLS"/wheels/*/*.whl; do
python3 -m zipfile -e "$VENDOR_WHEEL" "$PAYLOAD/usr/lib/vocalinux/vendor/"
python3 -m zipfile -e "$VENDOR_WHEEL" "$PAYLOAD/usr/lib/voculinux/vendor/"
done
# --- portable libggml-cpu -------------------------------------------------
@@ -144,7 +144,7 @@ swapped=0
while IFS= read -r vendored; do
cp "$BASELINE_CPU" "$vendored"
swapped=$((swapped + 1))
done < <(find "$PAYLOAD/usr/lib/vocalinux/vendor" -name 'libggml-cpu*.so*')
done < <(find "$PAYLOAD/usr/lib/voculinux/vendor" -name 'libggml-cpu*.so*')
[ "$swapped" -gt 0 ] || fail "found no vendored libggml-cpu to replace"
# The baseline library's own NEEDED entries use the unmangled sonames
# (libggml-base.so.0, libgomp.so.1), so alias the vendored copies — the same
@@ -157,30 +157,30 @@ while IFS= read -r libs_dir; do
vendored_gomp="$(find "$libs_dir" -name 'libgomp*.so*' | head -1)"
[ -n "$vendored_gomp" ] || fail "no vendored libgomp in $libs_dir"
cp "$vendored_gomp" "$libs_dir/libgomp.so.1"
done < <(find "$PAYLOAD/usr/lib/vocalinux/vendor" -type d -name 'pywhispercpp.libs')
done < <(find "$PAYLOAD/usr/lib/voculinux/vendor" -type d -name 'pywhispercpp.libs')
# --- launchers -----------------------------------------------------------
# Same environment install.sh's wrapper establishes: the vendored
# pywhispercpp.libs on LD_LIBRARY_PATH, user site-packages off so they cannot
# shadow the distro modules, and sg input so evdev hotkeys work on Wayland
# when the user is already in the input group but the session has not picked
# up the membership yet. vocalinux-gui exists because the wheel declares a
# up the membership yet. voculinux-gui exists because the wheel declares a
# gui-script of that name; on Linux it runs the same entry.
write_launcher() {
local path="$1"
cat > "$path" <<'LAUNCHER_EOF'
#!/bin/sh
# Launcher for the distro-packaged Vocalinux. The vendored wheels (the
# Launcher for the distro-packaged Voculinux. The vendored wheels (the
# pywhispercpp/pynput copies Debian and Fedora do not ship) live under
# /usr/lib/vocalinux/vendor with the app itself under .../app; the loader in
# /usr/lib/voculinux/vendor with the app itself under .../app; the loader in
# utils/pywhispercpp_loader.py finds the bundled .so files from sys.path.
PYTHONNOUSERSITE=1
VOCALINUX_VENDOR="/usr/lib/vocalinux/vendor"
if [ -d "$VOCALINUX_VENDOR/pywhispercpp.libs" ]; then
LD_LIBRARY_PATH="$VOCALINUX_VENDOR/pywhispercpp.libs${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
VOCULINUX_VENDOR="/usr/lib/voculinux/vendor"
if [ -d "$VOCULINUX_VENDOR/pywhispercpp.libs" ]; then
LD_LIBRARY_PATH="$VOCULINUX_VENDOR/pywhispercpp.libs${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
export LD_LIBRARY_PATH
fi
export PYTHONNOUSERSITE PYTHONPATH="/usr/lib/vocalinux/app:$VOCALINUX_VENDOR${PYTHONPATH:+:$PYTHONPATH}"
export PYTHONNOUSERSITE PYTHONPATH="/usr/lib/voculinux/app:$VOCULINUX_VENDOR${PYTHONPATH:+:$PYTHONPATH}"
if grep -q "^input:.*\b$(whoami)\b" /etc/group 2>/dev/null \
&& ! groups | grep -q "\binput\b" && command -v sg >/dev/null 2>&1; then
# sg(1) takes a single command string, so each argument is re-quoted for
@@ -196,14 +196,14 @@ exec /usr/bin/python3 -m vocalinux.main "$@"
LAUNCHER_EOF
chmod 755 "$path"
}
write_launcher "$PAYLOAD/usr/bin/vocalinux"
write_launcher "$PAYLOAD/usr/bin/vocalinux-gui"
write_launcher "$PAYLOAD/usr/bin/voculinux"
write_launcher "$PAYLOAD/usr/bin/voculinux-gui"
# --- desktop entry, icon, license ----------------------------------------
install -Dm644 "$REPO_ROOT/vocalinux.desktop" \
"$PAYLOAD/usr/share/applications/vocalinux.desktop"
install -Dm644 "$REPO_ROOT/resources/icons/scalable/vocalinux.svg" \
"$PAYLOAD/usr/share/icons/hicolor/scalable/apps/vocalinux.svg"
install -Dm644 "$REPO_ROOT/voculinux.desktop" \
"$PAYLOAD/usr/share/applications/voculinux.desktop"
install -Dm644 "$REPO_ROOT/resources/icons/scalable/voculinux.svg" \
"$PAYLOAD/usr/share/icons/hicolor/scalable/apps/voculinux.svg"
install -Dm644 "$REPO_ROOT/LICENSE" "$DOC/LICENSE"
# --- package -------------------------------------------------------------
+7 -7
View File
@@ -8,8 +8,8 @@
# the gi typelibs come from the distribution, and so do every Python
# dependency the distros package. Only pywhispercpp and pynput are vendored —
# pywhispercpp is not packaged by Debian or Fedora at all, and pynput is
# missing from Fedora — both under /usr/lib/vocalinux/vendor/, on sys.path via
# the launcher in /usr/bin/vocalinux. Vendoring the pywhispercpp wheel per
# missing from Fedora — both under /usr/lib/voculinux/vendor/, on sys.path via
# the launcher in /usr/bin/voculinux. Vendoring the pywhispercpp wheel per
# supported CPython is what keeps the package free of a bundled interpreter.
#
# Version: VOCA_VERSION is the tag without its leading v and without a
@@ -17,7 +17,7 @@
# nfpm maps a prerelease to 0.19.0~beta-1, which orders before the stable
# 0.19.0-1 in both formats.
name: vocalinux
name: voculinux
arch: ${VOCA_ARCH}
platform: linux
version: ${VOCA_VERSION}
@@ -31,12 +31,12 @@ description: |
transcribe speech locally (whisper.cpp by default), and type into the
focused window. GTK, PyGObject and the interpreter come from the
distribution; pywhispercpp and pynput are vendored under
/usr/lib/vocalinux/vendor because neither distro packages them.
/usr/lib/voculinux/vendor because neither distro packages them.
homepage: https://github.com/VocaHQ/vocalinux
license: AGPL-3.0-only
contents:
# The staged payload tree: /usr/lib/vocalinux/{app,vendor}, the launchers in
# The staged payload tree: /usr/lib/voculinux/{app,vendor}, the launchers in
# /usr/bin, the desktop file and the hicolor icon. build.sh writes it.
- src: ./payload/
dst: /
@@ -45,10 +45,10 @@ contents:
# License placement differs between the formats: Debian policy wants the
# copyright file under /usr/share/doc, Fedora marks /usr/share/licenses.
- src: ./pkg-doc/LICENSE
dst: /usr/share/doc/vocalinux/copyright
dst: /usr/share/doc/voculinux/copyright
packager: deb
- src: ./pkg-doc/LICENSE
dst: /usr/share/licenses/vocalinux/LICENSE
dst: /usr/share/licenses/voculinux/LICENSE
packager: rpm
overrides:
+6 -6
View File
@@ -52,17 +52,17 @@ echo "== installed: $PKG"
# before check_dependencies() touches GTK, so it proves the wrapper, the
# PYTHONPATH layout and the entry module's import chain — the dependency
# typelibs are exercised by the import smoke below.
vocalinux --version
vocalinux-gui --version
voculinux --version
voculinux-gui --version
# Import smoke in the shape of the AUR gate: the app modules, the vendored
# pywhispercpp wheel, and the distro gi/GTK typelibs, all through the same
# interpreter and environment the launcher sets up. pynput is only checked
# for presence: its __init__ selects a display backend and raises ImportError
# with no DISPLAY, which is why the app guards it with PYNPUT_AVAILABLE.
PYTHONPATH="/usr/lib/vocalinux/app:/usr/lib/vocalinux/vendor" \
PYTHONPATH="/usr/lib/voculinux/app:/usr/lib/voculinux/vendor" \
PYTHONNOUSERSITE=1 \
LD_LIBRARY_PATH="/usr/lib/vocalinux/vendor/pywhispercpp.libs" \
LD_LIBRARY_PATH="/usr/lib/voculinux/vendor/pywhispercpp.libs" \
/usr/bin/python3 - <<'PY'
import importlib
import importlib.util
@@ -83,11 +83,11 @@ for module in MODULES:
importlib.import_module(module)
spec = importlib.util.find_spec("pynput")
assert spec and spec.origin and spec.origin.startswith("/usr/lib/vocalinux/vendor/"), \
assert spec and spec.origin and spec.origin.startswith("/usr/lib/voculinux/vendor/"), \
f"vendored pynput not found on sys.path: {spec}"
tag = f"cpython-{sys.version_info.major}{sys.version_info.minor}-"
exts = [p.name for p in Path("/usr/lib/vocalinux/vendor").glob("_pywhispercpp.*.so")]
exts = [p.name for p in Path("/usr/lib/voculinux/vendor").glob("_pywhispercpp.*.so")]
assert any(tag in name for name in exts), \
f"no _pywhispercpp extension for {tag[:-1]} among {exts}"