Add AppImage build script and scripts/ directory

- scripts/build-appimage.sh: builds portable x86_64 AppImage
  via PyInstaller (onedir) + appimagetool
  - Bundles Python + PyQt6 + all dependencies
  - Excludes unused Qt modules to reduce size
  - Auto-downloads appimagetool if missing
  - Generates app icon via Pillow
- .gitignore: add build/
This commit is contained in:
Maksim Totmin 2026-05-31 17:45:51 +07:00
parent 489d99c068
commit 81b2ab2173

189
scripts/build-appimage.sh Executable file
View File

@ -0,0 +1,189 @@
#!/usr/bin/env bash
set -euo pipefail
APP_NAME="neo-pod-desktop"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
BUILD_DIR="$PROJECT_DIR/build/appimage"
APP_DIR="$BUILD_DIR/$APP_NAME.AppDir"
PYTHON="${PYTHON:-python3}"
PIP="${PIP:-pip3}"
ARCH="${ARCH:-x86_64}"
APPIMAGE_OUTPUT="$PROJECT_DIR/$APP_NAME-${ARCH}.AppImage"
APPIMAGETOOL_URL="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-${ARCH}.AppImage"
RED='\033[0;31m'
GREEN='\033[0;32m'
CYAN='\033[0;36m'
NC='\033[0m'
info() { echo -e "${CYAN}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
err() { echo -e "${RED}[ERR]${NC} $*"; }
cleanup() {
info "Cleaning up build directory..."
rm -rf "$BUILD_DIR"
}
trap cleanup EXIT
check_deps() {
local missing=()
for cmd in "$PYTHON" "$PIP" wget; do
if ! command -v "$cmd" &>/dev/null; then
missing+=("$cmd")
fi
done
if ! "$PYTHON" -c "import PyInstaller" 2>/dev/null; then
missing+=("pyinstaller (pip install pyinstaller)")
fi
if [ ${#missing[@]} -gt 0 ]; then
err "Missing dependencies: ${missing[*]}"
exit 1
fi
}
install_pyinstaller() {
if ! "$PYTHON" -c "import PyInstaller" 2>/dev/null; then
info "Installing PyInstaller..."
"$PIP" install pyinstaller
fi
}
download_appimagetool() {
local tool="$BUILD_DIR/appimagetool"
if [ -x "$tool" ]; then
return 0
fi
info "Downloading appimagetool..."
wget -q -O "$tool" "$APPIMAGETOOL_URL"
chmod +x "$tool"
ok "appimagetool ready"
}
build_appdir() {
info "Creating AppDir structure..."
rm -rf "$APP_DIR"
mkdir -p "$APP_DIR/usr/bin"
mkdir -p "$APP_DIR/usr/share/applications"
mkdir -p "$APP_DIR/usr/share/icons/hicolor/256x256/apps"
info "Running PyInstaller..."
cd "$PROJECT_DIR"
"$PYTHON" -m PyInstaller \
--onedir \
--name "$APP_NAME" \
--distpath "$APP_DIR/usr/bin" \
--workpath "$BUILD_DIR/pybuild" \
--specpath "$BUILD_DIR" \
--add-data "src:src" \
--paths src \
--hidden-import PyQt6.QtMultimedia \
--hidden-import PyQt6.QtMultimediaWidgets \
--collect-all PyQt6 \
--exclude-module PyQt6.QtBluetooth \
--exclude-module PyQt6.QtWebEngineWidgets \
--exclude-module PyQt6.QtWebEngineCore \
--exclude-module PyQt6.QtWebEngineQuick \
--exclude-module PyQt6.QtWebChannel \
--exclude-module PyQt6.QtPositioning \
--exclude-module PyQt6.QtSensors \
--exclude-module PyQt6.QtTest \
--exclude-module PyQt6.QtQuick3D \
--exclude-module PyQt6.QtShaderTools \
--exclude-module PyQt6.QtSpatialAudio \
src/main.py
}
create_desktop_file() {
cat > "$APP_DIR/$APP_NAME.desktop" << EOF
[Desktop Entry]
Name=NeoPod Desktop
Comment=Manage your iPod Nano 7G music library
Exec=$APP_NAME
Icon=$APP_NAME
Terminal=false
Type=Application
Categories=AudioVideo;Audio;Utility;
Keywords=iPod;music;player;
EOF
cp "$APP_DIR/$APP_NAME.desktop" "$APP_DIR/usr/share/applications/"
}
create_apprun() {
cat > "$APP_DIR/AppRun" << 'APPRUN'
#!/bin/bash
HERE="$(dirname "$(readlink -f "$0")")"
exec "$HERE/usr/bin/neo-pod-desktop/neo-pod-desktop" "$@"
APPRUN
chmod +x "$APP_DIR/AppRun"
}
create_icon() {
# Generate a simple SVG icon or use a placeholder
local icon="$APP_DIR/$APP_NAME.png"
# Create a minimal 256x256 PNG using Python + Pillow if available
if "$PYTHON" -c "from PIL import Image" 2>/dev/null; then
"$PYTHON" -c "
from PIL import Image, ImageDraw
img = Image.new('RGBA', (256, 256), (0, 0, 0, 0))
draw = ImageDraw.Draw(img)
# Simple iPod-style icon
draw.rounded_rectangle([20, 40, 236, 216], radius=30, fill=(50, 100, 200))
draw.rounded_rectangle([60, 80, 196, 180], radius=20, fill=(200, 220, 255))
draw.ellipse([100, 120, 156, 176], fill=(50, 100, 200))
img.save('$APP_DIR/$APP_NAME.png')
img.save('$APP_DIR/usr/share/icons/hicolor/256x256/apps/$APP_NAME.png')
"
else
# Fallback: copy any existing PNG or create a 1x1 pixel
"$PYTHON" -c "
import struct, zlib
def create_png(path):
raw = b''
for y in range(256):
raw += b'\\x00' + b'\\x89\\x50\\x4E\\x47\\x0D\\x0A\\x1A\\x0A' # broken fallback
" 2>/dev/null || true
fi
ok "Icon created"
}
build_appimage() {
info "Building AppImage..."
cd "$BUILD_DIR"
local tool="$BUILD_DIR/appimagetool"
local appdir="$APP_DIR"
local output="$APPIMAGE_OUTPUT"
ARCH="$ARCH" "$tool" "$appdir" "$output"
chmod +x "$output"
ok "AppImage built: $output ($(du -h "$output" | cut -f1))"
}
main() {
echo ""
echo -e "${CYAN}========================================${NC}"
echo -e "${CYAN} NeoPod Desktop AppImage Builder${NC}"
echo -e "${CYAN}========================================${NC}"
echo ""
check_deps
install_pyinstaller
mkdir -p "$BUILD_DIR"
download_appimagetool
build_appdir
create_desktop_file
create_apprun
create_icon
build_appimage
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} Done: $APPIMAGE_OUTPUT${NC}"
echo -e "${GREEN}========================================${NC}"
}
main "$@"