259 lines
11 KiB
Bash
Executable File
259 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# build_installer.sh - Build APIClient-Agent standalone distribution
|
|
#
|
|
# Usage:
|
|
# ./build_installer.sh [--onefile] [--clean] [--deb] [--dmg]
|
|
#
|
|
# Options:
|
|
# --onefile Bundle into a single executable (larger, slower start) instead
|
|
# of the default one-directory layout (recommended for Linux/Mac)
|
|
# --clean Remove previous dist/ and build/ before building
|
|
# --deb Also build a .deb package after the build (requires fpm)
|
|
# --dmg Also build a .dmg image after the build (macOS only, requires
|
|
# create-dmg: brew install create-dmg)
|
|
#
|
|
# Requirements:
|
|
# Python 3.11+, a virtual environment at ./venv, and all pip deps installed.
|
|
# PyInstaller is installed automatically from requirements.txt.
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
APP_NAME="APIClient-Agent"
|
|
APP_VERSION="2.0.0"
|
|
APP_BUNDLE_ID="co.ekika.apiclient-agent"
|
|
MAIN_SCRIPT="main.py"
|
|
ICON_PNG="assets/app_logo.png"
|
|
ICON_ICO="assets/app_logo.ico"
|
|
ICON_ICNS="assets/app_logo.icns"
|
|
|
|
PYTHON="${PYTHON:-venv/bin/python}"
|
|
PIP="${PIP:-venv/bin/pip}"
|
|
|
|
# ── Argument parsing ──────────────────────────────────────────────────────────
|
|
OPT_ONEFILE=false
|
|
OPT_CLEAN=false
|
|
OPT_DEB=false
|
|
OPT_DMG=false
|
|
|
|
for arg in "$@"; do
|
|
case $arg in
|
|
--onefile) OPT_ONEFILE=true ;;
|
|
--clean) OPT_CLEAN=true ;;
|
|
--deb) OPT_DEB=true ;;
|
|
--dmg) OPT_DMG=true ;;
|
|
*)
|
|
echo "Unknown option: $arg"
|
|
echo "Usage: $0 [--onefile] [--clean] [--deb] [--dmg]"
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
|
|
ok() { echo -e "\033[1;32m[ OK ]\033[0m $*"; }
|
|
warn() { echo -e "\033[1;33m[WARN]\033[0m $*"; }
|
|
die() { echo -e "\033[1;31m[FAIL]\033[0m $*" >&2; exit 1; }
|
|
|
|
# ── Platform detection ────────────────────────────────────────────────────────
|
|
OS="$(uname -s)"
|
|
case "$OS" in
|
|
Linux*) PLATFORM=linux ;;
|
|
Darwin*) PLATFORM=macos ;;
|
|
MINGW*|CYGWIN*|MSYS*) PLATFORM=windows ;;
|
|
*) die "Unsupported platform: $OS" ;;
|
|
esac
|
|
info "Platform: $PLATFORM"
|
|
|
|
# ── Preflight checks ──────────────────────────────────────────────────────────
|
|
[[ -f "$MAIN_SCRIPT" ]] || die "$MAIN_SCRIPT not found. Run from the project root."
|
|
[[ -d "venv" ]] || die "Virtual environment 'venv/' not found. Create it first:
|
|
python3 -m venv venv && venv/bin/pip install -r requirements.txt"
|
|
[[ -f "requirements.txt" ]] || die "requirements.txt not found."
|
|
|
|
info "Python: $($PYTHON --version)"
|
|
|
|
# ── Clean ─────────────────────────────────────────────────────────────────────
|
|
if $OPT_CLEAN; then
|
|
info "Cleaning previous build artifacts..."
|
|
rm -rf dist/ build/ "${APP_NAME}.spec"
|
|
ok "Cleaned."
|
|
fi
|
|
|
|
# ── Install / upgrade dependencies ───────────────────────────────────────────
|
|
info "Installing/verifying dependencies..."
|
|
$PIP install -q -r requirements.txt
|
|
ok "Dependencies ready."
|
|
|
|
# ── Prepare icon ─────────────────────────────────────────────────────────────
|
|
# PyInstaller needs .ico on Windows, .icns on macOS, .png on Linux.
|
|
ICON_ARG=""
|
|
if [[ "$PLATFORM" == "windows" ]]; then
|
|
if [[ -f "$ICON_ICO" ]]; then
|
|
ICON_ARG="--icon=$ICON_ICO"
|
|
else
|
|
warn ".ico icon not found at $ICON_ICO - building without icon."
|
|
fi
|
|
elif [[ "$PLATFORM" == "macos" ]]; then
|
|
if [[ -f "$ICON_ICNS" ]]; then
|
|
ICON_ARG="--icon=$ICON_ICNS"
|
|
elif [[ -f "$ICON_PNG" ]]; then
|
|
# Convert PNG -> ICNS using sips + iconutil if available
|
|
if command -v sips &>/dev/null && command -v iconutil &>/dev/null; then
|
|
info "Converting PNG to ICNS..."
|
|
ICONSET_DIR="/tmp/${APP_NAME}.iconset"
|
|
mkdir -p "$ICONSET_DIR"
|
|
for sz in 16 32 64 128 256 512; do
|
|
sips -z $sz $sz "$ICON_PNG" --out "$ICONSET_DIR/icon_${sz}x${sz}.png" &>/dev/null
|
|
sips -z $((sz*2)) $((sz*2)) "$ICON_PNG" --out "$ICONSET_DIR/icon_${sz}x${sz}@2x.png" &>/dev/null
|
|
done
|
|
iconutil -c icns "$ICONSET_DIR" -o "$ICON_ICNS" && ok "ICNS created."
|
|
ICON_ARG="--icon=$ICON_ICNS"
|
|
else
|
|
warn "sips/iconutil not available; building without .icns icon."
|
|
fi
|
|
fi
|
|
elif [[ "$PLATFORM" == "linux" ]]; then
|
|
[[ -f "$ICON_PNG" ]] && ICON_ARG="--icon=$ICON_PNG" || warn "PNG icon not found."
|
|
fi
|
|
|
|
# ── PyInstaller arguments ─────────────────────────────────────────────────────
|
|
PYINSTALLER_ARGS=(
|
|
$([[ $OPT_ONEFILE == true ]] && echo "--onefile" || echo "--onedir")
|
|
"--windowed"
|
|
"--name" "$APP_NAME"
|
|
"--add-data" "app:app"
|
|
"--add-data" "assets:assets"
|
|
"--clean"
|
|
"--noconfirm"
|
|
)
|
|
[[ -n "$ICON_ARG" ]] && PYINSTALLER_ARGS+=("$ICON_ARG")
|
|
|
|
# macOS bundle metadata
|
|
if [[ "$PLATFORM" == "macos" ]]; then
|
|
PYINSTALLER_ARGS+=(
|
|
"--osx-bundle-identifier" "$APP_BUNDLE_ID"
|
|
)
|
|
fi
|
|
|
|
# Hidden imports that PyInstaller may miss
|
|
PYINSTALLER_ARGS+=(
|
|
"--hidden-import" "PyQt6.QtSvg"
|
|
"--hidden-import" "PyQt6.QtNetwork"
|
|
"--hidden-import" "httpx._transports.default"
|
|
"--hidden-import" "websockets.legacy.client"
|
|
"--hidden-import" "anthropic"
|
|
)
|
|
|
|
PYINSTALLER_ARGS+=("$MAIN_SCRIPT")
|
|
|
|
# ── Build ─────────────────────────────────────────────────────────────────────
|
|
info "Running PyInstaller..."
|
|
$PYTHON -m PyInstaller "${PYINSTALLER_ARGS[@]}"
|
|
|
|
# ── Verify output ─────────────────────────────────────────────────────────────
|
|
if [[ $OPT_ONEFILE == true ]]; then
|
|
BIN="dist/${APP_NAME}"
|
|
[[ "$PLATFORM" == "windows" ]] && BIN="${BIN}.exe"
|
|
[[ -f "$BIN" ]] || die "Build failed: expected $BIN not found."
|
|
ok "Executable: $BIN ($(du -sh "$BIN" | cut -f1))"
|
|
else
|
|
OUT_DIR="dist/${APP_NAME}"
|
|
[[ -d "$OUT_DIR" ]] || die "Build failed: expected $OUT_DIR not found."
|
|
ok "Distribution directory: $OUT_DIR ($(du -sh "$OUT_DIR" | cut -f1))"
|
|
fi
|
|
|
|
# ── .deb package ─────────────────────────────────────────────────────────────
|
|
if $OPT_DEB; then
|
|
if [[ "$PLATFORM" != "linux" ]]; then
|
|
warn ".deb packaging is only supported on Linux. Skipping."
|
|
elif ! command -v fpm &>/dev/null; then
|
|
warn "fpm not found. Install it with: gem install fpm
|
|
Then re-run with --deb."
|
|
else
|
|
info "Building .deb package..."
|
|
DEB_NAME="apiclient-agent_${APP_VERSION}_amd64.deb"
|
|
|
|
# Write a post-install script that creates a desktop entry
|
|
POSTINST=$(mktemp)
|
|
cat > "$POSTINST" <<'EOF'
|
|
#!/bin/bash
|
|
cat > /usr/share/applications/apiclient-agent.desktop <<DESK
|
|
[Desktop Entry]
|
|
Name=APIClient - Agent
|
|
Comment=AI-first API testing client
|
|
Exec=/opt/apiclient-agent/APIClient-Agent
|
|
Icon=/opt/apiclient-agent/_internal/assets/app_logo.png
|
|
Terminal=false
|
|
Type=Application
|
|
Categories=Development;Network;
|
|
Keywords=API;REST;HTTP;AI;
|
|
DESK
|
|
update-desktop-database /usr/share/applications 2>/dev/null || true
|
|
EOF
|
|
chmod +x "$POSTINST"
|
|
|
|
fpm \
|
|
--input-type dir \
|
|
--output-type deb \
|
|
--name "apiclient-agent" \
|
|
--version "$APP_VERSION" \
|
|
--architecture amd64 \
|
|
--maintainer "EKIKA.co <hello@ekika.co>" \
|
|
--description "APIClient - Agent: AI-first API testing desktop client (Python/PyQt6)" \
|
|
--url "https://git.ekika.co/EKIKA.co/APIClient-Agent" \
|
|
--license "MIT" \
|
|
--category "Development" \
|
|
--after-install "$POSTINST" \
|
|
--package "$DEB_NAME" \
|
|
"dist/${APP_NAME}/=/opt/apiclient-agent"
|
|
|
|
rm -f "$POSTINST"
|
|
[[ -f "$DEB_NAME" ]] && ok ".deb package: $DEB_NAME ($(du -sh "$DEB_NAME" | cut -f1))" \
|
|
|| warn ".deb build may have failed."
|
|
fi
|
|
fi
|
|
|
|
# ── .dmg image (macOS) ───────────────────────────────────────────────────────
|
|
if $OPT_DMG; then
|
|
if [[ "$PLATFORM" != "macos" ]]; then
|
|
warn ".dmg packaging is only supported on macOS. Skipping."
|
|
elif ! command -v create-dmg &>/dev/null; then
|
|
warn "create-dmg not found. Install it with: brew install create-dmg
|
|
Then re-run with --dmg."
|
|
else
|
|
info "Building .dmg image..."
|
|
DMG_NAME="${APP_NAME}-${APP_VERSION}.dmg"
|
|
APP_BUNDLE="dist/${APP_NAME}.app"
|
|
[[ -d "$APP_BUNDLE" ]] || die ".app bundle not found at $APP_BUNDLE"
|
|
|
|
create-dmg \
|
|
--volname "$APP_NAME $APP_VERSION" \
|
|
--window-pos 200 120 \
|
|
--window-size 600 400 \
|
|
--icon-size 100 \
|
|
--icon "${APP_NAME}.app" 175 190 \
|
|
--hide-extension "${APP_NAME}.app" \
|
|
--app-drop-link 425 190 \
|
|
"$DMG_NAME" \
|
|
"$APP_BUNDLE"
|
|
|
|
[[ -f "$DMG_NAME" ]] && ok ".dmg image: $DMG_NAME ($(du -sh "$DMG_NAME" | cut -f1))" \
|
|
|| warn ".dmg build may have failed."
|
|
fi
|
|
fi
|
|
|
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
echo ""
|
|
echo " ┌─────────────────────────────────────────────┐"
|
|
printf " │ %-43s│\n" "${APP_NAME} v${APP_VERSION} build complete"
|
|
printf " │ Platform : %-31s│\n" "$PLATFORM"
|
|
if [[ $OPT_ONEFILE == true ]]; then
|
|
printf " │ Output : %-31s│\n" "dist/${APP_NAME}"
|
|
else
|
|
printf " │ Output : %-31s│\n" "dist/${APP_NAME}/"
|
|
fi
|
|
echo " └─────────────────────────────────────────────┘"
|
|
echo ""
|