[RELEASE] Final version.
This commit is contained in:
@@ -1,34 +1,258 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
#!/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
|
||||
|
||||
echo "=== Building API Client installer ==="
|
||||
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"
|
||||
|
||||
# Install deps
|
||||
pip install -r requirements.txt
|
||||
PYTHON="${PYTHON:-venv/bin/python}"
|
||||
PIP="${PIP:-venv/bin/pip}"
|
||||
|
||||
# Build with PyInstaller
|
||||
pyinstaller \
|
||||
--onedir \
|
||||
--windowed \
|
||||
--name "APIClient" \
|
||||
--add-data "app:app" \
|
||||
main.py
|
||||
# ── Argument parsing ──────────────────────────────────────────────────────────
|
||||
OPT_ONEFILE=false
|
||||
OPT_CLEAN=false
|
||||
OPT_DEB=false
|
||||
OPT_DMG=false
|
||||
|
||||
echo ""
|
||||
echo "=== Build complete ==="
|
||||
echo "Executable: dist/APIClient/APIClient"
|
||||
echo ""
|
||||
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
|
||||
|
||||
# Optional: create .deb (requires fpm: gem install fpm)
|
||||
if command -v fpm &> /dev/null; then
|
||||
echo "Creating .deb package..."
|
||||
fpm -s dir -t deb \
|
||||
-n api-client \
|
||||
-v 1.0.0 \
|
||||
--description "Postman-like API client" \
|
||||
dist/APIClient/=/opt/api-client \
|
||||
--after-install /dev/null
|
||||
echo "Package: api-client_1.0.0_amd64.deb"
|
||||
else
|
||||
echo "Tip: install fpm (gem install fpm) to also generate a .deb package"
|
||||
# ── 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 ""
|
||||
|
||||
Reference in New Issue
Block a user