diff --git a/edge-ai-platform/Makefile b/edge-ai-platform/Makefile index 9b8cde3..3a88170 100644 --- a/edge-ai-platform/Makefile +++ b/edge-ai-platform/Makefile @@ -130,10 +130,16 @@ installer-payload: build-server-tray ## Stage payload files for GUI installer cp scripts/kneron_detect.py installer/payload/scripts/ cp server/scripts/firmware/KL520/*.bin installer/payload/scripts/firmware/KL520/ cp server/scripts/firmware/KL720/*.bin installer/payload/scripts/firmware/KL720/ - @# Copy KneronPLUS wheel if available (for Windows installer) + @# Copy KneronPLUS wheels if available (Windows from local_service_win, macOS from Downloads) @if ls ../local_service_win/KneronPLUS*.whl 1>/dev/null 2>&1; then \ cp ../local_service_win/KneronPLUS*.whl installer/payload/scripts/; \ - echo " KneronPLUS wheel bundled."; \ + echo " KneronPLUS Windows wheel bundled."; \ + fi + @mkdir -p installer/payload/scripts/macos + @MACOS_WHL="$$(find ~/Downloads/KL520Web/package/macos -name 'KneronPLUS*.whl' 2>/dev/null | head -1)"; \ + if [ -n "$$MACOS_WHL" ]; then \ + cp "$$MACOS_WHL" installer/payload/scripts/macos/; \ + echo " KneronPLUS macOS wheel bundled."; \ fi @# Copy WinUSB driver files (for Windows installer) @mkdir -p installer/payload/drivers/amd64 @@ -150,6 +156,10 @@ installer: installer-payload ## Build GUI installer app codesign --force --deep --sign - installer/build/bin/EdgeAI-Installer.app @echo "Installer built and signed! Check installer/build/" +installer-linux: installer-payload ## Build GUI installer for Linux (Ubuntu) + cd installer && wails build -clean + @echo "Installer built! Check installer/build/bin/EdgeAI-Installer" + installer-dev: installer-payload ## Run GUI installer in dev mode cd installer && wails dev diff --git a/edge-ai-platform/installer/app.go b/edge-ai-platform/installer/app.go index 7eec41f..2f8022a 100644 --- a/edge-ai-platform/installer/app.go +++ b/edge-ai-platform/installer/app.go @@ -399,58 +399,6 @@ func (inst *Installer) extractDir(embedDir, destDir string) error { }) } -// installPython3Windows attempts to install Python 3 via winget on Windows. -func (inst *Installer) installPython3Windows() error { - if runtime.GOOS != "windows" { - return fmt.Errorf("not windows") - } - - // Check if winget is available - if _, err := exec.LookPath("winget"); err != nil { - return fmt.Errorf("winget not found — please install Python 3 manually from https://python.org") - } - - inst.emitProgress(ProgressEvent{ - Step: "python", - Message: "Installing Python 3 via winget...", - Percent: 73, - }) - - cmd := exec.Command("winget", "install", "Python.Python.3.12", - "--source", "winget", - "--accept-source-agreements", "--accept-package-agreements", "--silent") - if out, err := cmd.CombinedOutput(); err != nil { - return fmt.Errorf("winget install Python failed: %s — %w", string(out), err) - } - - // winget installs Python to %LOCALAPPDATA%\Programs\Python\Python312\ - // We need to add it to PATH for the current process - pythonDir := filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Python", "Python312") - scriptsDir := filepath.Join(pythonDir, "Scripts") - os.Setenv("PATH", pythonDir+";"+scriptsDir+";"+os.Getenv("PATH")) - - // Verify it works - if _, err := findPython3(); err != nil { - // Try common alternative paths - for _, ver := range []string{"Python313", "Python311", "Python310"} { - altDir := filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Python", ver) - if _, err := os.Stat(filepath.Join(altDir, "python.exe")); err == nil { - os.Setenv("PATH", altDir+";"+filepath.Join(altDir, "Scripts")+";"+os.Getenv("PATH")) - break - } - } - if _, err := findPython3(); err != nil { - return fmt.Errorf("Python installed but not found in PATH — please restart the installer") - } - } - - inst.emitProgress(ProgressEvent{ - Step: "python", - Message: "Python 3 installed successfully.", - Percent: 75, - }) - return nil -} // setupPythonVenv creates a Python virtual environment and installs requirements. func (inst *Installer) setupPythonVenv(installDir string) error { @@ -459,17 +407,12 @@ func (inst *Installer) setupPythonVenv(installDir string) error { pythonPath, err := findPython3() if err != nil { - // On Windows, try to install Python automatically - if runtime.GOOS == "windows" { - if installErr := inst.installPython3Windows(); installErr != nil { - return fmt.Errorf("python3 not found and auto-install failed: %w", installErr) - } - pythonPath, err = findPython3() - if err != nil { - return fmt.Errorf("python3 still not found after installation: %w", err) - } - } else { - return fmt.Errorf("python3 not found on PATH: %w", err) + if installErr := inst.autoInstallPython3(); installErr != nil { + return fmt.Errorf("python3 not found and auto-install failed: %w", installErr) + } + pythonPath, err = findPython3() + if err != nil { + return fmt.Errorf("python3 still not found after installation: %w", err) } } diff --git a/edge-ai-platform/installer/platform_darwin.go b/edge-ai-platform/installer/platform_darwin.go index 8f46fcd..ea38bf2 100644 --- a/edge-ai-platform/installer/platform_darwin.go +++ b/edge-ai-platform/installer/platform_darwin.go @@ -48,6 +48,31 @@ func removeSystemLink() { os.Remove("/usr/local/bin/edge-ai-server") } +// autoInstallPython3 attempts to install Python 3 via Homebrew on macOS. +func (inst *Installer) autoInstallPython3() error { + if _, err := exec.LookPath("brew"); err != nil { + return fmt.Errorf("Homebrew not found — please install Python 3: brew install python3 (or https://brew.sh)") + } + + inst.emitProgress(ProgressEvent{ + Step: "python", + Message: "Installing Python 3 via Homebrew...", + Percent: 73, + }) + + cmd := exec.Command("brew", "install", "python@3.12") + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("brew install python failed: %s — %w", string(out), err) + } + + inst.emitProgress(ProgressEvent{ + Step: "python", + Message: "Python 3 installed successfully.", + Percent: 75, + }) + return nil +} + func installLibusb(installDir string) error { if _, err := exec.LookPath("brew"); err != nil { return fmt.Errorf("Homebrew not found. Install from https://brew.sh then retry") @@ -104,6 +129,11 @@ func installAutoRestart(installDir string) error { ` ` + binPath + ``, ` --tray`, ` `, + ` EnvironmentVariables`, + ` `, + ` PATH`, + ` /usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin`, + ` `, ` WorkingDirectory`, ` ` + installDir + ``, ` KeepAlive`, diff --git a/edge-ai-platform/installer/platform_linux.go b/edge-ai-platform/installer/platform_linux.go index efa1840..2906fa6 100644 --- a/edge-ai-platform/installer/platform_linux.go +++ b/edge-ai-platform/installer/platform_linux.go @@ -109,6 +109,33 @@ func installAutoRestart(installDir string) error { return nil } +// autoInstallPython3 attempts to install Python 3 + venv via apt-get on Ubuntu/Debian. +func (inst *Installer) autoInstallPython3() error { + if _, err := exec.LookPath("apt-get"); err != nil { + return fmt.Errorf("apt-get not found — please install python3 and python3-venv manually") + } + + inst.emitProgress(ProgressEvent{ + Step: "python", + Message: "Installing Python 3 via apt-get (may require authentication)...", + Percent: 73, + }) + + // Install python3, python3-venv, and pip in one command + cmd := exec.Command("pkexec", "apt-get", "install", "-y", + "python3", "python3-venv", "python3-pip") + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("apt-get install python3 failed: %s — %w", string(out), err) + } + + inst.emitProgress(ProgressEvent{ + Step: "python", + Message: "Python 3 installed successfully.", + Percent: 75, + }) + return nil +} + func installFfmpeg(inst *Installer, needFfmpeg, needYtdlp bool) error { if _, err := exec.LookPath("apt-get"); err != nil { return fmt.Errorf("package manager not found — please install ffmpeg manually") diff --git a/edge-ai-platform/installer/platform_windows.go b/edge-ai-platform/installer/platform_windows.go index 7b8b343..322549f 100644 --- a/edge-ai-platform/installer/platform_windows.go +++ b/edge-ai-platform/installer/platform_windows.go @@ -70,6 +70,50 @@ func removeSystemLink() { fmt.Sprintf(`[Environment]::SetEnvironmentVariable("PATH", "%s", "User")`, newPath)).Run() } +// autoInstallPython3 attempts to install Python 3 via winget on Windows. +func (inst *Installer) autoInstallPython3() error { + if _, err := exec.LookPath("winget"); err != nil { + return fmt.Errorf("winget not found — please install Python 3 manually from https://python.org") + } + + inst.emitProgress(ProgressEvent{ + Step: "python", + Message: "Installing Python 3 via winget...", + Percent: 73, + }) + + cmd := exec.Command("winget", "install", "Python.Python.3.12", + "--source", "winget", + "--accept-source-agreements", "--accept-package-agreements", "--silent") + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("winget install Python failed: %s — %w", string(out), err) + } + + // winget installs Python to %LOCALAPPDATA%\Programs\Python\Python312\ + // We need to add it to PATH for the current process + pythonDir := filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Python", "Python312") + scriptsDir := filepath.Join(pythonDir, "Scripts") + os.Setenv("PATH", pythonDir+";"+scriptsDir+";"+os.Getenv("PATH")) + + // Verify it works — try common alternative version paths + if _, err := findPython3(); err != nil { + for _, ver := range []string{"Python313", "Python311", "Python310"} { + altDir := filepath.Join(os.Getenv("LOCALAPPDATA"), "Programs", "Python", ver) + if _, err := os.Stat(filepath.Join(altDir, "python.exe")); err == nil { + os.Setenv("PATH", altDir+";"+filepath.Join(altDir, "Scripts")+";"+os.Getenv("PATH")) + break + } + } + } + + inst.emitProgress(ProgressEvent{ + Step: "python", + Message: "Python 3 installed successfully.", + Percent: 75, + }) + return nil +} + func installLibusb(installDir string) error { // 1. Extract libusb-1.0.dll from payload dllDest := filepath.Join(installDir, "libusb-1.0.dll") diff --git a/edge-ai-platform/scripts/build-installer-linux.sh b/edge-ai-platform/scripts/build-installer-linux.sh new file mode 100755 index 0000000..8a1dece --- /dev/null +++ b/edge-ai-platform/scripts/build-installer-linux.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +# Edge AI Platform - Linux (Ubuntu) Installer Build Script +# Usage: bash scripts/build-installer-linux.sh +# +# Prerequisites: Go 1.23+, Node.js 20+, pnpm, Wails CLI +# sudo apt-get install -y build-essential libgtk-3-dev libwebkit2gtk-4.0-dev +# go install github.com/wailsapp/wails/v2/cmd/wails@latest +# +# Run from edge-ai-platform/ directory + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BASE_DIR="$(dirname "$SCRIPT_DIR")" +REPO_ROOT="$(dirname "$BASE_DIR")" + +cd "$BASE_DIR" + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +err() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } +step() { echo -e "\n${CYAN}=== $* ===${NC}\n"; } + +echo "" +info "Edge AI Platform - Linux Installer Build" +echo "" + +# ── Step 1/5: Check prerequisites ── + +step "1/5 Checking prerequisites" + +missing=() +command -v go >/dev/null 2>&1 || missing+=("Go (https://go.dev/dl/)") +command -v node >/dev/null 2>&1 || missing+=("Node.js (https://nodejs.org/)") +command -v pnpm >/dev/null 2>&1 || missing+=("pnpm (npm install -g pnpm)") +command -v wails >/dev/null 2>&1 || missing+=("Wails (go install github.com/wailsapp/wails/v2/cmd/wails@latest)") + +# Check Wails Linux dependencies +pkg-config --exists gtk+-3.0 2>/dev/null || missing+=("libgtk-3-dev (sudo apt-get install libgtk-3-dev)") +pkg-config --exists webkit2gtk-4.0 2>/dev/null || missing+=("libwebkit2gtk-4.0-dev (sudo apt-get install libwebkit2gtk-4.0-dev)") + +if [ ${#missing[@]} -gt 0 ]; then + echo -e "${RED}Missing prerequisites:${NC}" + for m in "${missing[@]}"; do + echo -e " ${RED}- $m${NC}" + done + exit 1 +fi + +info "Go: $(go version)" +info "Node: $(node --version)" +info "pnpm: $(pnpm --version)" +info "Wails: $(wails version 2>&1 | grep -oP 'v[\d.]+')" +echo "" + +# ── Step 2/5: Build frontend ── + +step "2/5 Building frontend" + +cd frontend +pnpm install --frozen-lockfile +pnpm build +rm -rf ../server/web/out +cp -r out ../server/web/out +cd "$BASE_DIR" +info "Frontend built." + +# ── Step 3/5: Build server ── + +step "3/5 Building server" + +mkdir -p dist +cd server +CGO_ENABLED=1 go build \ + -ldflags="-s -w" \ + -o ../dist/edge-ai-server \ + main.go +cd "$BASE_DIR" + +size=$(du -m dist/edge-ai-server | cut -f1) +info "Server built: dist/edge-ai-server (${size} MB)" + +# ── Step 4/5: Stage installer payload ── + +step "4/5 Staging installer payload" + +rm -rf installer/payload +mkdir -p installer/payload/data/nef/kl520 +mkdir -p installer/payload/data/nef/kl720 +mkdir -p installer/payload/scripts/firmware/KL520 +mkdir -p installer/payload/scripts/firmware/KL720 + +cp dist/edge-ai-server installer/payload/ +cp server/data/models.json installer/payload/data/ +cp server/data/nef/kl520/*.nef installer/payload/data/nef/kl520/ 2>/dev/null || true +cp server/data/nef/kl720/*.nef installer/payload/data/nef/kl720/ 2>/dev/null || true +cp server/scripts/kneron_bridge.py installer/payload/scripts/ +cp server/scripts/requirements.txt installer/payload/scripts/ +cp server/scripts/update_kl720_firmware.py installer/payload/scripts/ +cp scripts/kneron_detect.py installer/payload/scripts/ +cp server/scripts/firmware/KL520/*.bin installer/payload/scripts/firmware/KL520/ 2>/dev/null || true +cp server/scripts/firmware/KL720/*.bin installer/payload/scripts/firmware/KL720/ 2>/dev/null || true + +# Copy KneronPLUS wheel if available +kp_wheel=$(find "$REPO_ROOT" -maxdepth 2 -name 'KneronPLUS*linux*.whl' 2>/dev/null | head -1) +if [ -n "$kp_wheel" ]; then + cp "$kp_wheel" installer/payload/scripts/ + info "KneronPLUS wheel bundled: $(basename "$kp_wheel")" +else + warn "KneronPLUS wheel not found, skipping." +fi + +file_count=$(find installer/payload -type f | wc -l) +info "Payload staged: $file_count files." + +# ── Step 5/5: Build Wails installer ── + +step "5/5 Building Wails installer" + +cd installer +wails build -clean +cd "$BASE_DIR" + +output="installer/build/bin/EdgeAI-Installer" +if [ -f "$output" ]; then + size=$(du -m "$output" | cut -f1) + echo "" + echo -e "${CYAN}=== Build complete! ===${NC}" + info "Output: $BASE_DIR/$output (${size} MB)" + echo "" + info "To run the installer:" + info " chmod +x $output && ./$output" +else + err "Build output not found!" +fi