499 lines
16 KiB
Python
499 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
RFCP Installer — Detects hardware, installs dependencies, sets up GPU acceleration.
|
|
|
|
Usage:
|
|
python install_rfcp.py
|
|
|
|
The installer handles:
|
|
- Python dependency installation
|
|
- GPU detection (NVIDIA/Intel/AMD)
|
|
- GPU acceleration setup (CuPy for CUDA, PyOpenCL for Intel/AMD)
|
|
- Frontend build (if Node.js available)
|
|
- Verification of installation
|
|
"""
|
|
|
|
import subprocess
|
|
import sys
|
|
import platform
|
|
import os
|
|
import shutil
|
|
|
|
|
|
def print_header(text: str):
|
|
"""Print section header."""
|
|
print(f"\n{'=' * 60}")
|
|
print(f" {text}")
|
|
print('=' * 60)
|
|
|
|
|
|
def print_step(text: str):
|
|
"""Print step indicator."""
|
|
print(f"\n>>> {text}")
|
|
|
|
|
|
def check_python() -> bool:
|
|
"""Verify Python 3.10+ is available."""
|
|
version = sys.version_info
|
|
if version.major < 3 or version.minor < 10:
|
|
print(f"[X] Python 3.10+ required, found {version.major}.{version.minor}")
|
|
return False
|
|
print(f"[OK] Python {version.major}.{version.minor}.{version.micro}")
|
|
return True
|
|
|
|
|
|
def check_node() -> bool:
|
|
"""Verify Node.js 18+ is available."""
|
|
try:
|
|
result = subprocess.run(
|
|
["node", "--version"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
version = result.stdout.strip().lstrip('v')
|
|
major = int(version.split('.')[0])
|
|
if major < 18:
|
|
print(f"[!] Node.js 18+ recommended, found {version}")
|
|
return False
|
|
print(f"[OK] Node.js {version}")
|
|
return True
|
|
except FileNotFoundError:
|
|
print("[!] Node.js not found (frontend build will be skipped)")
|
|
return False
|
|
except Exception as e:
|
|
print(f"[!] Node.js check failed: {e}")
|
|
return False
|
|
|
|
|
|
def detect_gpu() -> dict:
|
|
"""Detect available GPU hardware."""
|
|
gpus = {
|
|
"nvidia": False,
|
|
"nvidia_name": "",
|
|
"nvidia_memory_mb": 0,
|
|
"intel": False,
|
|
"intel_name": "",
|
|
"amd": False,
|
|
"amd_name": ""
|
|
}
|
|
|
|
# Check NVIDIA via nvidia-smi
|
|
try:
|
|
result = subprocess.run(
|
|
["nvidia-smi", "--query-gpu=name,driver_version,memory.total",
|
|
"--format=csv,noheader"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
info = result.stdout.strip()
|
|
parts = info.split(",")
|
|
gpus["nvidia"] = True
|
|
gpus["nvidia_name"] = parts[0].strip()
|
|
if len(parts) >= 3:
|
|
mem_str = parts[2].strip().replace(" MiB", "").replace(" MB", "")
|
|
try:
|
|
gpus["nvidia_memory_mb"] = int(mem_str)
|
|
except ValueError:
|
|
pass
|
|
print(f"[OK] NVIDIA GPU: {gpus['nvidia_name']}")
|
|
except FileNotFoundError:
|
|
pass
|
|
except subprocess.TimeoutExpired:
|
|
print("[!] nvidia-smi timed out")
|
|
except Exception as e:
|
|
print(f"[!] NVIDIA detection error: {e}")
|
|
|
|
# Check Intel/AMD via WMI (Windows) or lspci (Linux)
|
|
if platform.system() == "Windows":
|
|
try:
|
|
result = subprocess.run(
|
|
["wmic", "path", "win32_videocontroller", "get",
|
|
"name", "/format:csv"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
for line in result.stdout.strip().split('\n'):
|
|
line_lower = line.lower()
|
|
if 'intel' in line_lower and ('uhd' in line_lower or 'iris' in line_lower or 'hd graphics' in line_lower):
|
|
gpus["intel"] = True
|
|
# Extract name from CSV
|
|
parts = line.split(',')
|
|
for part in parts:
|
|
if 'Intel' in part:
|
|
gpus["intel_name"] = part.strip()
|
|
break
|
|
if gpus["intel_name"]:
|
|
print(f"[OK] Intel GPU: {gpus['intel_name']}")
|
|
elif 'amd' in line_lower or 'radeon' in line_lower:
|
|
gpus["amd"] = True
|
|
parts = line.split(',')
|
|
for part in parts:
|
|
if 'AMD' in part or 'Radeon' in part:
|
|
gpus["amd_name"] = part.strip()
|
|
break
|
|
if gpus["amd_name"]:
|
|
print(f"[OK] AMD GPU: {gpus['amd_name']}")
|
|
except Exception:
|
|
pass
|
|
else:
|
|
# Linux: use lspci
|
|
try:
|
|
result = subprocess.run(
|
|
["lspci"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10
|
|
)
|
|
for line in result.stdout.split('\n'):
|
|
if 'VGA' in line or 'Display' in line or '3D' in line:
|
|
if 'Intel' in line:
|
|
gpus["intel"] = True
|
|
gpus["intel_name"] = line.split(':')[-1].strip() if ':' in line else "Intel GPU"
|
|
print(f"[OK] Intel GPU: {gpus['intel_name']}")
|
|
elif 'AMD' in line or 'Radeon' in line:
|
|
gpus["amd"] = True
|
|
gpus["amd_name"] = line.split(':')[-1].strip() if ':' in line else "AMD GPU"
|
|
print(f"[OK] AMD GPU: {gpus['amd_name']}")
|
|
except Exception:
|
|
pass
|
|
|
|
if not gpus["nvidia"] and not gpus["intel"] and not gpus["amd"]:
|
|
print("[i] No GPU detected - will use CPU (NumPy)")
|
|
|
|
return gpus
|
|
|
|
|
|
def install_core_dependencies() -> bool:
|
|
"""Install core Python dependencies."""
|
|
print_step("Installing core dependencies...")
|
|
|
|
req_file = os.path.join(os.path.dirname(__file__), "backend", "requirements.txt")
|
|
if not os.path.exists(req_file):
|
|
print(f"[X] requirements.txt not found at {req_file}")
|
|
return False
|
|
|
|
try:
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", "-r", req_file,
|
|
"--quiet", "--no-warn-script-location"],
|
|
check=True,
|
|
timeout=600
|
|
)
|
|
print("[OK] Core dependencies installed")
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[X] pip install failed: {e}")
|
|
return False
|
|
except subprocess.TimeoutExpired:
|
|
print("[X] pip install timed out (10 min)")
|
|
return False
|
|
|
|
|
|
def install_gpu_dependencies(gpus: dict) -> bool:
|
|
"""Install GPU-specific dependencies based on detected hardware."""
|
|
print_step("Setting up GPU acceleration...")
|
|
|
|
gpu_installed = False
|
|
|
|
# NVIDIA - install CuPy (includes CUDA runtime)
|
|
if gpus["nvidia"]:
|
|
print(f" Installing CuPy for {gpus['nvidia_name']}...")
|
|
try:
|
|
# Try CUDA 12 first (newer cards, RTX 30xx/40xx)
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", "cupy-cuda12x",
|
|
"--quiet", "--no-warn-script-location"],
|
|
check=True,
|
|
timeout=600
|
|
)
|
|
print(f" [OK] CuPy (CUDA 12) installed")
|
|
gpu_installed = True
|
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
|
try:
|
|
# Fallback to CUDA 11 (older cards)
|
|
print(" [!] CUDA 12 failed, trying CUDA 11...")
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", "cupy-cuda11x",
|
|
"--quiet", "--no-warn-script-location"],
|
|
check=True,
|
|
timeout=600
|
|
)
|
|
print(f" [OK] CuPy (CUDA 11) installed")
|
|
gpu_installed = True
|
|
except Exception as e:
|
|
print(f" [X] CuPy installation failed: {e}")
|
|
print(f" Manual install: pip install cupy-cuda12x")
|
|
|
|
# Intel/AMD - install PyOpenCL
|
|
if gpus["intel"] or gpus["amd"]:
|
|
gpu_name = gpus["intel_name"] or gpus["amd_name"]
|
|
print(f" Installing PyOpenCL for {gpu_name}...")
|
|
try:
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", "pyopencl",
|
|
"--quiet", "--no-warn-script-location"],
|
|
check=True,
|
|
timeout=300
|
|
)
|
|
print(f" [OK] PyOpenCL installed")
|
|
gpu_installed = True
|
|
except Exception as e:
|
|
print(f" [X] PyOpenCL installation failed: {e}")
|
|
print(f" Manual install: pip install pyopencl")
|
|
|
|
if not gpu_installed and not gpus["nvidia"] and not gpus["intel"] and not gpus["amd"]:
|
|
print(" [i] No GPU acceleration - using CPU (NumPy)")
|
|
print(" This is fine! GPU just makes large calculations faster.")
|
|
|
|
return gpu_installed
|
|
|
|
|
|
def install_frontend(has_node: bool) -> bool:
|
|
"""Install frontend dependencies and build."""
|
|
if not has_node:
|
|
print_step("Skipping frontend build (Node.js not available)")
|
|
return False
|
|
|
|
print_step("Setting up frontend...")
|
|
frontend_dir = os.path.join(os.path.dirname(__file__), "frontend")
|
|
|
|
if not os.path.exists(os.path.join(frontend_dir, "package.json")):
|
|
print("[!] Frontend directory not found")
|
|
return False
|
|
|
|
try:
|
|
print(" Installing npm packages...")
|
|
subprocess.run(
|
|
["npm", "install"],
|
|
cwd=frontend_dir,
|
|
check=True,
|
|
timeout=300,
|
|
capture_output=True
|
|
)
|
|
print(" Building frontend...")
|
|
subprocess.run(
|
|
["npm", "run", "build"],
|
|
cwd=frontend_dir,
|
|
check=True,
|
|
timeout=300,
|
|
capture_output=True
|
|
)
|
|
print("[OK] Frontend built")
|
|
return True
|
|
except subprocess.CalledProcessError as e:
|
|
print(f"[X] Frontend build failed: {e}")
|
|
return False
|
|
except subprocess.TimeoutExpired:
|
|
print("[X] Frontend build timed out")
|
|
return False
|
|
|
|
|
|
def create_launcher() -> bool:
|
|
"""Create launcher scripts."""
|
|
print_step("Creating launcher scripts...")
|
|
|
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
if platform.system() == "Windows":
|
|
# Create RFCP.bat
|
|
launcher_path = os.path.join(base_dir, "RFCP.bat")
|
|
with open(launcher_path, 'w') as f:
|
|
f.write('@echo off\n')
|
|
f.write('title RFCP - RF Coverage Planner\n')
|
|
f.write(f'cd /d "{base_dir}"\n')
|
|
f.write('echo Starting RFCP...\n')
|
|
f.write('echo Open http://localhost:8090 in your browser\n')
|
|
f.write('echo Press Ctrl+C to stop\n')
|
|
f.write('echo.\n')
|
|
f.write(f'cd backend\n')
|
|
f.write(f'"{sys.executable}" -m uvicorn app.main:app --host 0.0.0.0 --port 8090\n')
|
|
print(f" [OK] Created: RFCP.bat")
|
|
|
|
# Create install.bat for first-time setup
|
|
install_bat_path = os.path.join(base_dir, "install.bat")
|
|
with open(install_bat_path, 'w') as f:
|
|
f.write('@echo off\n')
|
|
f.write('title RFCP - First Time Setup\n')
|
|
f.write('echo ============================================\n')
|
|
f.write('echo RFCP - RF Coverage Planner - Setup\n')
|
|
f.write('echo ============================================\n')
|
|
f.write('echo.\n')
|
|
f.write('python --version >nul 2>&1\n')
|
|
f.write('if errorlevel 1 (\n')
|
|
f.write(' echo ERROR: Python not found!\n')
|
|
f.write(' echo Please install Python 3.10+ from python.org\n')
|
|
f.write(' pause\n')
|
|
f.write(' exit /b 1\n')
|
|
f.write(')\n')
|
|
f.write(f'cd /d "{base_dir}"\n')
|
|
f.write('python install_rfcp.py\n')
|
|
f.write('echo.\n')
|
|
f.write('echo Setup complete! Run RFCP.bat to start.\n')
|
|
f.write('pause\n')
|
|
print(f" [OK] Created: install.bat")
|
|
else:
|
|
# Linux/macOS
|
|
launcher_path = os.path.join(base_dir, "rfcp.sh")
|
|
with open(launcher_path, 'w') as f:
|
|
f.write('#!/bin/bash\n')
|
|
f.write(f'cd "{base_dir}"\n')
|
|
f.write('echo "Starting RFCP..."\n')
|
|
f.write('echo "Open http://localhost:8090 in your browser"\n')
|
|
f.write('echo "Press Ctrl+C to stop"\n')
|
|
f.write('cd backend\n')
|
|
f.write(f'{sys.executable} -m uvicorn app.main:app --host 0.0.0.0 --port 8090\n')
|
|
os.chmod(launcher_path, 0o755)
|
|
print(f" [OK] Created: rfcp.sh")
|
|
|
|
return True
|
|
|
|
|
|
def verify_installation() -> bool:
|
|
"""Run quick verification tests."""
|
|
print_step("Verifying installation...")
|
|
|
|
checks = []
|
|
critical_fail = False
|
|
|
|
# Check core imports
|
|
try:
|
|
import numpy as np
|
|
checks.append(f"[OK] NumPy {np.__version__}")
|
|
except ImportError:
|
|
checks.append("[X] NumPy missing")
|
|
critical_fail = True
|
|
|
|
try:
|
|
import scipy
|
|
checks.append(f"[OK] SciPy {scipy.__version__}")
|
|
except ImportError:
|
|
checks.append("[X] SciPy missing")
|
|
critical_fail = True
|
|
|
|
try:
|
|
import fastapi
|
|
checks.append(f"[OK] FastAPI {fastapi.__version__}")
|
|
except ImportError:
|
|
checks.append("[X] FastAPI missing")
|
|
critical_fail = True
|
|
|
|
try:
|
|
import uvicorn
|
|
checks.append(f"[OK] Uvicorn {uvicorn.__version__}")
|
|
except ImportError:
|
|
checks.append("[X] Uvicorn missing")
|
|
critical_fail = True
|
|
|
|
# Check GPU acceleration
|
|
try:
|
|
import cupy as cp
|
|
device_count = cp.cuda.runtime.getDeviceCount()
|
|
if device_count > 0:
|
|
props = cp.cuda.runtime.getDeviceProperties(0)
|
|
name = props["name"]
|
|
if isinstance(name, bytes):
|
|
name = name.decode()
|
|
mem_mb = props["totalGlobalMem"] // (1024 * 1024)
|
|
checks.append(f"[OK] CuPy (CUDA) -> {name} ({mem_mb} MB)")
|
|
else:
|
|
checks.append("[i] CuPy installed but no CUDA devices found")
|
|
except ImportError:
|
|
checks.append("[i] CuPy not available (NVIDIA GPU acceleration disabled)")
|
|
except Exception as e:
|
|
checks.append(f"[!] CuPy error: {e}")
|
|
|
|
try:
|
|
import pyopencl as cl
|
|
devices = []
|
|
for p in cl.get_platforms():
|
|
for d in p.get_devices():
|
|
devices.append(d.name.strip())
|
|
if devices:
|
|
checks.append(f"[OK] PyOpenCL -> {', '.join(devices[:2])}")
|
|
else:
|
|
checks.append("[i] PyOpenCL installed but no devices found")
|
|
except ImportError:
|
|
checks.append("[i] PyOpenCL not available (Intel/AMD GPU acceleration disabled)")
|
|
except Exception as e:
|
|
checks.append(f"[!] PyOpenCL error: {e}")
|
|
|
|
for check in checks:
|
|
print(f" {check}")
|
|
|
|
return not critical_fail
|
|
|
|
|
|
def main():
|
|
"""Main installer entry point."""
|
|
print_header("RFCP - RF Coverage Planner - Installer")
|
|
|
|
# Step 1: Check prerequisites
|
|
print_step("Checking prerequisites...")
|
|
if not check_python():
|
|
print("\n[X] Python 3.10+ is required. Please install from python.org")
|
|
sys.exit(1)
|
|
|
|
has_node = check_node()
|
|
|
|
# Step 2: Detect GPU
|
|
print_step("Detecting GPU hardware...")
|
|
gpus = detect_gpu()
|
|
|
|
# Step 3: Install core dependencies
|
|
if not install_core_dependencies():
|
|
print("\n[X] Core dependency installation failed")
|
|
sys.exit(1)
|
|
|
|
# Step 4: Install GPU dependencies
|
|
install_gpu_dependencies(gpus)
|
|
|
|
# Step 5: Frontend (optional)
|
|
install_frontend(has_node)
|
|
|
|
# Step 6: Create launcher
|
|
create_launcher()
|
|
|
|
# Step 7: Verify
|
|
success = verify_installation()
|
|
|
|
# Summary
|
|
print_header("Installation Summary")
|
|
|
|
if success:
|
|
print(" [OK] RFCP installed successfully!")
|
|
print()
|
|
print(" To start RFCP:")
|
|
if platform.system() == "Windows":
|
|
print(" Double-click RFCP.bat")
|
|
print(" Or run: python -m uvicorn app.main:app --port 8090")
|
|
else:
|
|
print(" Run: ./rfcp.sh")
|
|
print(" Or: python -m uvicorn app.main:app --port 8090")
|
|
print()
|
|
print(" Then open: http://localhost:8090")
|
|
print()
|
|
|
|
# GPU summary
|
|
if gpus["nvidia"]:
|
|
print(f" GPU: {gpus['nvidia_name']} (CUDA)")
|
|
elif gpus["intel"]:
|
|
print(f" GPU: {gpus['intel_name']} (OpenCL)")
|
|
elif gpus["amd"]:
|
|
print(f" GPU: {gpus['amd_name']} (OpenCL)")
|
|
else:
|
|
print(" Mode: CPU only (NumPy)")
|
|
else:
|
|
print(" [!] Installation completed with errors")
|
|
print(" Some features may not work correctly")
|
|
|
|
print()
|
|
print('=' * 60)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|