feat(install): surface root cause of web dependency install failures

install_dependencies_apt.py previously reported only which packages
failed, not why - the actual apt/pip error was discarded (apt) or
could scroll out of the on_error log tail (pip), leaving "Step 7:
Install web interface dependencies (line 915)" as the only visible
detail.

Capture command output for each install attempt and print a compact
DEPENDENCY INSTALLATION FAILURES summary with the last lines of error
output per package. Also run the installer with `python3 -u` for
real-time, correctly-ordered logging, and widen the on_error tail from
50 to 100 lines so the summary isn't cut off.
This commit is contained in:
Chuck
2026-06-11 09:42:59 -04:00
parent 6ea9862c14
commit 6c9a3510ee
2 changed files with 120 additions and 81 deletions

View File

@@ -15,8 +15,8 @@ on_error() {
echo "✗ An error occurred during: $CURRENT_STEP (line $line_no, exit $exit_code)" >&2 echo "✗ An error occurred during: $CURRENT_STEP (line $line_no, exit $exit_code)" >&2
if [ -n "${LOG_FILE:-}" ]; then if [ -n "${LOG_FILE:-}" ]; then
echo "See the log for details: $LOG_FILE" >&2 echo "See the log for details: $LOG_FILE" >&2
echo "-- Last 50 lines from log --" >&2 echo "-- Last 100 lines from log --" >&2
tail -n 50 "$LOG_FILE" >&2 || true tail -n 100 "$LOG_FILE" >&2 || true
fi fi
echo "\nCommon fixes:" >&2 echo "\nCommon fixes:" >&2
echo "- Ensure the Pi is online (try: ping -c1 8.8.8.8)." >&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 # Try to install dependencies using the smart installer if available
if [ -f "$PROJECT_ROOT_DIR/scripts/install_dependencies_apt.py" ]; then if [ -f "$PROJECT_ROOT_DIR/scripts/install_dependencies_apt.py" ]; then
echo "Using smart dependency installer..." 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 else
echo "Using pip to install dependencies..." echo "Using pip to install dependencies..."
if [ -f "$PROJECT_ROOT_DIR/requirements_web_v2.txt" ]; then if [ -f "$PROJECT_ROOT_DIR/requirements_web_v2.txt" ]; then

View File

@@ -9,43 +9,54 @@ import sys
import warnings import warnings
from pathlib import Path 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): def install_via_apt(package_name):
"""Try to install a package via apt.""" """Try to install a package via apt. Returns (success, output)."""
try: # Map pip package names to apt package names
# Map pip package names to apt package names apt_package_map = {
apt_package_map = { 'flask': 'python3-flask',
'flask': 'python3-flask', 'PIL': 'python3-pil',
'PIL': 'python3-pil', 'freetype': 'python3-freetype',
'freetype': 'python3-freetype', 'psutil': 'python3-psutil',
'psutil': 'python3-psutil', 'werkzeug': 'python3-werkzeug',
'werkzeug': 'python3-werkzeug', 'numpy': 'python3-numpy',
'numpy': 'python3-numpy', 'requests': 'python3-requests',
'requests': 'python3-requests', 'python-dateutil': 'python3-dateutil',
'python-dateutil': 'python3-dateutil', 'pytz': 'python3-tz',
'pytz': 'python3-tz', 'geopy': 'python3-geopy',
'geopy': 'python3-geopy', 'unidecode': 'python3-unidecode',
'unidecode': 'python3-unidecode', 'websockets': 'python3-websockets',
'websockets': 'python3-websockets', 'websocket-client': 'python3-websocket-client'
'websocket-client': 'python3-websocket-client' }
}
apt_package = apt_package_map.get(package_name, f'python3-{package_name}') apt_package = apt_package_map.get(package_name, f'python3-{package_name}')
print(f"Trying to install {apt_package} via apt...") print(f"Trying to install {apt_package} via apt...")
subprocess.check_call([ _run(['sudo', 'apt', 'update']) # best-effort refresh; ignore failures here
'sudo', 'apt', 'update'
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.check_call([
'sudo', 'apt', 'install', '-y', apt_package
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
success, output = _run(['sudo', 'apt', 'install', '-y', apt_package])
if success:
print(f"Successfully installed {apt_package} via apt") print(f"Successfully installed {apt_package} via apt")
return True return True, ""
print(f"Failed to install {apt_package} via apt, will try pip")
return False, output
except subprocess.CalledProcessError:
print(f"Failed to install {package_name} via apt, will try pip")
return False
def install_via_pip(package_name): def install_via_pip(package_name):
"""Install a package via pip with --break-system-packages and --prefer-binary. """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. Debian/Ubuntu-based systems without a virtual environment.
--prefer-binary prefers pre-built wheels over source distributions to avoid --prefer-binary prefers pre-built wheels over source distributions to avoid
exhausting /tmp space during compilation. exhausting /tmp space during compilation.
Returns (success, output).
""" """
try: print(f"Installing {package_name} via pip...")
print(f"Installing {package_name} via pip...") success, output = _run([
subprocess.check_call([ sys.executable, '-m', 'pip', 'install', '--break-system-packages', '--prefer-binary', package_name
sys.executable, '-m', 'pip', 'install', '--break-system-packages', '--prefer-binary', package_name ])
]) if success:
print(f"Successfully installed {package_name} via pip") print(f"Successfully installed {package_name} via pip")
return True return True, ""
except subprocess.CalledProcessError as e:
print(f"Failed to install {package_name} via pip: {e}") print(f"Failed to install {package_name} via pip (see failure summary at end of log)")
return False return False, output
def check_package_installed(package_name): def check_package_installed(package_name):
"""Check if a package is already installed.""" """Check if a package is already installed."""
@@ -78,6 +92,23 @@ def check_package_installed(package_name):
except ImportError: except ImportError:
return False 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(): def main():
"""Main installation function.""" """Main installation function."""
print("Installing dependencies for LED Matrix Web Interface V2...") print("Installing dependencies for LED Matrix Web Interface V2...")
@@ -100,6 +131,7 @@ def main():
] ]
failed_packages = [] failed_packages = []
failure_details = {}
for package in required_packages: for package in required_packages:
if check_package_installed(package): if check_package_installed(package):
@@ -107,9 +139,12 @@ def main():
continue continue
# Try apt first, then pip # Try apt first, then pip
if not install_via_apt(package): ok, apt_output = install_via_apt(package)
if not install_via_pip(package): if not ok:
ok, pip_output = install_via_pip(package)
if not ok:
failed_packages.append(package) failed_packages.append(package)
failure_details[package] = pip_output or apt_output
# Install packages that don't have apt equivalents # Install packages that don't have apt equivalents
special_packages = [ special_packages = [
@@ -124,8 +159,10 @@ def main():
] ]
for package in special_packages: 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) failed_packages.append(package)
failure_details[package] = pip_output
# Install rgbmatrix module from local source (optional - may already be installed in Step 6) # Install rgbmatrix module from local source (optional - may already be installed in Step 6)
# Check if already installed first # Check if already installed first
@@ -133,36 +170,36 @@ def main():
print("rgbmatrix module already installed, skipping...") print("rgbmatrix module already installed, skipping...")
else: else:
print("Installing rgbmatrix module from local source...") print("Installing rgbmatrix module from local source...")
try: # Get project root (parent of scripts directory)
# Get project root (parent of scripts directory) PROJECT_ROOT = Path(__file__).parent.parent
PROJECT_ROOT = Path(__file__).parent.parent rgbmatrix_path = PROJECT_ROOT / 'rpi-rgb-led-matrix-master' / 'bindings' / 'python'
rgbmatrix_path = PROJECT_ROOT / 'rpi-rgb-led-matrix-master' / 'bindings' / 'python' if rgbmatrix_path.exists():
if rgbmatrix_path.exists(): # Check if the module has been built (look for setup.py)
# Check if the module has been built (look for setup.py) setup_py = rgbmatrix_path / 'setup.py'
setup_py = rgbmatrix_path / 'setup.py' if setup_py.exists():
if setup_py.exists(): # Try installing - use regular install, not editable mode
# Try installing - use regular install, not editable mode # This is optional for web interface and should already be installed in Step 6
# 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)])
subprocess.check_call([ if ok:
sys.executable, '-m', 'pip', 'install', '--break-system-packages', str(rgbmatrix_path)
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print("rgbmatrix module installed successfully") print("rgbmatrix module installed successfully")
else: else:
print("Warning: rgbmatrix setup.py not found, module may need to be built first") # Don't fail the whole installation - rgbmatrix is optional for web interface
print(" This is normal if Step 6 hasn't completed yet.") # 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: else:
print("Warning: rgbmatrix source not found (this is normal if Step 6 hasn't run yet)") print("Warning: rgbmatrix setup.py not found, module may need to be built first")
except subprocess.CalledProcessError as e: print(" This is normal if Step 6 hasn't completed yet.")
# Don't fail the whole installation - rgbmatrix is optional for web interface else:
# and should be installed in Step 6 of first_time_install.sh print("Warning: rgbmatrix source not found (this is normal if Step 6 hasn't run yet)")
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
if failed_packages: if failed_packages:
print(f"\nFailed to install the following packages: {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("You may need to install them manually or check your system configuration.")
print_failure_summary(failed_packages, failure_details)
return False return False
else: else:
print("\nAll dependencies installed successfully!") print("\nAll dependencies installed successfully!")