feat: Phase 5 & 6 - Complete cross-distro compatibility fixes (#168)
Phase 5: Fix remaining hardcoded GI_TYPELIB_PATH values - install.sh: Use dynamic GI_TYPELIB_DETECTED in package verification (line 1208) - install.sh: Use dynamic path in desktop entry Exec line (line 1651) - CI workflow: Use pkg-config for dynamic GI_TYPELIB_PATH detection (line 151) Phase 6: Testing & validation improvements - Add comprehensive test_cross_distro_compatibility.py test suite - Tests verify no hardcoded paths, dynamic detection works, wrapper scripts correct - Update DISTRO_COMPATIBILITY.md with implementation status and technical details Fixes #157 (remaining issues) Related to #48 (Snap packaging)
This commit is contained in:
@@ -148,7 +148,8 @@ jobs:
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
# Set GI_TYPELIB_PATH so pip-installed PyGObject can find system typelibs (AppIndicator3, etc.)
|
||||
export GI_TYPELIB_PATH=/usr/lib/x86_64-linux-gnu/girepository-1.0:/usr/lib/girepository-1.0
|
||||
# Use dynamic detection for cross-distro compatibility, fallback to common Ubuntu paths
|
||||
export GI_TYPELIB_PATH=$(pkg-config --variable=typelibdir gobject-introspection-1.0 2>/dev/null || echo "/usr/lib/x86_64-linux-gnu/girepository-1.0:/usr/lib/girepository-1.0")
|
||||
PYTHONPATH=$PWD pytest --junitxml=junit-${{ matrix.python-version }}.xml --cov=src --cov-report=xml tests/
|
||||
|
||||
- name: Upload test results
|
||||
|
||||
@@ -1,5 +1,47 @@
|
||||
# Linux Distribution Compatibility
|
||||
|
||||
## Implementation Status
|
||||
|
||||
The cross-distribution compatibility improvements have been implemented in phases:
|
||||
|
||||
| Phase | Status | Description |
|
||||
|-------|--------|-------------|
|
||||
| Phase 1 | ✅ Complete | Dynamic GI_TYPELIB_PATH detection using pkg-config, multi-arch support, ALSA library fallbacks |
|
||||
| Phase 2 | ✅ Complete | Enhanced distro detection (Gentoo, Alpine, Void, Solus, Mageia) |
|
||||
| Phase 3 | ✅ Complete | System dependency checker, improved error messages |
|
||||
| Phase 4 | ✅ Complete | CI test matrix, pkg-config as core dependency |
|
||||
| **Phase 5** | ✅ **Complete** | **Fixed remaining hardcoded GI_TYPELIB_PATH values in install.sh and CI workflow** |
|
||||
| **Phase 6** | ✅ **Complete** | **Added wrapper script verification tests, updated documentation** |
|
||||
| Phase 7 | 📋 Planned | Flatpak packaging for universal distribution support |
|
||||
| Phase 8 | 📋 Planned | Snap packaging for Ubuntu Software Store |
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Dynamic GI_TYPELIB_PATH Detection
|
||||
|
||||
The installer now uses a robust multi-step approach to detect the correct GI_TYPELIB_PATH across different distributions:
|
||||
|
||||
1. **Primary Method**: Uses `pkg-config --variable=typelibdir gobject-introspection-1.0` (most reliable)
|
||||
2. **Fallback Paths**: Checks common distribution-specific paths in priority order:
|
||||
- Ubuntu/Debian multi-arch: `/usr/lib/x86_64-linux-gnu/girepository-1.0`
|
||||
- ARM64: `/usr/lib/aarch64-linux-gnu/girepository-1.0`
|
||||
- Fedora/RHEL: `/usr/lib64/girepository-1.0`
|
||||
- Arch/Generic: `/usr/lib/girepository-1.0`
|
||||
- Local installs: `/usr/local/lib/girepository-1.0`
|
||||
|
||||
3. **Architecture Support**: Detected paths for x86_64, ARM64, ARMHF, RISC-V, POWER, and s390x
|
||||
|
||||
### Wrapper Scripts
|
||||
|
||||
The installer creates wrapper scripts (`~/.local/bin/vocalinux` and `~/.local/bin/vocalinux-gui`) that:
|
||||
- Set the correct `GI_TYPELIB_PATH` environment variable
|
||||
- Handle input group permissions for Wayland keyboard shortcuts
|
||||
- Provide seamless cross-distro compatibility
|
||||
|
||||
### Desktop Entry
|
||||
|
||||
The `.desktop` entry is automatically configured with the detected `GI_TYPELIB_PATH`, ensuring the application launches correctly from the application menu regardless of distribution.
|
||||
|
||||
## Officially Supported Distributions
|
||||
|
||||
These distributions are tested and known to work well with Vocalinux:
|
||||
|
||||
+11
-5
@@ -1201,11 +1201,18 @@ install_python_package() {
|
||||
local PIP_LOG_DIR=$(mktemp -d)
|
||||
local PIP_LOG_FILE="$PIP_LOG_DIR/pip_log.txt"
|
||||
|
||||
# Detect GI_TYPELIB_PATH early for cross-distro compatibility
|
||||
# This ensures the path is available for both verification and wrapper scripts
|
||||
local GI_TYPELIB_DETECTED
|
||||
GI_TYPELIB_DETECTED=$(detect_typelib_path)
|
||||
print_info "Detected GI_TYPELIB_PATH: $GI_TYPELIB_DETECTED"
|
||||
|
||||
# Function to verify package installation
|
||||
verify_package_installed() {
|
||||
local PKG_NAME="vocalinux"
|
||||
# Use venv python and set GI_TYPELIB_PATH for PyGObject
|
||||
GI_TYPELIB_PATH=/usr/lib/girepository-1.0 "$VENV_DIR/bin/python" -c "import $PKG_NAME" 2>/dev/null
|
||||
# Use the detected path for cross-distro compatibility
|
||||
GI_TYPELIB_PATH="$GI_TYPELIB_DETECTED" "$VENV_DIR/bin/python" -c "import $PKG_NAME" 2>/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
@@ -1381,9 +1388,7 @@ VOSK_INTERACTIVE_CONFIG
|
||||
# Clean up log file if installation was successful
|
||||
rm -rf "$PIP_LOG_DIR"
|
||||
|
||||
# Detect GI_TYPELIB_PATH for cross-distro compatibility
|
||||
GI_TYPELIB_DETECTED=$(detect_typelib_path)
|
||||
print_info "Detected GI_TYPELIB_PATH: $GI_TYPELIB_DETECTED"
|
||||
# GI_TYPELIB_PATH was already detected at the start of install_python_package
|
||||
|
||||
# Create wrapper scripts in ~/.local/bin for easy access
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
@@ -1648,7 +1653,8 @@ install_desktop_entry() {
|
||||
print_warning "Desktop entry may not work correctly"
|
||||
else
|
||||
# Update Exec line to include GI_TYPELIB_PATH for PyGObject
|
||||
sed -i "s|^Exec=vocalinux|Exec=env GI_TYPELIB_PATH=/usr/lib/girepository-1.0 $WRAPPER_SCRIPT|" "$DESKTOP_DIR/vocalinux.desktop" || {
|
||||
# Use the detected path for cross-distro compatibility
|
||||
sed -i "s|^Exec=vocalinux|Exec=env GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED $WRAPPER_SCRIPT|" "$DESKTOP_DIR/vocalinux.desktop" || {
|
||||
print_warning "Failed to update desktop entry path"
|
||||
}
|
||||
print_info "Updated desktop entry to use wrapper script with GI_TYPELIB_PATH"
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
"""
|
||||
Test cross-distro compatibility features.
|
||||
|
||||
This module tests the cross-distribution compatibility features including:
|
||||
- Dynamic GI_TYPELIB_PATH detection
|
||||
- Wrapper script generation with correct paths
|
||||
- Desktop entry configuration
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def get_repo_root() -> Path:
|
||||
"""Get the repository root directory."""
|
||||
# Start from current file and traverse up to find repo root
|
||||
current = Path(__file__).resolve()
|
||||
for parent in [current] + list(current.parents):
|
||||
# Check for common repo indicators
|
||||
if (parent / ".git").exists() or (parent / "install.sh").exists():
|
||||
return parent
|
||||
# Also check if we're in a 'vocalinux' subdirectory (CI structure)
|
||||
if parent.name == "vocalinux" and (parent.parent / "vocalinux" / "install.sh").exists():
|
||||
return parent.parent / "vocalinux"
|
||||
# Fallback: use parent of tests directory
|
||||
return current.parent.parent.parent
|
||||
|
||||
|
||||
REPO_ROOT = get_repo_root()
|
||||
|
||||
|
||||
class TestCrossDistroCompatibility:
|
||||
"""Test cross-distro compatibility features."""
|
||||
|
||||
@pytest.fixture
|
||||
def install_sh_content(self):
|
||||
"""Fixture to load install.sh content."""
|
||||
install_sh_path = REPO_ROOT / "install.sh"
|
||||
if not install_sh_path.exists():
|
||||
pytest.skip(f"install.sh not found at {install_sh_path}")
|
||||
return install_sh_path.read_text()
|
||||
|
||||
@pytest.fixture
|
||||
def workflow_content(self):
|
||||
"""Fixture to load workflow content."""
|
||||
workflow_path = REPO_ROOT / ".github" / "workflows" / "unified-pipeline.yml"
|
||||
if not workflow_path.exists():
|
||||
pytest.skip(f"Workflow file not found at {workflow_path}")
|
||||
return workflow_path.read_text()
|
||||
|
||||
def test_detect_typelib_path_function_exists(self, install_sh_content):
|
||||
"""Test that detect_typelib_path function exists in install.sh."""
|
||||
assert (
|
||||
"detect_typelib_path()" in install_sh_content
|
||||
), "detect_typelib_path function should exist"
|
||||
assert (
|
||||
"pkg-config --variable=typelibdir" in install_sh_content
|
||||
), "Should use pkg-config for detection"
|
||||
|
||||
def test_no_hardcoded_gi_typelib_path_in_verification(self, install_sh_content):
|
||||
"""Test that package verification uses dynamic GI_TYPELIB_PATH."""
|
||||
# The verification function should use the detected path, not hardcoded
|
||||
assert (
|
||||
"verify_package_installed()" in install_sh_content
|
||||
), "verify_package_installed function should exist"
|
||||
|
||||
# Check that it uses GI_TYPELIB_DETECTED variable
|
||||
verify_section = install_sh_content[
|
||||
install_sh_content.find("verify_package_installed()") : install_sh_content.find(
|
||||
"verify_package_installed()"
|
||||
)
|
||||
+ 500
|
||||
]
|
||||
assert (
|
||||
"GI_TYPELIB_DETECTED" in verify_section or '"$GI_TYPELIB_DETECTED"' in verify_section
|
||||
), "verify_package_installed should use GI_TYPELIB_DETECTED variable"
|
||||
|
||||
def test_wrapper_script_uses_detected_path(self, install_sh_content):
|
||||
"""Test that wrapper scripts use the detected GI_TYPELIB_PATH."""
|
||||
# Check that wrapper script creation uses GI_TYPELIB_DETECTED
|
||||
assert (
|
||||
"export GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED" in install_sh_content
|
||||
), "Wrapper script should export GI_TYPELIB_PATH with detected value"
|
||||
|
||||
def test_desktop_entry_uses_detected_path(self, install_sh_content):
|
||||
"""Test that desktop entry uses detected GI_TYPELIB_PATH."""
|
||||
# The sed command for desktop entry should use the detected path
|
||||
assert (
|
||||
"GI_TYPELIB_PATH=$GI_TYPELIB_DETECTED" in install_sh_content
|
||||
or "GI_TYPELIB_PATH=\${GI_TYPELIB_DETECTED}" in install_sh_content
|
||||
), "Desktop entry sed command should use GI_TYPELIB_DETECTED"
|
||||
|
||||
# Should NOT have hardcoded /usr/lib/girepository-1.0 in desktop entry section
|
||||
desktop_start = install_sh_content.find("install_desktop_entry()")
|
||||
if desktop_start == -1:
|
||||
pytest.skip("install_desktop_entry function not found")
|
||||
desktop_section = install_sh_content[desktop_start : desktop_start + 1000]
|
||||
if "sed -i" in desktop_section and "vocalinux.desktop" in desktop_section:
|
||||
assert (
|
||||
"/usr/lib/girepository-1.0"
|
||||
not in desktop_section.split("sed -i")[1].split("vocalinux.desktop")[0]
|
||||
), "Desktop entry should not hardcode /usr/lib/girepository-1.0 path"
|
||||
|
||||
def test_detect_typelib_path_with_pkgconfig(self, install_sh_content):
|
||||
"""Test that detect_typelib_path uses pkg-config when available."""
|
||||
# Extract the detect_typelib_path function
|
||||
func_start = install_sh_content.find("detect_typelib_path() {")
|
||||
if func_start == -1:
|
||||
pytest.skip("detect_typelib_path function not found")
|
||||
func_end = install_sh_content.find("}", func_start)
|
||||
func_content = install_sh_content[func_start:func_end]
|
||||
|
||||
# Should check for pkg-config first
|
||||
assert "pkg-config" in func_content, "Should check for pkg-config"
|
||||
assert "gobject-introspection-1.0" in func_content, "Should query gobject-introspection-1.0"
|
||||
|
||||
def test_detect_typelib_path_fallback_paths(self, install_sh_content):
|
||||
"""Test that detect_typelib_path has fallback paths for various distros."""
|
||||
# Should have fallback paths for different distros
|
||||
expected_paths = [
|
||||
"/usr/lib/x86_64-linux-gnu/girepository-1.0", # Ubuntu/Debian x86_64
|
||||
"/usr/lib/aarch64-linux-gnu/girepository-1.0", # ARM64
|
||||
"/usr/lib64/girepository-1.0", # Fedora/RHEL
|
||||
"/usr/lib/girepository-1.0", # Arch and fallback
|
||||
]
|
||||
|
||||
for path in expected_paths:
|
||||
assert path in install_sh_content, f"Should have fallback path: {path}"
|
||||
|
||||
def test_ci_workflow_uses_dynamic_detection(self, workflow_content):
|
||||
"""Test that CI workflow uses dynamic GI_TYPELIB_PATH detection."""
|
||||
# Should use pkg-config or dynamic detection
|
||||
assert (
|
||||
"pkg-config --variable=typelibdir" in workflow_content
|
||||
), "CI workflow should use pkg-config for GI_TYPELIB_PATH"
|
||||
|
||||
|
||||
class TestResourceManagerCrossDistro:
|
||||
"""Test ResourceManager cross-distro path handling."""
|
||||
|
||||
def test_resource_manager_has_xdg_support(self):
|
||||
"""Test that ResourceManager supports XDG directories."""
|
||||
from vocalinux.utils.resource_manager import ResourceManager
|
||||
|
||||
rm = ResourceManager()
|
||||
resources_dir = rm.resources_dir
|
||||
|
||||
# Should be a valid path string
|
||||
assert isinstance(resources_dir, str), "resources_dir should be a string"
|
||||
assert len(resources_dir) > 0, "resources_dir should not be empty"
|
||||
|
||||
|
||||
class TestRecognitionManagerCrossDistro:
|
||||
"""Test RecognitionManager cross-distro model path handling."""
|
||||
|
||||
def test_system_model_paths_exists(self):
|
||||
"""Test that system model paths configuration exists."""
|
||||
from vocalinux.speech_recognition.recognition_manager import SYSTEM_MODELS_DIRS
|
||||
|
||||
# Should be a list of paths
|
||||
assert isinstance(SYSTEM_MODELS_DIRS, list), "SYSTEM_MODELS_DIRS should be a list"
|
||||
assert len(SYSTEM_MODELS_DIRS) > 0, "SYSTEM_MODELS_DIRS should not be empty"
|
||||
|
||||
# Should include XDG paths
|
||||
has_xdg = any("/share/" in path for path in SYSTEM_MODELS_DIRS)
|
||||
assert has_xdg, "SYSTEM_MODELS_DIRS should include XDG paths"
|
||||
|
||||
|
||||
class TestDocumentation:
|
||||
"""Test that documentation is accurate."""
|
||||
|
||||
def test_distro_compatibility_documentation_exists(self):
|
||||
"""Test that DISTRO_COMPATIBILITY.md exists and is accurate."""
|
||||
docs_path = REPO_ROOT / "docs" / "DISTRO_COMPATIBILITY.md"
|
||||
|
||||
if not docs_path.exists():
|
||||
pytest.skip(f"DISTRO_COMPATIBILITY.md not found at {docs_path}")
|
||||
|
||||
content = docs_path.read_text()
|
||||
|
||||
# Should mention the dynamic path detection
|
||||
assert (
|
||||
"pkg-config" in content or "detect_typelib_path" in content
|
||||
), "Should document dynamic path detection"
|
||||
|
||||
# Should mention supported distros
|
||||
expected_distros = ["Ubuntu", "Debian", "Fedora", "Arch", "openSUSE"]
|
||||
for distro in expected_distros:
|
||||
assert distro in content, f"Should mention {distro} support"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user