Improve first-time install error diagnostics and resilience (#369)

* fix(install): don't let outer ERR trap mask first_time_install.sh failures

set +e alone doesn't suppress bash's ERR trap, so any non-zero exit from
first_time_install.sh inside the one-shot installer immediately triggered
the outer on_error handler with a generic "Main installation, line 370"
message — before the script could report the real exit code or point to
logs/. Suspend the trap for that block so the existing if/else handling
runs instead.

* 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.

* feat(install): harden first-time install against common Pi failure modes

- wait_for_apt_lock: apt_update/apt_install now wait (up to 3min) for
  unattended-upgrades to release the dpkg lock instead of failing
  outright with "Command failed after 3 attempts" right after first boot.
- check_disk_space: new pre-flight check (Step 1) so a full SD card fails
  fast with a clear message instead of a cryptic mid-build error.
- Step 6: wrap rpi-rgb-led-matrix git clone/submodule operations in retry
  for resilience to transient network issues.
- Step 6: capture `pip install .` build output and print the last 50
  lines on failure, so the actual cmake/compiler error is visible instead
  of just "Failed to install rpi-rgb-led-matrix Python package".

* fix(install): bound subprocess output and dedupe apt update in dependency installer

Address coderabbitai review on PR #369:
- _run() now streams combined stdout/stderr to a temp file and returns
  only the last ERROR_TAIL_LINES lines, instead of buffering full
  output in memory (Codacy also flagged the previous capture_output
  call as a subprocess-without-static-string security issue; the new
  call is annotated as safe since cmd is built from hardcoded args).
- `apt update` now runs once in main() instead of once per package
  needing an apt fallback.

* fix(install): suppress remaining Codacy subprocess false-positive

Codacy's Semgrep-based check still flagged the cmd-built subprocess.run
call as "without a static string" even with the Bandit nosec applied.
Add a nosemgrep marker alongside it - cmd is always a hardcoded
apt/pip argument list, never user input.

* fix(install): correctly detect already-installed dateutil/websocket-client

Address remaining coderabbitai findings on PR #369:
- check_package_installed() did __import__(package_name) directly, but
  python-dateutil and websocket-client import as dateutil/websocket. Both
  always failed the "already installed" check and were reinstalled on
  every run. Add an IMPORT_NAME_MAP for the mismatched names.
- _run() still read the entire temp file into memory before slicing the
  tail. Stream it line-by-line into a deque(maxlen=ERROR_TAIL_LINES)
  instead so memory use stays bounded for very chatty commands.

---------

Co-authored-by: Chuck <chuck@example.com>
This commit is contained in:
Chuck
2026-06-11 18:12:35 -04:00
committed by GitHub
parent cf28a8c0d5
commit 5beef0aa01
3 changed files with 213 additions and 94 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
@@ -202,8 +202,33 @@ retry() {
done done
} }
apt_update() { retry apt update; } # Wait for another apt/dpkg process (commonly unattended-upgrades running
apt_install() { retry apt install -y "$@"; } # shortly after first boot) to release its lock before we try apt ourselves.
# Without this, apt_update/apt_install can fail outright in the first couple
# minutes after a fresh Pi OS boot with a generic "Command failed after 3
# attempts" error.
wait_for_apt_lock() {
command -v flock >/dev/null 2>&1 || return 0
local lock_file="/var/lib/dpkg/lock-frontend"
local max_wait=180
local waited=0
local printed=0
while ! flock -n "$lock_file" -c true 2>/dev/null; do
if [ "$printed" -eq 0 ]; then
echo "⚠ Waiting for another apt/dpkg process to finish (e.g. unattended-upgrades on first boot)..."
printed=1
fi
if [ "$waited" -ge "$max_wait" ]; then
echo "⚠ Still waiting after ${max_wait}s; proceeding anyway."
break
fi
sleep 5
waited=$((waited+5))
done
}
apt_update() { wait_for_apt_lock; retry apt update; }
apt_install() { wait_for_apt_lock; retry apt install -y "$@"; }
apt_remove() { apt-get remove -y "$@" || true; } apt_remove() { apt-get remove -y "$@" || true; }
check_network() { check_network() {
@@ -222,6 +247,22 @@ check_network() {
exit 1 exit 1
} }
check_disk_space() {
command -v df >/dev/null 2>&1 || return 0
local available_mb
available_mb=$(df -m "$PROJECT_ROOT_DIR" | awk 'NR==2{print $4}')
available_mb=${available_mb:-0}
if [ "$available_mb" -lt 500 ]; then
echo "✗ ERROR: Insufficient disk space: ${available_mb}MB available (need at least 500MB)"
echo " Free up space first, e.g.: sudo apt clean && sudo apt autoremove"
exit 1
elif [ "$available_mb" -lt 1024 ]; then
echo "⚠ Limited disk space: ${available_mb}MB available (recommend at least 1GB for the rpi-rgb-led-matrix build in Step 6)"
else
echo "✓ Disk space sufficient: ${available_mb}MB available"
fi
}
echo "" echo ""
echo "This script will perform the following steps:" echo "This script will perform the following steps:"
echo "1. Install system dependencies" echo "1. Install system dependencies"
@@ -271,8 +312,9 @@ CURRENT_STEP="Install system dependencies"
echo "Step 1: Installing system dependencies..." echo "Step 1: Installing system dependencies..."
echo "----------------------------------------" echo "----------------------------------------"
# Ensure network is available before APT operations # Pre-flight checks before APT operations
check_network check_network
check_disk_space
# Update package list # Update package list
apt_update apt_update
@@ -822,14 +864,14 @@ else
# Try to initialize submodule if .gitmodules exists # Try to initialize submodule if .gitmodules exists
if [ -f "$PROJECT_ROOT_DIR/.gitmodules" ] && grep -q "rpi-rgb-led-matrix" "$PROJECT_ROOT_DIR/.gitmodules"; then if [ -f "$PROJECT_ROOT_DIR/.gitmodules" ] && grep -q "rpi-rgb-led-matrix" "$PROJECT_ROOT_DIR/.gitmodules"; then
echo "Initializing rpi-rgb-led-matrix submodule..." echo "Initializing rpi-rgb-led-matrix submodule..."
if ! git submodule update --init --recursive rpi-rgb-led-matrix-master 2>&1; then if ! retry git submodule update --init --recursive rpi-rgb-led-matrix-master; then
echo "⚠ Submodule init failed, cloning directly from GitHub..." echo "⚠ Submodule init failed, cloning directly from GitHub..."
git clone https://github.com/hzeller/rpi-rgb-led-matrix.git rpi-rgb-led-matrix-master retry git clone https://github.com/hzeller/rpi-rgb-led-matrix.git rpi-rgb-led-matrix-master
fi fi
else else
# Fallback: clone directly if submodule not configured # Fallback: clone directly if submodule not configured
echo "Submodule not configured, cloning directly from GitHub..." echo "Submodule not configured, cloning directly from GitHub..."
git clone https://github.com/hzeller/rpi-rgb-led-matrix.git rpi-rgb-led-matrix-master retry git clone https://github.com/hzeller/rpi-rgb-led-matrix.git rpi-rgb-led-matrix-master
fi fi
fi fi
@@ -841,23 +883,34 @@ else
cd "$PROJECT_ROOT_DIR" cd "$PROJECT_ROOT_DIR"
rm -rf rpi-rgb-led-matrix-master rm -rf rpi-rgb-led-matrix-master
if [ -f "$PROJECT_ROOT_DIR/.gitmodules" ] && grep -q "rpi-rgb-led-matrix" "$PROJECT_ROOT_DIR/.gitmodules"; then if [ -f "$PROJECT_ROOT_DIR/.gitmodules" ] && grep -q "rpi-rgb-led-matrix" "$PROJECT_ROOT_DIR/.gitmodules"; then
git submodule update --init --recursive rpi-rgb-led-matrix-master retry git submodule update --init --recursive rpi-rgb-led-matrix-master
else else
git clone https://github.com/hzeller/rpi-rgb-led-matrix.git rpi-rgb-led-matrix-master retry git clone https://github.com/hzeller/rpi-rgb-led-matrix.git rpi-rgb-led-matrix-master
fi fi
fi fi
pushd "$PROJECT_ROOT_DIR/rpi-rgb-led-matrix-master" >/dev/null pushd "$PROJECT_ROOT_DIR/rpi-rgb-led-matrix-master" >/dev/null
echo "Installing rpi-rgb-led-matrix Python package (scikit-build-core + cmake)..." echo "Installing rpi-rgb-led-matrix Python package (scikit-build-core + cmake)..."
echo " Build deps required: python-dev-is-python3 cmake" echo " Build deps required: python-dev-is-python3 cmake"
echo " This compiles C++ — may take 2-5 minutes on Pi 4/5..." echo " This compiles C++ — may take 2-5 minutes on Pi 4/5..."
if ! python3 -m pip install --break-system-packages .; then BUILD_OUTPUT=$(mktemp)
BUILD_SUCCESS=false
if python3 -m pip install --break-system-packages . > "$BUILD_OUTPUT" 2>&1; then
BUILD_SUCCESS=true
fi
cat "$BUILD_OUTPUT" >> "$LOG_FILE"
if [ "$BUILD_SUCCESS" != true ]; then
echo "✗ Failed to install rpi-rgb-led-matrix Python package" echo "✗ Failed to install rpi-rgb-led-matrix Python package"
echo " Ensure build tools are installed:" echo " Ensure build tools are installed:"
echo " sudo apt install -y python-dev-is-python3 cmake build-essential" echo " sudo apt install -y python-dev-is-python3 cmake build-essential"
echo ""
echo "-- Last 50 lines of build output --"
tail -n 50 "$BUILD_OUTPUT"
rm -f "$BUILD_OUTPUT"
popd >/dev/null popd >/dev/null
exit 1 exit 1
fi fi
rm -f "$BUILD_OUTPUT"
popd >/dev/null popd >/dev/null
else else
echo "✗ rpi-rgb-led-matrix-master directory not found at $PROJECT_ROOT_DIR" echo "✗ rpi-rgb-led-matrix-master directory not found at $PROJECT_ROOT_DIR"
@@ -912,7 +965,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

@@ -340,9 +340,14 @@ main() {
echo "" echo ""
# Execute with proper error handling and non-interactive mode # Execute with proper error handling and non-interactive mode
# Temporarily disable errexit to capture exit code instead of exiting immediately # Temporarily disable errexit AND the ERR trap to capture exit code instead of
# exiting immediately. `set +e` alone does not suppress the ERR trap, so without
# `trap '' ERR` a non-zero exit from first_time_install.sh would trigger on_error
# here with the generic "Main installation" message instead of the detailed
# if/else handling below.
set +e set +e
trap '' ERR
# Check /tmp permissions - only fix if actually wrong (common in automated scenarios) # Check /tmp permissions - only fix if actually wrong (common in automated scenarios)
# When running manually, /tmp usually has correct permissions (1777) # When running manually, /tmp usually has correct permissions (1777)
TMP_PERMS=$(stat -c '%a' /tmp 2>/dev/null || echo "unknown") TMP_PERMS=$(stat -c '%a' /tmp 2>/dev/null || echo "unknown")
@@ -370,6 +375,7 @@ main() {
sudo -E env TMPDIR=/tmp LEDMATRIX_ASSUME_YES=1 bash ./first_time_install.sh -y </dev/null sudo -E env TMPDIR=/tmp LEDMATRIX_ASSUME_YES=1 bash ./first_time_install.sh -y </dev/null
fi fi
INSTALL_EXIT_CODE=$? INSTALL_EXIT_CODE=$?
trap 'on_error $LINENO' ERR # Re-enable ERR trap
set -e # Re-enable errexit set -e # Re-enable errexit
if [ $INSTALL_EXIT_CODE -eq 0 ]; then if [ $INSTALL_EXIT_CODE -eq 0 ]; then

View File

@@ -6,46 +6,67 @@ then falls back to pip with --break-system-packages
import subprocess import subprocess
import sys import sys
import tempfile
import warnings import warnings
from collections import deque
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, streaming combined stdout/stderr to a temp file.
Returns (success, output) instead of raising, so callers can report
*why* a command failed rather than just that it failed. `output` is
bounded to the last ERROR_TAIL_LINES lines so failures from very
chatty commands (e.g. pip build logs) don't get buffered in memory.
"""
with tempfile.TemporaryFile(mode='w+b') as f:
result = subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT) # nosec B603 B607 - hardcoded apt/pip args # nosemgrep
f.seek(0)
# Stream line-by-line so only the last ERROR_TAIL_LINES are ever held
# in memory, regardless of how much output the command produced.
tail = deque(
(line.decode('utf-8', errors='replace').rstrip('\n') for line in f),
maxlen=ERROR_TAIL_LINES,
)
return result.returncode == 0, '\n'.join(tail)
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...") success, output = _run(['sudo', 'apt', 'install', '-y', apt_package])
subprocess.check_call([ if success:
'sudo', 'apt', 'update'
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.check_call([
'sudo', 'apt', 'install', '-y', apt_package
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print(f"Successfully installed {apt_package} via apt") print(f"Successfully installed {apt_package} via apt")
return True return True, ""
except subprocess.CalledProcessError: print(f"Failed to install {apt_package} via apt, will try pip")
print(f"Failed to install {package_name} via apt, will try pip") return False, output
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,34 +75,65 @@ 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
# Distribution (pip/apt) names whose importable module name differs.
IMPORT_NAME_MAP = {
'python-dateutil': 'dateutil',
'websocket-client': 'websocket',
}
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."""
import_name = IMPORT_NAME_MAP.get(package_name, package_name)
# Suppress deprecation warnings when checking if packages are installed # Suppress deprecation warnings when checking if packages are installed
# (we're just checking, not using them) # (we're just checking, not using them)
with warnings.catch_warnings(): with warnings.catch_warnings():
warnings.filterwarnings('ignore', category=DeprecationWarning) warnings.filterwarnings('ignore', category=DeprecationWarning)
try: try:
__import__(package_name) __import__(import_name)
return True return True
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...")
print("Refreshing apt package index...")
_run(['sudo', 'apt', 'update']) # best-effort; individual installs surface their own errors
# List of required packages # List of required packages
required_packages = [ required_packages = [
'flask', 'flask',
@@ -98,19 +150,23 @@ def main():
'websockets', 'websockets',
'websocket-client' 'websocket-client'
] ]
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):
print(f"{package} is already installed") print(f"{package} is already installed")
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 = [
'timezonefinder>=6.5.0,<7.0.0', 'timezonefinder>=6.5.0,<7.0.0',
@@ -122,47 +178,49 @@ def main():
'python-socketio>=5.11.0,<6.0.0', 'python-socketio>=5.11.0,<6.0.0',
'python-engineio>=4.9.0,<5.0.0' 'python-engineio>=4.9.0,<5.0.0'
] ]
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
if check_package_installed('rgbmatrix'): if check_package_installed('rgbmatrix'):
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!")