mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-06-19 11:08:39 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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!")
|
||||||
|
|||||||
Reference in New Issue
Block a user