diff --git a/first_time_install.sh b/first_time_install.sh index e43a7b8b..7bfa334e 100644 --- a/first_time_install.sh +++ b/first_time_install.sh @@ -15,8 +15,8 @@ on_error() { echo "✗ An error occurred during: $CURRENT_STEP (line $line_no, exit $exit_code)" >&2 if [ -n "${LOG_FILE:-}" ]; then echo "See the log for details: $LOG_FILE" >&2 - echo "-- Last 50 lines from log --" >&2 - tail -n 50 "$LOG_FILE" >&2 || true + echo "-- Last 100 lines from log --" >&2 + tail -n 100 "$LOG_FILE" >&2 || true fi echo "\nCommon fixes:" >&2 echo "- Ensure the Pi is online (try: ping -c1 8.8.8.8)." >&2 @@ -912,7 +912,9 @@ else # Try to install dependencies using the smart installer if available if [ -f "$PROJECT_ROOT_DIR/scripts/install_dependencies_apt.py" ]; then echo "Using smart dependency installer..." - python3 "$PROJECT_ROOT_DIR/scripts/install_dependencies_apt.py" + # -u: unbuffered stdout/stderr so output is captured in $LOG_FILE in + # real time and in order relative to this script's own echo statements + python3 -u "$PROJECT_ROOT_DIR/scripts/install_dependencies_apt.py" else echo "Using pip to install dependencies..." if [ -f "$PROJECT_ROOT_DIR/requirements_web_v2.txt" ]; then diff --git a/scripts/install_dependencies_apt.py b/scripts/install_dependencies_apt.py index ef019118..41730844 100644 --- a/scripts/install_dependencies_apt.py +++ b/scripts/install_dependencies_apt.py @@ -9,43 +9,54 @@ 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.""" - try: - # 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...") - subprocess.check_call([ - 'sudo', 'apt', 'update' - ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - - subprocess.check_call([ - 'sudo', 'apt', 'install', '-y', apt_package - ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) - + """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 - - except subprocess.CalledProcessError: - print(f"Failed to install {package_name} via apt, will try pip") - return False + 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. @@ -54,17 +65,20 @@ def install_via_pip(package_name): 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). """ - try: - print(f"Installing {package_name} via pip...") - subprocess.check_call([ - sys.executable, '-m', 'pip', 'install', '--break-system-packages', '--prefer-binary', package_name - ]) + 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 - except subprocess.CalledProcessError as e: - print(f"Failed to install {package_name} via pip: {e}") - return False + 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.""" @@ -78,10 +92,27 @@ def check_package_installed(package_name): 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', @@ -98,19 +129,23 @@ def main(): '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 - if not install_via_apt(package): - if not install_via_pip(package): + 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', @@ -122,47 +157,49 @@ def main(): 'python-socketio>=5.11.0,<6.0.0', 'python-engineio>=4.9.0,<5.0.0' ] - + for package in special_packages: - if not install_via_pip(package): + 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...") - try: - # 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 - subprocess.check_call([ - sys.executable, '-m', 'pip', 'install', '--break-system-packages', str(rgbmatrix_path) - ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # 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: - 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.") + # 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 source not found (this is normal if Step 6 hasn't run yet)") - except subprocess.CalledProcessError as e: - # 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(f"Warning: Failed to install rgbmatrix module: {e}") - print(" This is normal if rgbmatrix hasn't been built yet (Step 6).") - print(" The web interface will work without it.") - # Don't add to failed_packages since it's optional - + 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!")