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