From 04eada775b08204b693af670ebcf1c0eaf1b51b8 Mon Sep 17 00:00:00 2001 From: Jatin K Malik Date: Mon, 9 Feb 2026 21:07:25 -0800 Subject: [PATCH] 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) --- .github/workflows/unified-pipeline.yml | 3 +- docs/DISTRO_COMPATIBILITY.md | 42 +++++ install.sh | 16 +- tests/test_cross_distro_compatibility.py | 195 +++++++++++++++++++++++ 4 files changed, 250 insertions(+), 6 deletions(-) create mode 100644 tests/test_cross_distro_compatibility.py diff --git a/.github/workflows/unified-pipeline.yml b/.github/workflows/unified-pipeline.yml index f32028e..f415125 100644 --- a/.github/workflows/unified-pipeline.yml +++ b/.github/workflows/unified-pipeline.yml @@ -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 diff --git a/docs/DISTRO_COMPATIBILITY.md b/docs/DISTRO_COMPATIBILITY.md index e43e944..ef7ffa2 100644 --- a/docs/DISTRO_COMPATIBILITY.md +++ b/docs/DISTRO_COMPATIBILITY.md @@ -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: diff --git a/install.sh b/install.sh index df34110..7b9c2ce 100755 --- a/install.sh +++ b/install.sh @@ -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" diff --git a/tests/test_cross_distro_compatibility.py b/tests/test_cross_distro_compatibility.py new file mode 100644 index 0000000..948fd0b --- /dev/null +++ b/tests/test_cross_distro_compatibility.py @@ -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"])