mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 13:02:59 +00:00
* fix(plugins): Remove compatible_versions requirement from single plugin install
Remove compatible_versions from required fields in install_from_url method
to match install_plugin behavior. This allows installing plugins from URLs
without manifest version requirements, consistent with store plugin installation.
* fix(7-segment-clock): Update submodule with separator and spacing fixes
* fix(plugins): Add onchange handlers to existing custom feed inputs
- Add onchange handlers to key and value inputs for existing patternProperties fields
- Fixes bug where editing existing custom RSS feeds didn't save changes
- Ensures hidden JSON input field is updated when users edit feed entries
- Affects all plugins using patternProperties (custom_feeds, feed_logo_map, etc.)
* Add array-of-objects widget support to web UI
- Add support for rendering arrays of objects in web UI (for custom_feeds)
- Implement add/remove/update functions for array-of-objects widgets
- Support file-upload widgets within array items
- Update form data handling to support array JSON data fields
* Update plugins_manager.js cache-busting version
Update version parameter to force browser to load new JavaScript with array-of-objects widget support.
* Fix: Move array-of-objects detection before file-upload/checkbox checks
Move the array-of-objects widget detection to the top of the array handler so it's checked before file-upload and checkbox-group widgets. This ensures custom_feeds is properly detected as an array of objects.
* Update cache-busting version for array-of-objects fix
* Remove duplicate array-of-objects check
* Update cache version again
* Add array-of-objects widget support to server-side template
Add detection and rendering for array-of-objects in the Jinja2 template (plugin_config.html).
This enables the custom_feeds widget to display properly with name, URL, enabled checkbox, and logo upload fields.
The widget is detected by checking if prop.items.type == 'object' && prop.items.properties,
and is rendered before the file-upload widget check.
* Use window. prefix for array-of-objects JavaScript functions
Explicitly use window.addArrayObjectItem, window.removeArrayObjectItem, etc.
in the template to ensure the functions are accessible from inline event handlers.
Also add safety checks to prevent errors if functions aren't loaded yet.
* Fix duplicate display settings in config
Prevent display settings from being saved at both nested (display.hardware/runtime) and root level. The save_main_config function was processing display fields twice - once correctly in the nested structure, and again in the catch-all section creating root-level duplicates.
Added display_fields to the skip list in the catch-all section to prevent root-level duplicates. All code expects the nested format, so this ensures consistency.
* fix: Recreate one-shot install script with APT permission and non-interactive fixes
Recreate one-shot install script that was deleted, with fixes for:
1. APT permission denied errors on /tmp
2. Non-interactive mode support
Fixes:
1. Fix /tmp permissions before running first_time_install.sh:
- chmod 1777 /tmp to ensure APT can write temp files
- Set TMPDIR=/tmp explicitly
- Preserve TMPDIR when using sudo -E
2. Enable non-interactive mode:
- Pass -y flag or LEDMATRIX_ASSUME_YES=1 to first_time_install.sh
- Prevents read prompt failure at line 242 when run via curl | bash
3. Better error handling:
- Temporarily disable errexit to capture exit code
- Re-enable errexit after capturing
- Added fix_tmp_permissions() function
This resolves the 'Permission denied' errors for APT temp files and the
interactive prompt failure when running via pipe.
* fix(plugins): Restore version and display_modes to required_fields and fix array object data persistence
- Restore 'version' and 'display_modes' to required_fields in store_manager.py manifest validation (both occurrences at lines 839 and 977)
- Fix updateArrayObjectData to merge input fields with existing item data to preserve non-editable properties like logo objects
- Implement handleArrayObjectFileUpload to properly upload files and store metadata in data-file-data attribute
- Implement removeArrayObjectFile to properly remove file metadata and update data structure
- Update renderArrayObjectItem to preserve file data in data-file-data attribute when rendering existing items
* fix(plugins): Remove version from required_fields, keep display_modes required
- Remove 'version' from required_fields in store_manager.py (both occurrences)
- Some existing plugins have version: null or no version field (basketball-scoreboard, odds-ticker)
- All code uses safe accessors (manifest.get('version')), so optional is safe
- Keep 'display_modes' as required - all plugins have it and tests expect it
* fix: Preserve exit codes in retry() and fix null handling in JSON data detection
- Fix retry() function to preserve original command exit code by capturing status immediately after command execution
- Fix JSON data detection to prevent null from overwriting config by checking jsonValue !== null before treating as object
- Both fixes prevent edge cases that could cause incorrect behavior or data corruption
* fix: Resolve merge conflict, fix array-of-objects file upload, and improve retry function
- Remove unresolved merge conflict marker in array rendering (checkbox input attributes)
- Fix array-of-objects file upload selector mismatch by adding id to wrapper element
- Fix index-based preserve corruption by using data-item-data attributes instead of array indices
- Add showNotification guards to prevent errors when notifications aren't available
- Fix retry() function to work with set -Eeuo pipefail by disabling errexit for command execution
* fix: Remove duplicate implementations, fix upload config, and add type coercion
- Remove/guard duplicate updateArrayObjectData, handleArrayObjectFileUpload, and removeArrayObjectFile stub implementations that were overwriting real implementations
- Fix hard-coded plugin ID fallback in renderArrayObjectItem - use null instead of 'ledmatrix-news'
- Fix upload config to use uploadConfig.allowed_types and uploadConfig.max_size_mb from schema instead of hard-coded values
- Store uploadConfig in data-upload-config attribute and read it in handleArrayObjectFileUpload for validation
- Add type coercion to updateArrayObjectData: coerce number inputs to Number, array inputs via JSON.parse with comma-split fallback
* fix: Use event-based element lookup in handleArrayObjectFileUpload
- Change from constructing ID to using event.target.closest('.array-object-item') to find item element
- Query fileUploadContainer from itemEl instead of using constructed ID lookup
- Remove reliance on `${fieldId}_item_${itemIndex}` which breaks after reindexing
- Add response.ok check before calling response.json() to avoid JSON parsing errors on HTTP errors
- Handle non-OK responses with proper error messages (JSON parse with fallback)
* fix: Improve HTML escaping and add pluginId validation for file uploads
- Replace manual single-quote escaping with escapeAttribute() for proper HTML escaping in array-of-objects hidden input
- Update default allowed_types to include 'image/jpg' in handleArrayObjectFileUpload
- Add explicit pluginId validation before upload to fail fast with clear error message
- Prevents XSS vulnerabilities and backend rejections from invalid uploads
* fix: Use propKey-scoped selector and harden pluginId validation
- Narrow file widget lookup to use propKey-specific selector (.file-upload-widget-inline[data-prop-key]) to target correct widget when item has multiple file widgets
- Harden pluginId validation by checking typeof pluginId === 'string' before calling trim() to prevent errors on non-string values
---------
Co-authored-by: Chuck <chuck@example.com>
416 lines
15 KiB
Bash
Executable File
416 lines
15 KiB
Bash
Executable File
#!/bin/bash
|
|
|
|
# LED Matrix One-Shot Installation Script
|
|
# This script provides a single-command installation experience
|
|
# Usage: curl -fsSL https://raw.githubusercontent.com/ChuckBuilds/LEDMatrix/main/scripts/install/one-shot-install.sh | bash
|
|
|
|
set -Eeuo pipefail
|
|
|
|
# Global state for error tracking
|
|
CURRENT_STEP="initialization"
|
|
|
|
# Color codes for output
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m' # No Color
|
|
|
|
# Error handler for explicit failures
|
|
on_error() {
|
|
local exit_code=$?
|
|
local line_no=${1:-unknown}
|
|
echo "" >&2
|
|
echo -e "${RED}✗ ERROR: Installation failed at step: $CURRENT_STEP${NC}" >&2
|
|
echo -e "${RED} Line: $line_no, Exit code: $exit_code${NC}" >&2
|
|
echo "" >&2
|
|
echo "Common fixes:" >&2
|
|
echo " - Check internet connectivity: ping -c1 8.8.8.8" >&2
|
|
echo " - Verify sudo access: sudo -v" >&2
|
|
echo " - Check disk space: df -h /" >&2
|
|
echo " - If APT lock error: sudo dpkg --configure -a" >&2
|
|
echo " - If /tmp permission error: sudo chmod 1777 /tmp" >&2
|
|
echo " - Wait a few minutes and try again" >&2
|
|
echo "" >&2
|
|
echo "This script is safe to run multiple times. You can re-run it to continue." >&2
|
|
exit "$exit_code"
|
|
}
|
|
trap 'on_error $LINENO' ERR
|
|
|
|
# Helper functions for colored output
|
|
print_step() {
|
|
echo ""
|
|
echo -e "${BLUE}==========================================${NC}"
|
|
echo -e "${BLUE}$1${NC}"
|
|
echo -e "${BLUE}==========================================${NC}"
|
|
echo ""
|
|
}
|
|
|
|
print_success() {
|
|
echo -e "${GREEN}✓${NC} $1"
|
|
}
|
|
|
|
print_warning() {
|
|
echo -e "${YELLOW}⚠${NC} $1"
|
|
}
|
|
|
|
print_error() {
|
|
echo -e "${RED}✗${NC} $1"
|
|
}
|
|
|
|
# Retry function for network operations
|
|
retry() {
|
|
local attempt=1
|
|
local max_attempts=3
|
|
local delay_seconds=5
|
|
local status
|
|
while true; do
|
|
# Run command in a context that disables errexit so we can capture exit code
|
|
# This prevents errexit from triggering before status=$? runs
|
|
if ! "$@"; then
|
|
status=$?
|
|
else
|
|
status=0
|
|
fi
|
|
if [ $status -eq 0 ]; then
|
|
return 0
|
|
fi
|
|
if [ $attempt -ge $max_attempts ]; then
|
|
print_error "Command failed after $attempt attempts: $*"
|
|
return $status
|
|
fi
|
|
print_warning "Command failed (attempt $attempt/$max_attempts). Retrying in ${delay_seconds}s: $*"
|
|
attempt=$((attempt+1))
|
|
sleep "$delay_seconds"
|
|
done
|
|
}
|
|
|
|
# Check network connectivity
|
|
check_network() {
|
|
CURRENT_STEP="Network connectivity check"
|
|
print_step "Checking network connectivity..."
|
|
|
|
if command -v ping >/dev/null 2>&1; then
|
|
if ping -c 1 -W 3 8.8.8.8 >/dev/null 2>&1; then
|
|
print_success "Internet connectivity confirmed (ping test)"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
if curl -Is --max-time 5 http://deb.debian.org >/dev/null 2>&1; then
|
|
print_success "Internet connectivity confirmed (curl test)"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
if command -v wget >/dev/null 2>&1; then
|
|
if wget --spider --timeout=5 http://deb.debian.org >/dev/null 2>&1; then
|
|
print_success "Internet connectivity confirmed (wget test)"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
print_error "No internet connectivity detected"
|
|
echo ""
|
|
echo "Please ensure your Raspberry Pi is connected to the internet and try again."
|
|
exit 1
|
|
}
|
|
|
|
# Check disk space
|
|
check_disk_space() {
|
|
CURRENT_STEP="Disk space check"
|
|
if ! command -v df >/dev/null 2>&1; then
|
|
print_warning "df command not available, skipping disk space check"
|
|
return 0
|
|
fi
|
|
|
|
# Check available space in MB
|
|
AVAILABLE_SPACE=$(df -m / | awk 'NR==2{print $4}' || echo "0")
|
|
# Ensure AVAILABLE_SPACE has a default value if empty (handles unexpected df output)
|
|
AVAILABLE_SPACE=${AVAILABLE_SPACE:-0}
|
|
|
|
if [ "$AVAILABLE_SPACE" -lt 500 ]; then
|
|
print_error "Insufficient disk space: ${AVAILABLE_SPACE}MB available (need at least 500MB)"
|
|
echo ""
|
|
echo "Please free up disk space before continuing:"
|
|
echo " - Remove unnecessary packages: sudo apt autoremove"
|
|
echo " - Clean APT cache: sudo apt clean"
|
|
echo " - Check large files: sudo du -sh /* | sort -h"
|
|
exit 1
|
|
elif [ "$AVAILABLE_SPACE" -lt 1024 ]; then
|
|
print_warning "Limited disk space: ${AVAILABLE_SPACE}MB available (recommend at least 1GB)"
|
|
else
|
|
print_success "Disk space sufficient: ${AVAILABLE_SPACE}MB available"
|
|
fi
|
|
}
|
|
|
|
# Ensure sudo access
|
|
check_sudo() {
|
|
CURRENT_STEP="Sudo access check"
|
|
print_step "Checking sudo access..."
|
|
|
|
# Check if running as root
|
|
if [ "$EUID" -eq 0 ]; then
|
|
print_success "Running as root"
|
|
return 0
|
|
fi
|
|
|
|
# Check if sudo is available
|
|
if ! command -v sudo >/dev/null 2>&1; then
|
|
print_error "sudo is not available and script is not running as root"
|
|
echo ""
|
|
echo "Please either:"
|
|
echo " 1. Run as root: sudo bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/ChuckBuilds/LEDMatrix/main/scripts/install/one-shot-install.sh)\""
|
|
echo " 2. Or install sudo first"
|
|
exit 1
|
|
fi
|
|
|
|
# Test sudo access
|
|
if ! sudo -n true 2>/dev/null; then
|
|
print_warning "Need sudo password - you may be prompted"
|
|
if ! sudo -v; then
|
|
print_error "Failed to obtain sudo privileges"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
print_success "Sudo access confirmed"
|
|
}
|
|
|
|
# Fix /tmp permissions if needed (common issue when running via curl | bash)
|
|
# Note: /tmp permission fixing is now done inline before running first_time_install.sh
|
|
# This function is kept for backward compatibility but not actively used
|
|
fix_tmp_permissions() {
|
|
CURRENT_STEP="TMP directory check"
|
|
# Only fix if /tmp is actually not writable (don't preemptively fix)
|
|
if [ ! -w /tmp ]; then
|
|
print_warning "/tmp is not writable, attempting to fix..."
|
|
if [ "$EUID" -eq 0 ]; then
|
|
chmod 1777 /tmp 2>/dev/null || true
|
|
else
|
|
sudo chmod 1777 /tmp 2>/dev/null || true
|
|
fi
|
|
fi
|
|
|
|
# Ensure TMPDIR is set correctly
|
|
if [ -z "${TMPDIR:-}" ] || [ ! -w "${TMPDIR:-/tmp}" ]; then
|
|
export TMPDIR=/tmp
|
|
fi
|
|
}
|
|
|
|
# Main installation function
|
|
main() {
|
|
print_step "LED Matrix One-Shot Installation"
|
|
|
|
echo "This script will:"
|
|
echo " 1. Check prerequisites (network, disk space, sudo)"
|
|
echo " 2. Install system dependencies (git, python3, build tools)"
|
|
echo " 3. Clone the LEDMatrix repository"
|
|
echo " 4. Run the first-time installation script"
|
|
echo ""
|
|
|
|
# Check prerequisites
|
|
check_network
|
|
check_disk_space
|
|
check_sudo
|
|
# Note: /tmp permissions are checked and fixed inline before running first_time_install.sh
|
|
# (only if actually wrong, not preemptively)
|
|
|
|
# Install basic system dependencies needed for cloning
|
|
CURRENT_STEP="Installing system dependencies"
|
|
print_step "Installing system dependencies..."
|
|
|
|
# Validate HOME variable
|
|
if [ -z "${HOME:-}" ]; then
|
|
print_error "HOME environment variable is not set"
|
|
echo "Please set HOME or run: export HOME=\$(eval echo ~\$(whoami))"
|
|
exit 1
|
|
fi
|
|
|
|
# Update package list first
|
|
if [ "$EUID" -eq 0 ]; then
|
|
retry apt-get update -qq
|
|
else
|
|
retry sudo apt-get update -qq
|
|
fi
|
|
|
|
# Install git and curl (needed for cloning and the script itself)
|
|
if ! command -v git >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
|
|
print_warning "git or curl not found, installing..."
|
|
if [ "$EUID" -eq 0 ]; then
|
|
retry apt-get install -y git curl
|
|
else
|
|
retry sudo apt-get install -y git curl
|
|
fi
|
|
print_success "git and curl installed"
|
|
else
|
|
print_success "git and curl already installed"
|
|
fi
|
|
|
|
# Determine repository location
|
|
REPO_DIR="${HOME}/LEDMatrix"
|
|
REPO_URL="https://github.com/ChuckBuilds/LEDMatrix.git"
|
|
|
|
CURRENT_STEP="Repository setup"
|
|
print_step "Setting up repository..."
|
|
|
|
# Check if directory exists and handle accordingly
|
|
if [ -d "$REPO_DIR" ]; then
|
|
if [ -d "$REPO_DIR/.git" ]; then
|
|
print_warning "Repository already exists at $REPO_DIR"
|
|
print_warning "Pulling latest changes..."
|
|
if ! cd "$REPO_DIR"; then
|
|
print_error "Failed to change to directory: $REPO_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
# Detect current branch or try main/master
|
|
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main")
|
|
if [ "$CURRENT_BRANCH" = "HEAD" ] || [ -z "$CURRENT_BRANCH" ]; then
|
|
CURRENT_BRANCH="main"
|
|
fi
|
|
|
|
# Try to safely update current branch first (fast-forward only to avoid unintended merges)
|
|
PULL_SUCCESS=false
|
|
if git pull --ff-only origin "$CURRENT_BRANCH" >/dev/null 2>&1; then
|
|
print_success "Repository updated successfully (branch: $CURRENT_BRANCH)"
|
|
PULL_SUCCESS=true
|
|
else
|
|
# Current branch pull failed, check if other branches exist on remote
|
|
# Fetch (don't merge) to verify remote branches exist
|
|
for branch in "main" "master"; do
|
|
if [ "$branch" != "$CURRENT_BRANCH" ]; then
|
|
if git fetch origin "$branch" >/dev/null 2>&1; then
|
|
print_warning "Current branch ($CURRENT_BRANCH) could not be updated, but remote branch '$branch' exists"
|
|
print_warning "Consider switching branches or resolving conflicts"
|
|
break
|
|
fi
|
|
fi
|
|
done
|
|
fi
|
|
|
|
if [ "$PULL_SUCCESS" = false ]; then
|
|
print_warning "Git pull failed, but continuing with existing repository"
|
|
print_warning "You may have local changes or the repository may be on a different branch"
|
|
fi
|
|
else
|
|
print_warning "Directory exists but is not a git repository"
|
|
print_warning "Removing and cloning fresh..."
|
|
if ! cd "$HOME"; then
|
|
print_error "Failed to change to home directory: $HOME"
|
|
exit 1
|
|
fi
|
|
rm -rf "$REPO_DIR"
|
|
print_success "Cloning repository..."
|
|
retry git clone "$REPO_URL" "$REPO_DIR"
|
|
fi
|
|
else
|
|
print_success "Cloning repository to $REPO_DIR..."
|
|
retry git clone "$REPO_URL" "$REPO_DIR"
|
|
fi
|
|
|
|
# Verify repository is accessible
|
|
if [ ! -d "$REPO_DIR" ] || [ ! -f "$REPO_DIR/first_time_install.sh" ]; then
|
|
print_error "Repository setup failed: $REPO_DIR/first_time_install.sh not found"
|
|
exit 1
|
|
fi
|
|
|
|
print_success "Repository ready at $REPO_DIR"
|
|
|
|
# Execute main installation script
|
|
CURRENT_STEP="Main installation"
|
|
print_step "Running main installation script..."
|
|
|
|
if ! cd "$REPO_DIR"; then
|
|
print_error "Failed to change to repository directory: $REPO_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
# Make sure the script is executable
|
|
chmod +x first_time_install.sh
|
|
|
|
# Check if script exists
|
|
if [ ! -f "first_time_install.sh" ]; then
|
|
print_error "first_time_install.sh not found in $REPO_DIR"
|
|
exit 1
|
|
fi
|
|
|
|
print_success "Starting main installation (this may take 10-30 minutes)..."
|
|
echo ""
|
|
|
|
# Execute with proper error handling and non-interactive mode
|
|
# Temporarily disable errexit to capture exit code instead of exiting immediately
|
|
set +e
|
|
|
|
# Check /tmp permissions - only fix if actually wrong (common in automated scenarios)
|
|
# When running manually, /tmp usually has correct permissions (1777)
|
|
TMP_PERMS=$(stat -c '%a' /tmp 2>/dev/null || echo "unknown")
|
|
if [ "$TMP_PERMS" != "1777" ] && [ "$TMP_PERMS" != "unknown" ]; then
|
|
CURRENT_STEP="Fixing /tmp permissions"
|
|
print_warning "/tmp has incorrect permissions ($TMP_PERMS), fixing to 1777..."
|
|
if [ "$EUID" -eq 0 ]; then
|
|
chmod 1777 /tmp 2>/dev/null || print_warning "Failed to fix /tmp permissions, continuing anyway..."
|
|
else
|
|
sudo chmod 1777 /tmp 2>/dev/null || print_warning "Failed to fix /tmp permissions, continuing anyway..."
|
|
fi
|
|
fi
|
|
|
|
# Execute main installation script with non-interactive mode
|
|
CURRENT_STEP="Main installation"
|
|
export TMPDIR=/tmp
|
|
if [ "$EUID" -eq 0 ]; then
|
|
# Run in non-interactive mode with ASSUME_YES (both -y flag and env var for safety)
|
|
export LEDMATRIX_ASSUME_YES=1
|
|
bash ./first_time_install.sh -y
|
|
else
|
|
# Pass both -y flag AND environment variable for non-interactive mode
|
|
# This ensures it works even if the script re-executes itself with sudo
|
|
# Also ensure stdin is properly handled for non-interactive mode
|
|
sudo -E env TMPDIR=/tmp LEDMATRIX_ASSUME_YES=1 bash ./first_time_install.sh -y </dev/null
|
|
fi
|
|
INSTALL_EXIT_CODE=$?
|
|
set -e # Re-enable errexit
|
|
|
|
if [ $INSTALL_EXIT_CODE -eq 0 ]; then
|
|
echo ""
|
|
print_step "Installation Complete!"
|
|
print_success "LED Matrix has been successfully installed!"
|
|
echo ""
|
|
echo "Next steps:"
|
|
echo " 1. Configure your settings: sudo nano $REPO_DIR/config/config.json"
|
|
if command -v hostname >/dev/null 2>&1; then
|
|
# Get first usable IP address (filter out loopback, IPv6 loopback, and link-local)
|
|
IP_ADDRESS=$(hostname -I 2>/dev/null | awk '{for(i=1;i<=NF;i++){ip=$i; if(ip!="127.0.0.1" && ip!="::1" && substr(ip,1,5)!="fe80:"){print ip; exit}}}' || echo "")
|
|
if [ -n "$IP_ADDRESS" ]; then
|
|
# Check if IPv6 address (contains colons but no periods)
|
|
if [[ "$IP_ADDRESS" =~ .*:.* ]] && [[ ! "$IP_ADDRESS" =~ .*\..* ]]; then
|
|
# IPv6 addresses need brackets in URLs
|
|
echo " 2. Or use the web interface: http://[$IP_ADDRESS]:5000"
|
|
else
|
|
# IPv4 address
|
|
echo " 2. Or use the web interface: http://$IP_ADDRESS:5000"
|
|
fi
|
|
else
|
|
echo " 2. Or use the web interface: http://<your-pi-ip>:5000"
|
|
fi
|
|
else
|
|
echo " 2. Or use the web interface: http://<your-pi-ip>:5000"
|
|
fi
|
|
echo " 3. Start the service: sudo systemctl start ledmatrix.service"
|
|
echo ""
|
|
else
|
|
print_error "Main installation script exited with code $INSTALL_EXIT_CODE"
|
|
echo ""
|
|
echo "The installation may have partially completed."
|
|
echo "You can:"
|
|
echo " 1. Re-run this script to continue (it's safe to run multiple times)"
|
|
echo " 2. Check logs in $REPO_DIR/logs/"
|
|
echo " 3. Review the error messages above"
|
|
exit $INSTALL_EXIT_CODE
|
|
fi
|
|
}
|
|
|
|
# Run main function
|
|
main "$@"
|