#!/usr/bin/env python3 """ Alternative dependency installer that tries apt packages first, then falls back to pip with --break-system-packages """ import subprocess import sys import warnings from pathlib import Path # How many trailing lines of a failed command's output to keep for the # end-of-run failure summary. Keeps the root cause near the end of the log, # which is where first_time_install.sh's error handler tails from. ERROR_TAIL_LINES = 15 def _run(cmd): """Run a command, capturing combined stdout/stderr. Returns (success, output) instead of raising, so callers can report *why* a command failed rather than just that it failed. """ result = subprocess.run(cmd, capture_output=True, text=True) return result.returncode == 0, (result.stdout or "") + (result.stderr or "") def install_via_apt(package_name): """Try to install a package via apt. Returns (success, output).""" # Map pip package names to apt package names apt_package_map = { 'flask': 'python3-flask', 'PIL': 'python3-pil', 'freetype': 'python3-freetype', 'psutil': 'python3-psutil', 'werkzeug': 'python3-werkzeug', 'numpy': 'python3-numpy', 'requests': 'python3-requests', 'python-dateutil': 'python3-dateutil', 'pytz': 'python3-tz', 'geopy': 'python3-geopy', 'unidecode': 'python3-unidecode', 'websockets': 'python3-websockets', 'websocket-client': 'python3-websocket-client' } apt_package = apt_package_map.get(package_name, f'python3-{package_name}') print(f"Trying to install {apt_package} via apt...") _run(['sudo', 'apt', 'update']) # best-effort refresh; ignore failures here success, output = _run(['sudo', 'apt', 'install', '-y', apt_package]) if success: print(f"Successfully installed {apt_package} via apt") return True, "" print(f"Failed to install {apt_package} via apt, will try pip") return False, output def install_via_pip(package_name): """Install a package via pip with --break-system-packages and --prefer-binary. --break-system-packages allows pip to install into the system Python on Debian/Ubuntu-based systems without a virtual environment. --prefer-binary prefers pre-built wheels over source distributions to avoid exhausting /tmp space during compilation. Returns (success, output). """ print(f"Installing {package_name} via pip...") success, output = _run([ sys.executable, '-m', 'pip', 'install', '--break-system-packages', '--prefer-binary', package_name ]) if success: print(f"Successfully installed {package_name} via pip") return True, "" print(f"Failed to install {package_name} via pip (see failure summary at end of log)") return False, output def check_package_installed(package_name): """Check if a package is already installed.""" # Suppress deprecation warnings when checking if packages are installed # (we're just checking, not using them) with warnings.catch_warnings(): warnings.filterwarnings('ignore', category=DeprecationWarning) try: __import__(package_name) return True except ImportError: return False def print_failure_summary(failed_packages, failure_details): print("\n" + "=" * 60) print("DEPENDENCY INSTALLATION FAILURES - DETAILS") print("=" * 60) for package in failed_packages: print(f"\nPackage: {package}") print("-" * 40) output = failure_details.get(package, "").strip() if not output: print(" (no output captured)") continue for line in output.splitlines()[-ERROR_TAIL_LINES:]: print(f" {line}") print("=" * 60) def main(): """Main installation function.""" print("Installing dependencies for LED Matrix Web Interface V2...") # List of required packages required_packages = [ 'flask', 'PIL', 'freetype', 'psutil', 'werkzeug', 'numpy', 'requests', 'python-dateutil', 'pytz', 'geopy', 'unidecode', 'websockets', 'websocket-client' ] failed_packages = [] failure_details = {} for package in required_packages: if check_package_installed(package): print(f"{package} is already installed") continue # Try apt first, then pip ok, apt_output = install_via_apt(package) if not ok: ok, pip_output = install_via_pip(package) if not ok: failed_packages.append(package) failure_details[package] = pip_output or apt_output # Install packages that don't have apt equivalents special_packages = [ 'timezonefinder>=6.5.0,<7.0.0', 'google-auth-oauthlib>=1.2.0,<2.0.0', 'google-auth-httplib2>=0.2.0,<1.0.0', 'google-api-python-client>=2.147.0,<3.0.0', 'spotipy', 'icalevents', 'python-socketio>=5.11.0,<6.0.0', 'python-engineio>=4.9.0,<5.0.0' ] for package in special_packages: ok, pip_output = install_via_pip(package) if not ok: failed_packages.append(package) failure_details[package] = pip_output # Install rgbmatrix module from local source (optional - may already be installed in Step 6) # Check if already installed first if check_package_installed('rgbmatrix'): print("rgbmatrix module already installed, skipping...") else: print("Installing rgbmatrix module from local source...") # Get project root (parent of scripts directory) PROJECT_ROOT = Path(__file__).parent.parent rgbmatrix_path = PROJECT_ROOT / 'rpi-rgb-led-matrix-master' / 'bindings' / 'python' if rgbmatrix_path.exists(): # Check if the module has been built (look for setup.py) setup_py = rgbmatrix_path / 'setup.py' if setup_py.exists(): # Try installing - use regular install, not editable mode # This is optional for web interface and should already be installed in Step 6 ok, output = _run([sys.executable, '-m', 'pip', 'install', '--break-system-packages', str(rgbmatrix_path)]) if ok: print("rgbmatrix module installed successfully") else: # Don't fail the whole installation - rgbmatrix is optional for web interface # and should be installed in Step 6 of first_time_install.sh print("Warning: Failed to install rgbmatrix module:") for line in output.strip().splitlines()[-ERROR_TAIL_LINES:]: print(f" {line}") print(" This is normal if rgbmatrix hasn't been built yet (Step 6).") print(" The web interface will work without it.") else: print("Warning: rgbmatrix setup.py not found, module may need to be built first") print(" This is normal if Step 6 hasn't completed yet.") else: print("Warning: rgbmatrix source not found (this is normal if Step 6 hasn't run yet)") if failed_packages: print(f"\nFailed to install the following packages: {failed_packages}") print("You may need to install them manually or check your system configuration.") print_failure_summary(failed_packages, failure_details) return False else: print("\nAll dependencies installed successfully!") return True if __name__ == '__main__': success = main() sys.exit(0 if success else 1)