feat: add Linux (Ubuntu) GUI installer build script
- Add scripts/build-installer-linux.sh for building the Wails GUI installer on Ubuntu (checks for GTK3 and WebKit2GTK dependencies) - Refactor autoInstallPython3() into per-platform files so each OS can auto-install Python 3 (winget on Windows, apt-get on Linux, brew on macOS) - Add installer-linux target to Makefile Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c558c9e3b8
commit
e46d179165
@ -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
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
` <string>` + binPath + `</string>`,
|
||||
` <string>--tray</string>`,
|
||||
` </array>`,
|
||||
` <key>EnvironmentVariables</key>`,
|
||||
` <dict>`,
|
||||
` <key>PATH</key>`,
|
||||
` <string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>`,
|
||||
` </dict>`,
|
||||
` <key>WorkingDirectory</key>`,
|
||||
` <string>` + installDir + `</string>`,
|
||||
` <key>KeepAlive</key>`,
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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")
|
||||
|
||||
141
edge-ai-platform/scripts/build-installer-linux.sh
Executable file
141
edge-ai-platform/scripts/build-installer-linux.sh
Executable file
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user