diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b728f9d..31892cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,10 @@ jobs: libappindicator3-dev \ librsvg2-dev \ patchelf \ - pkg-config + pkg-config \ + imagemagick \ + wget \ + squashfs-tools # Create symlink for webkit2gtk-4.0 if it doesn't exist (Wails looks for 4.0) for PKG_DIR in /usr/lib/x86_64-linux-gnu/pkgconfig /usr/lib/pkgconfig /usr/share/pkgconfig; do if [ -d "$PKG_DIR" ] && [ -f "${PKG_DIR}/webkit2gtk-4.1.pc" ] && [ ! -f "${PKG_DIR}/webkit2gtk-4.0.pc" ]; then @@ -145,3 +148,9 @@ jobs: else test -f build/bin/pubsub-gui && echo "✅ Linux binary created" || (echo "❌ Linux binary not found" && exit 1) fi + + - name: Verify AppImage creation (Linux) + if: matrix.os == 'ubuntu-latest' + shell: bash + run: | + ./scripts/create-appimage.sh ci-test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8a0c39..c04e829 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,6 +25,9 @@ jobs: - os: macos-15 platform: darwin/arm64 artifact_name: darwin_arm64 + - os: macos-15 + platform: darwin/amd64 + artifact_name: darwin_amd64 - os: ubuntu-latest platform: linux/amd64 artifact_name: linux_amd64 @@ -70,7 +73,10 @@ jobs: libappindicator3-dev \ librsvg2-dev \ patchelf \ - pkg-config + pkg-config \ + imagemagick \ + wget \ + squashfs-tools # Create symlink for webkit2gtk-4.0 if it doesn't exist (Wails looks for 4.0) # Check common pkg-config locations for PKG_DIR in /usr/lib/x86_64-linux-gnu/pkgconfig /usr/lib/pkgconfig /usr/share/pkgconfig; do @@ -121,6 +127,21 @@ jobs: run: | wails build -ldflags "-X main.version=$VERSION -X internal/version.Version=$VERSION" -platform ${{ matrix.platform }} -clean + - name: Create AppImage (Linux) + if: matrix.os == 'ubuntu-latest' + shell: bash + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + # Use shared script for AppImage creation (single source of truth) + ./scripts/create-appimage.sh "$VERSION" + + # Also create regular tar.gz for users who prefer it + cd build/bin + tar -czf "../../pubsub-gui_linux_amd64_${VERSION}.tar.gz" pubsub-gui + cd ../.. + echo "✓ Created tar.gz: pubsub-gui_linux_amd64_${VERSION}.tar.gz" + - name: Package artifacts (macOS/Linux) if: matrix.os != 'windows-latest' shell: bash @@ -133,12 +154,8 @@ jobs: cd build/bin tar -czf "../../pubsub-gui_${ARTIFACT_NAME}_${VERSION}.tar.gz" pubsub-gui.app cd ../.. - else - # Package binary for Linux - cd build/bin - tar -czf "../../pubsub-gui_${ARTIFACT_NAME}_${VERSION}.tar.gz" pubsub-gui - cd ../.. fi + # Note: Linux packaging is done in the AppImage step above - name: Package artifacts (Windows) if: matrix.os == 'windows-latest' @@ -161,6 +178,7 @@ jobs: path: | pubsub-gui_${{ matrix.artifact_name }}_*.tar.gz pubsub-gui_${{ matrix.artifact_name }}_*.zip + pubsub-gui_${{ matrix.artifact_name }}_*.AppImage release: name: Create Release @@ -203,7 +221,7 @@ jobs: - name: Create checksums run: | cd artifacts - find . -maxdepth 1 \( -name "*.tar.gz" -o -name "*.zip" \) | while read file; do + find . -maxdepth 1 \( -name "*.tar.gz" -o -name "*.zip" -o -name "*.AppImage" \) | while read file; do if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" >> pubsub-gui_${{ steps.version.outputs.version }}_checksums.txt else @@ -232,10 +250,51 @@ jobs: - **macOS (Intel)**: `pubsub-gui_darwin_amd64_*.tar.gz` - **macOS (Apple Silicon)**: `pubsub-gui_darwin_arm64_*.tar.gz` - **Windows**: `pubsub-gui_windows_amd64_*.zip` - - **Linux**: `pubsub-gui_linux_amd64_*.tar.gz` + - **Linux**: + - **AppImage (Recommended)**: `pubsub-gui_linux_amd64_*.AppImage` - No dependencies required! + - **tar.gz**: `pubsub-gui_linux_amd64_*.tar.gz` - Requires runtime libraries (see below) + + ### Installation Instructions + + **macOS:** + Extract the `.app` bundle to `/Applications` or `~/Applications` + + **Windows:** + Extract and run the `pubsub-gui.exe` binary + + **Linux - AppImage (Recommended):** + ```bash + chmod +x pubsub-gui_linux_amd64_*.AppImage + ./pubsub-gui_linux_amd64_*.AppImage + ``` + The AppImage bundles all dependencies - no additional installation required! + + **Linux - tar.gz (Alternative):** + Extract and run the `pubsub-gui` binary. Requires the following runtime libraries: + - `libwebkit2gtk-4.1-0` (or `webkit2gtk-4.1` on Arch) + - `libgtk-3-0` (or `gtk3` on Arch/RHEL) + - `libappindicator3-1` (or `libappindicator-gtk3` on Arch/RHEL) + + **Install on Debian/Ubuntu:** + ```bash + sudo apt-get update + sudo apt-get install libwebkit2gtk-4.1-0 libgtk-3-0 libappindicator3-1 + ``` + + **Install on RHEL/CentOS/Fedora:** + ```bash + sudo dnf install webkit2gtk4 gtk3 libappindicator-gtk3 + ``` + + **Install on Arch Linux:** + ```bash + sudo pacman -S webkit2gtk-4.1 gtk3 libappindicator-gtk3 + ``` - Extract the archive: - - **macOS**: Extract the `.app` bundle to `/Applications` or `~/Applications` - - **Windows/Linux**: Extract and run the `pubsub-gui` binary + **Or use the automated installer:** + ```bash + curl -fsSL https://raw.githubusercontent.com/b87/pubsub-gui/main/scripts/install.sh | bash + ``` + The installer will automatically detect and install missing dependencies. env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 71302f7..5a53f9c 100644 --- a/README.md +++ b/README.md @@ -48,11 +48,47 @@ curl -fsSL https://raw.githubusercontent.com/b87/pubsub-gui/main/scripts/install 1. Download the appropriate binary for your platform from the [Releases](https://github.com/b87/pubsub-gui/releases) page: - **macOS**: `pubsub-gui_darwin_amd64_*.tar.gz` or `pubsub-gui_darwin_arm64_*.tar.gz` - **Windows**: `pubsub-gui_windows_amd64_*.zip` - - **Linux**: `pubsub-gui_linux_amd64_*.tar.gz` or `pubsub-gui_linux_arm64_*.tar.gz` + - **Linux**: + - **AppImage (Recommended)**: `pubsub-gui_linux_amd64_*.AppImage` - No dependencies required! + - **tar.gz**: `pubsub-gui_linux_amd64_*.tar.gz` - Requires runtime libraries (see below) -2. Extract the archive and run the `pubsub-gui` binary +2. **Linux - AppImage (Recommended):** + ```bash + chmod +x pubsub-gui_linux_amd64_*.AppImage + ./pubsub-gui_linux_amd64_*.AppImage + ``` + The AppImage bundles all dependencies - no additional installation required! + +3. **Linux - tar.gz (Alternative):** + Extract the archive and run the `pubsub-gui` binary. + + **Linux Runtime Dependencies** (for tar.gz only): + + The Linux binary requires the following runtime libraries: + - `libwebkit2gtk-4.1-0` (Debian/Ubuntu) or `webkit2gtk4.1` (RHEL/CentOS/Fedora) or `webkit2gtk-4.1` (Arch) + - `libgtk-3-0` (Debian/Ubuntu) or `gtk3` (RHEL/CentOS/Fedora/Arch) + - `libappindicator3-1` (Debian/Ubuntu) or `libappindicator-gtk3` (RHEL/CentOS/Fedora/Arch) + + **Install on Debian/Ubuntu:** + ```bash + sudo apt-get update + sudo apt-get install libwebkit2gtk-4.1-0 libgtk-3-0 libappindicator3-1 + ``` + + **Install on RHEL/CentOS/Fedora:** + ```bash + sudo dnf install webkit2gtk4.1 gtk3 libappindicator-gtk3 + ``` + > **Note:** `webkit2gtk4.1` is available on Fedora 40+ and RHEL/CentOS 10+. For older versions (RHEL/CentOS 8-9, Fedora ≤39), only `webkit2gtk3` (4.0 ABI) is available, which may not be compatible. + + **Install on Arch Linux:** + ```bash + sudo pacman -S webkit2gtk-4.1 gtk3 libappindicator-gtk3 + ``` + + If you see an error like `libwebkit2gtk-4.1.so.0: cannot open shared object file`, install the missing dependencies above. -3. (Optional) Add the binary to your PATH for global access +4. (Optional) Add the binary to your PATH for global access ## 🎯 Quick Start diff --git a/Taskfile.yaml b/Taskfile.yaml index 8fd2b82..5d9dbc2 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -112,6 +112,16 @@ tasks: - task: build:windows - task: build:linux + build:appimage: + desc: Create Linux AppImage (requires Linux with GTK dev libs) + vars: + VERSION: '{{.VERSION | default "dev"}}' + preconditions: + - sh: test "$(uname -s)" = "Linux" + msg: "AppImage creation is only supported on Linux" + cmds: + - ./scripts/create-appimage.sh {{.VERSION}} + # ============================================================================ # Testing & Code Quality # ============================================================================ diff --git a/build/appicon.iconset/icon_128x128.png b/build/appicon.iconset/icon_128x128.png deleted file mode 100644 index 4e4a66f..0000000 Binary files a/build/appicon.iconset/icon_128x128.png and /dev/null differ diff --git a/build/appicon.iconset/icon_128x128@2x.png b/build/appicon.iconset/icon_128x128@2x.png deleted file mode 100644 index 78807b6..0000000 Binary files a/build/appicon.iconset/icon_128x128@2x.png and /dev/null differ diff --git a/build/appicon.iconset/icon_16x16.png b/build/appicon.iconset/icon_16x16.png deleted file mode 100644 index b82f5e3..0000000 Binary files a/build/appicon.iconset/icon_16x16.png and /dev/null differ diff --git a/build/appicon.iconset/icon_16x16@2x.png b/build/appicon.iconset/icon_16x16@2x.png deleted file mode 100644 index b9dd702..0000000 Binary files a/build/appicon.iconset/icon_16x16@2x.png and /dev/null differ diff --git a/build/appicon.iconset/icon_256x256.png b/build/appicon.iconset/icon_256x256.png deleted file mode 100644 index da63c3b..0000000 Binary files a/build/appicon.iconset/icon_256x256.png and /dev/null differ diff --git a/build/appicon.iconset/icon_256x256@2x.png b/build/appicon.iconset/icon_256x256@2x.png deleted file mode 100644 index 3528d20..0000000 Binary files a/build/appicon.iconset/icon_256x256@2x.png and /dev/null differ diff --git a/build/appicon.iconset/icon_32x32.png b/build/appicon.iconset/icon_32x32.png deleted file mode 100644 index b9dd702..0000000 Binary files a/build/appicon.iconset/icon_32x32.png and /dev/null differ diff --git a/build/appicon.iconset/icon_32x32@2x.png b/build/appicon.iconset/icon_32x32@2x.png deleted file mode 100644 index 038c49f..0000000 Binary files a/build/appicon.iconset/icon_32x32@2x.png and /dev/null differ diff --git a/build/appicon.iconset/icon_512x512.png b/build/appicon.iconset/icon_512x512.png deleted file mode 100644 index 74492e9..0000000 Binary files a/build/appicon.iconset/icon_512x512.png and /dev/null differ diff --git a/build/appicon.iconset/icon_512x512@2x.png b/build/appicon.iconset/icon_512x512@2x.png deleted file mode 100644 index 629f136..0000000 Binary files a/build/appicon.iconset/icon_512x512@2x.png and /dev/null differ diff --git a/build/appicon.iconset/icon_64x64.png b/build/appicon.iconset/icon_64x64.png deleted file mode 100644 index 038c49f..0000000 Binary files a/build/appicon.iconset/icon_64x64.png and /dev/null differ diff --git a/build/appicon.iconset/icon_64x64@2x.png b/build/appicon.iconset/icon_64x64@2x.png deleted file mode 100644 index 4e4a66f..0000000 Binary files a/build/appicon.iconset/icon_64x64@2x.png and /dev/null differ diff --git a/build/appicon.png b/build/appicon.png index 74492e9..0307a5b 100644 Binary files a/build/appicon.png and b/build/appicon.png differ diff --git a/scripts/create-appimage.sh b/scripts/create-appimage.sh new file mode 100755 index 0000000..37d9aef --- /dev/null +++ b/scripts/create-appimage.sh @@ -0,0 +1,362 @@ +#!/usr/bin/env bash +# +# Create AppImage for Pub/Sub GUI +# +# Usage: +# ./scripts/create-appimage.sh [VERSION] +# +# Arguments: +# VERSION - Version string (e.g., v1.0.0). Defaults to "dev" +# +# Environment Variables: +# BINARY_PATH - Path to the built binary (default: build/bin/pubsub-gui) +# ICON_PATH - Path to the icon file (default: build/appicon.png) +# OUTPUT_DIR - Output directory for AppImage (default: current directory) +# SKIP_CHECKSUM - Set to "1" to skip checksum verification (not recommended) +# +# Prerequisites: +# - Linux with GTK3 development libraries +# - wget, sha256sum, imagemagick (for placeholder icon) +# - squashfs-tools (for mksquashfs, required by appimagetool) +# - Built binary at BINARY_PATH +# + +set -euo pipefail + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# ============================================================================= +# Configuration - Pinned versions for reproducibility and security +# ============================================================================= + +# linuxdeploy: using specific release tag with SHA256 verification +LINUXDEPLOY_VERSION="1-alpha-20251107-1" +LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/${LINUXDEPLOY_VERSION}/linuxdeploy-x86_64.AppImage" +LINUXDEPLOY_SHA256_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/${LINUXDEPLOY_VERSION}/linuxdeploy-x86_64.AppImage.sha256" + +# GTK plugin: pinned to specific commit (commit hash provides content integrity) +GTK_PLUGIN_COMMIT="3b67a1d1c1b0c8268f57f2bce40fe2d33d409cea" +GTK_PLUGIN_URL="https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/${GTK_PLUGIN_COMMIT}/linuxdeploy-plugin-gtk.sh" +# Pre-computed SHA256 for the GTK plugin at this commit (13654 bytes) +GTK_PLUGIN_EXPECTED_SHA256="b0f4cbc684a0103a9651f0955b635eaea0096b3a66c0f5a2c2aa337960375171" + +# ============================================================================= +# Parse arguments and set defaults +# ============================================================================= + +VERSION="${1:-dev}" +BINARY_PATH="${BINARY_PATH:-build/bin/pubsub-gui}" +ICON_PATH="${ICON_PATH:-build/appicon.png}" +OUTPUT_DIR="${OUTPUT_DIR:-.}" +SKIP_CHECKSUM="${SKIP_CHECKSUM:-0}" + +# Ensure OUTPUT_DIR exists and is absolute +OUTPUT_DIR=$(cd "$OUTPUT_DIR" && pwd) +mkdir -p "$OUTPUT_DIR" + +# Working directory for downloads +WORK_DIR=$(mktemp -d) +trap "rm -rf '$WORK_DIR'" EXIT + +# ============================================================================= +# Helper functions +# ============================================================================= + +log_info() { + echo -e "${BLUE}$1${NC}" +} + +log_success() { + echo -e "${GREEN}$1${NC}" +} + +log_warn() { + echo -e "${YELLOW}$1${NC}" +} + +log_error() { + echo -e "${RED}$1${NC}" >&2 +} + +# ============================================================================= +# Validation +# ============================================================================= + +log_info "Creating AppImage for Pub/Sub GUI ${VERSION}" +echo "" + +# Check we're on Linux +if [[ "$(uname -s)" != "Linux" ]]; then + log_error "Error: AppImage creation is only supported on Linux" + exit 1 +fi + +# Check binary exists +if [[ ! -f "$BINARY_PATH" ]]; then + log_error "Error: Binary not found at $BINARY_PATH" + log_error "Run 'wails build -platform linux/amd64' first" + exit 1 +fi + +log_success "Binary found: $BINARY_PATH" + +# ============================================================================= +# Download and verify linuxdeploy +# ============================================================================= + +log_info "Downloading linuxdeploy ${LINUXDEPLOY_VERSION}..." +if ! wget -q "$LINUXDEPLOY_URL" -O "$WORK_DIR/linuxdeploy-x86_64.AppImage" 2>&1; then + log_error "Error: Failed to download linuxdeploy" + log_error "URL: $LINUXDEPLOY_URL" + log_error "Please verify the version exists and the URL is correct" + exit 1 +fi + +# Try to download checksum file (may not exist for all releases) +CHECKSUM_AVAILABLE=false +if wget -q "$LINUXDEPLOY_SHA256_URL" -O "$WORK_DIR/linuxdeploy-x86_64.AppImage.sha256" 2>&1; then + CHECKSUM_AVAILABLE=true + log_success "Downloaded linuxdeploy checksum" +else + log_warn "Checksum file not available for this release (this is normal for some releases)" + log_warn "Skipping checksum verification" +fi + +if [[ "$SKIP_CHECKSUM" != "1" ]] && [[ "$CHECKSUM_AVAILABLE" == "true" ]]; then + log_info "Verifying linuxdeploy checksum..." + cd "$WORK_DIR" + sha256sum -c linuxdeploy-x86_64.AppImage.sha256 || { + log_error "Error: linuxdeploy checksum verification failed!" + exit 1 + } + cd - > /dev/null + log_success "linuxdeploy checksum verified" +elif [[ "$SKIP_CHECKSUM" == "1" ]]; then + log_warn "Skipping linuxdeploy checksum verification (SKIP_CHECKSUM=1)" +fi + +# ============================================================================= +# Download and verify GTK plugin +# ============================================================================= + +log_info "Downloading linuxdeploy-plugin-gtk (commit ${GTK_PLUGIN_COMMIT:0:7})..." +wget -q "$GTK_PLUGIN_URL" -O "$WORK_DIR/linuxdeploy-plugin-gtk.sh" || { + log_error "Error: Failed to download GTK plugin" + exit 1 +} + +# Verify download isn't empty (catches bad commit hashes, 404s, etc.) +if [[ ! -s "$WORK_DIR/linuxdeploy-plugin-gtk.sh" ]]; then + log_error "Error: Downloaded GTK plugin is empty (bad URL or commit hash?)" + exit 1 +fi + +if [[ "$SKIP_CHECKSUM" != "1" ]]; then + log_info "Verifying GTK plugin checksum..." + GTK_PLUGIN_ACTUAL_SHA256=$(sha256sum "$WORK_DIR/linuxdeploy-plugin-gtk.sh" | cut -d' ' -f1) + if [[ "$GTK_PLUGIN_ACTUAL_SHA256" != "$GTK_PLUGIN_EXPECTED_SHA256" ]]; then + log_error "Error: GTK plugin checksum verification failed!" + log_error "Expected: $GTK_PLUGIN_EXPECTED_SHA256" + log_error "Actual: $GTK_PLUGIN_ACTUAL_SHA256" + log_error "The GTK plugin may have been modified. Update GTK_PLUGIN_EXPECTED_SHA256 if this is expected." + exit 1 + fi + log_success "GTK plugin checksum verified" +else + log_warn "Skipping GTK plugin checksum verification (SKIP_CHECKSUM=1)" +fi + +chmod +x "$WORK_DIR/linuxdeploy-x86_64.AppImage" "$WORK_DIR/linuxdeploy-plugin-gtk.sh" + +# ============================================================================= +# Prepare desktop file +# ============================================================================= + +log_info "Creating desktop file..." +cat > "$WORK_DIR/pubsub-gui.desktop" << 'EOF' +[Desktop Entry] +Type=Application +Name=PubSub GUI +Comment=Google Cloud Pub/Sub Management Tool +Exec=pubsub-gui +Icon=pubsub-gui +Categories=Development; +Terminal=false +EOF + +# ============================================================================= +# Prepare icon +# ============================================================================= + +if [[ ! -f "$ICON_PATH" ]]; then + log_warn "Icon not found at $ICON_PATH, creating placeholder..." + if command -v convert >/dev/null 2>&1; then + convert -size 256x256 xc:'#4A90D9' \ + -gravity center -pointsize 48 -fill white \ + -annotate 0 'PS' \ + "$ICON_PATH" + log_success "Created placeholder icon" + else + log_error "Error: ImageMagick (convert) not available to create placeholder icon" + log_error "Either install imagemagick or provide an icon at $ICON_PATH" + exit 1 + fi +fi + +if [[ ! -s "$ICON_PATH" ]]; then + log_error "Error: Icon file exists but is empty: $ICON_PATH" + exit 1 +fi + +log_success "Using icon: $ICON_PATH" + +# Copy icon to working directory with the name expected by desktop file +# The desktop file specifies Icon=pubsub-gui, so linuxdeploy expects pubsub-gui.png +ICON_EXT="${ICON_PATH##*.}" +ICON_FOR_LINUXDEPLOY="$WORK_DIR/pubsub-gui.${ICON_EXT}" +cp "$ICON_PATH" "$ICON_FOR_LINUXDEPLOY" +log_info "Icon copied to: $ICON_FOR_LINUXDEPLOY (matching desktop file Icon entry)" + +# ============================================================================= +# Create AppImage +# ============================================================================= + +log_info "Creating AppImage..." + +# Save original working directory +ORIGINAL_PWD=$(pwd) + +# Ensure OUTPUT_DIR exists and is writable +log_info "Output directory: $OUTPUT_DIR" +mkdir -p "$OUTPUT_DIR" +if [[ ! -w "$OUTPUT_DIR" ]]; then + log_error "Error: Output directory is not writable: $OUTPUT_DIR" + exit 1 +fi + +# Convert BINARY_PATH to absolute path if it's relative +if [[ "$BINARY_PATH" != /* ]]; then + ABS_BINARY_PATH="$ORIGINAL_PWD/$BINARY_PATH" +else + ABS_BINARY_PATH="$BINARY_PATH" +fi + +# Set TMPDIR to a writable location (appimagetool/mksquashfs uses this for temporary files) +# Prefer OUTPUT_DIR as it's guaranteed to be writable +export TMPDIR="$OUTPUT_DIR/.appimage-tmp" +mkdir -p "$TMPDIR" +if [[ ! -w "$TMPDIR" ]]; then + # Fallback to system temp if OUTPUT_DIR/.appimage-tmp isn't writable + export TMPDIR="${TMPDIR_SYSTEM:-/tmp}" + mkdir -p "$TMPDIR" + if [[ ! -w "$TMPDIR" ]]; then + log_error "Error: Cannot create writable temporary directory" + exit 1 + fi + log_info "Using system temp for temporary files: $TMPDIR" +else + log_info "Using OUTPUT_DIR for temporary files: $TMPDIR" +fi + +# Change to OUTPUT_DIR - linuxdeploy creates AppImage in current working directory +log_info "Changing to output directory: $OUTPUT_DIR" +cd "$OUTPUT_DIR" || { + log_error "Error: Cannot change to output directory: $OUTPUT_DIR" + exit 1 +} + +# Copy GTK plugin to OUTPUT_DIR (linuxdeploy looks for it in current directory) +cp "$WORK_DIR/linuxdeploy-plugin-gtk.sh" ./linuxdeploy-plugin-gtk.sh +chmod +x ./linuxdeploy-plugin-gtk.sh + +log_info "Working directory: $(pwd)" +log_info "Temporary directory: $TMPDIR" + +# Verify mksquashfs is available +if ! command -v mksquashfs >/dev/null 2>&1; then + log_error "Error: mksquashfs not found. Please install squashfs-tools:" + log_error " sudo apt-get install squashfs-tools" + exit 1 +fi +log_info "mksquashfs found: $(which mksquashfs)" + +# Verify we can write to current directory +if [[ ! -w . ]]; then + log_error "Error: Current directory is not writable: $(pwd)" + exit 1 +fi + +# Test write access by creating a test file +TEST_FILE=".appimage-test-$$" +if ! touch "$TEST_FILE" 2>/dev/null; then + log_error "Error: Cannot create files in current directory: $(pwd)" + exit 1 +fi +rm -f "$TEST_FILE" +log_info "Verified write access to current directory" + +export DEPLOY_GTK_VERSION=3 +"$WORK_DIR/linuxdeploy-x86_64.AppImage" \ + --appdir "$WORK_DIR/AppDir" \ + --executable "$ABS_BINARY_PATH" \ + --desktop-file "$WORK_DIR/pubsub-gui.desktop" \ + --icon-file "$ICON_FOR_LINUXDEPLOY" \ + --plugin gtk \ + --output appimage + +# Clean up GTK plugin from OUTPUT_DIR +rm -f ./linuxdeploy-plugin-gtk.sh + +# Clean up temporary directory if we created it in OUTPUT_DIR +if [[ "$TMPDIR" == "$OUTPUT_DIR/.appimage-tmp" ]]; then + rm -rf "$TMPDIR" +fi + +# Return to original directory +cd "$ORIGINAL_PWD" || { + log_error "Error: Cannot return to original directory: $ORIGINAL_PWD" + exit 1 +} + +# ============================================================================= +# Rename and move output +# ============================================================================= + +# Find the generated AppImage in OUTPUT_DIR (linuxdeploy creates it in CWD where we ran it) +# The name is based on the desktop file Name field (PubSub_GUI-x86_64.AppImage) +GENERATED_APPIMAGE=$(ls -1 "${OUTPUT_DIR}"/PubSub_GUI*.AppImage 2>/dev/null || ls -1 "${OUTPUT_DIR}"/*.AppImage 2>/dev/null | head -1) + +if [[ -z "$GENERATED_APPIMAGE" ]]; then + log_error "Error: AppImage was not created in output directory" + log_error "Output directory: $OUTPUT_DIR" + log_error "Contents of output directory:" + ls -la "$OUTPUT_DIR" || true + exit 1 +fi + +OUTPUT_NAME="pubsub-gui_linux_amd64_${VERSION}.AppImage" +OUTPUT_PATH="${OUTPUT_DIR}/${OUTPUT_NAME}" + +# Rename if the generated name is different +if [[ "$(basename "$GENERATED_APPIMAGE")" != "$OUTPUT_NAME" ]]; then + log_info "Renaming AppImage: $(basename "$GENERATED_APPIMAGE") -> $OUTPUT_NAME" + mv "$GENERATED_APPIMAGE" "$OUTPUT_PATH" +else + OUTPUT_PATH="$GENERATED_APPIMAGE" +fi + +chmod +x "$OUTPUT_PATH" + +# ============================================================================= +# Summary +# ============================================================================= + +echo "" +log_success "AppImage created successfully!" +log_success "Output: ${OUTPUT_DIR}/${OUTPUT_NAME}" +ls -lh "${OUTPUT_DIR}/${OUTPUT_NAME}" diff --git a/scripts/generate-icons.sh b/scripts/generate-icons.sh index bca5a87..368c372 100755 --- a/scripts/generate-icons.sh +++ b/scripts/generate-icons.sh @@ -26,9 +26,10 @@ else IMAGEMAGICK_CMD="convert" fi -# Generate PNG icon (512x512) for macOS +# Generate PNG icon (512x512) for macOS and Linux AppImage +# Resize to fit within 512x512 (preserving aspect ratio), then extend to exactly 512x512 square echo "📱 Generating PNG icon (512x512)..." -$IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 512x512 "$BUILD_DIR/appicon.png" +$IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 512x512 -gravity center -extent 512x512 "$BUILD_DIR/appicon.png" # Generate Windows ICO file (multiple sizes) echo "🪟 Generating Windows ICO file..." @@ -53,31 +54,45 @@ if [[ "$OSTYPE" == "darwin"* ]]; then echo "🍎 Generating macOS ICNS file..." ICONSET_DIR="$BUILD_DIR/appicon.iconset" + # Clean up any existing iconset directory + rm -rf "$ICONSET_DIR" mkdir -p "$ICONSET_DIR" - # Generate all required sizes for ICNS - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 512x512 "$ICONSET_DIR/icon_512x512.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 256x256 "$ICONSET_DIR/icon_256x256.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 128x128 "$ICONSET_DIR/icon_128x128.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 64x64 "$ICONSET_DIR/icon_64x64.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 32x32 "$ICONSET_DIR/icon_32x32.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 16x16 "$ICONSET_DIR/icon_16x16.png" + # Ensure output directory exists + mkdir -p "$BUILD_DIR/bin/pubsub-gui.app/Contents/Resources" + + # Generate all required sizes for ICNS (ensure they're exactly square) + # Standard sizes + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 512x512 -gravity center -extent 512x512 "$ICONSET_DIR/icon_512x512.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 256x256 -gravity center -extent 256x256 "$ICONSET_DIR/icon_256x256.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 128x128 -gravity center -extent 128x128 "$ICONSET_DIR/icon_128x128.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 64x64 -gravity center -extent 64x64 "$ICONSET_DIR/icon_64x64.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 32x32 -gravity center -extent 32x32 "$ICONSET_DIR/icon_32x32.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 16x16 -gravity center -extent 16x16 "$ICONSET_DIR/icon_16x16.png" # Generate @2x versions - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 1024x1024 "$ICONSET_DIR/icon_512x512@2x.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 512x512 "$ICONSET_DIR/icon_256x256@2x.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 256x256 "$ICONSET_DIR/icon_128x128@2x.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 128x128 "$ICONSET_DIR/icon_64x64@2x.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 64x64 "$ICONSET_DIR/icon_32x32@2x.png" - $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 32x32 "$ICONSET_DIR/icon_16x16@2x.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 1024x1024 -gravity center -extent 1024x1024 "$ICONSET_DIR/icon_512x512@2x.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 512x512 -gravity center -extent 512x512 "$ICONSET_DIR/icon_256x256@2x.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 256x256 -gravity center -extent 256x256 "$ICONSET_DIR/icon_128x128@2x.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 128x128 -gravity center -extent 128x128 "$ICONSET_DIR/icon_64x64@2x.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 64x64 -gravity center -extent 64x64 "$ICONSET_DIR/icon_32x32@2x.png" + $IMAGEMAGICK_CMD "$SVG_ICON" -background none -resize 32x32 -gravity center -extent 32x32 "$ICONSET_DIR/icon_16x16@2x.png" # Create ICNS file - iconutil -c icns "$ICONSET_DIR" -o "$BUILD_DIR/bin/pubsub-gui.app/Contents/Resources/iconfile.icns" + if iconutil -c icns "$ICONSET_DIR" -o "$BUILD_DIR/bin/pubsub-gui.app/Contents/Resources/iconfile.icns" 2>&1; then + echo "✅ macOS ICNS file generated!" + else + echo "❌ Failed to generate ICNS file" + echo " Checking iconset directory contents..." + ls -la "$ICONSET_DIR" || true + echo " Attempting to diagnose issue..." + # Clean up on failure + rm -rf "$ICONSET_DIR" + exit 1 + fi # Clean up iconset directory rm -rf "$ICONSET_DIR" - - echo "✅ macOS ICNS file generated!" else echo "⚠️ Skipping ICNS generation (macOS only)" fi diff --git a/scripts/install.sh b/scripts/install.sh index 66d5801..0616952 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -8,6 +8,22 @@ # - Automatic platform detection (Linux, macOS, Windows) # - SHA256 checksum verification for downloaded archives # - Support for latest release or specific version +# - Linux: AppImage by default (self-contained, no dependencies needed) +# +# Environment Variables: +# - PUBSUB_GUI_REPO_URL: Override the GitHub repository URL +# - PUBSUB_GUI_INSTALL_DIR: Override the installation directory +# - PUBSUB_GUI_USE_TARBALL: Set to "1" to use tar.gz instead of AppImage on Linux +# +# Examples: +# # Install latest version (AppImage on Linux) +# curl -fsSL https://raw.githubusercontent.com/b87/pubsub-gui/main/scripts/install.sh | bash +# +# # Install specific version +# curl -fsSL https://raw.githubusercontent.com/b87/pubsub-gui/main/scripts/install.sh | bash -s -- v1.2.0 +# +# # Force tar.gz instead of AppImage on Linux +# curl -fsSL https://raw.githubusercontent.com/b87/pubsub-gui/main/scripts/install.sh | PUBSUB_GUI_USE_TARBALL=1 bash # set -euo pipefail @@ -30,10 +46,17 @@ detect_platform() { local os="" local arch="" local ext="tar.gz" + local use_appimage="false" case "$(uname -s)" in Linux*) os="linux" + # Default to AppImage for Linux (self-contained, no dependencies needed) + # Can be overridden with PUBSUB_GUI_USE_TARBALL=1 + if [ "${PUBSUB_GUI_USE_TARBALL:-}" != "1" ]; then + use_appimage="true" + ext="AppImage" + fi ;; Darwin*) os="darwin" @@ -54,9 +77,19 @@ detect_platform() { ;; arm64|aarch64) arch="arm64" + # AppImage currently only available for amd64 + if [ "$os" = "linux" ]; then + use_appimage="false" + ext="tar.gz" + fi ;; armv7l|armv7) arch="armv7" + # AppImage not available for armv7 + if [ "$os" = "linux" ]; then + use_appimage="false" + ext="tar.gz" + fi ;; *) echo -e "${RED}Error: Unsupported architecture: $(uname -m)${NC}" >&2 @@ -73,6 +106,7 @@ detect_platform() { PLATFORM_OS="$os" PLATFORM_ARCH="$arch" ARCHIVE_EXT="$ext" + USE_APPIMAGE="$use_appimage" } # Normalize GitHub repository URL (remove www, ensure https://github.com format) @@ -239,6 +273,183 @@ verify_checksum() { fi } +# Check and install Linux runtime dependencies +check_linux_dependencies() { + if [ "$PLATFORM_OS" != "linux" ]; then + return 0 + fi + + echo -e "${BLUE}Checking Linux runtime dependencies...${NC}" + + # Check if required libraries are available + local missing_deps=() + + # Function to check if a library exists + check_library() { + local lib_name="$1" + + # Try ldconfig first (most reliable) + if command -v ldconfig >/dev/null 2>&1; then + if ldconfig -p 2>/dev/null | grep -q "${lib_name}.so"; then + return 0 + fi + fi + + # Fallback: check common library paths + local lib_paths=( + "/usr/lib" + "/usr/lib64" + "/usr/local/lib" + "/usr/local/lib64" + "/lib" + "/lib64" + ) + + for lib_path in "${lib_paths[@]}"; do + if [ -f "${lib_path}/${lib_name}.so" ] || [ -f "${lib_path}/${lib_name}.so.0" ] || \ + find "${lib_path}" -name "${lib_name}.so*" 2>/dev/null | grep -q .; then + return 0 + fi + done + + return 1 + } + + # Check for webkit2gtk-4.1 (most critical) + if ! check_library "libwebkit2gtk-4.1"; then + missing_deps+=("webkit2gtk-4.1") + fi + + # Check for GTK3 + if ! check_library "libgtk-3"; then + missing_deps+=("gtk-3") + fi + + # Check for appindicator3 + if ! check_library "libappindicator3"; then + missing_deps+=("appindicator3") + fi + + if [ ${#missing_deps[@]} -eq 0 ]; then + echo -e "${GREEN}✓ All runtime dependencies are installed${NC}" + return 0 + fi + + echo -e "${YELLOW}Missing runtime dependencies detected: ${missing_deps[*]}${NC}" + echo -e "${YELLOW}Attempting to install missing dependencies...${NC}" + + # Detect package manager and install dependencies + if command -v apt-get >/dev/null 2>&1; then + # Debian/Ubuntu + local packages=() + for dep in "${missing_deps[@]}"; do + case "$dep" in + webkit2gtk-4.1) + packages+=("libwebkit2gtk-4.1-0") + ;; + gtk-3) + packages+=("libgtk-3-0") + ;; + appindicator3) + packages+=("libappindicator3-1") + ;; + esac + done + + if [ ${#packages[@]} -gt 0 ]; then + echo -e "${BLUE}Installing: ${packages[*]}${NC}" + if [ "$EUID" -eq 0 ]; then + apt-get update -qq && apt-get install -y "${packages[@]}" || { + echo -e "${YELLOW}Warning: Failed to install dependencies automatically${NC}" + echo -e "${YELLOW}Please install manually: sudo apt-get install ${packages[*]}${NC}" + return 1 + } + else + echo -e "${YELLOW}Root privileges required to install dependencies${NC}" + echo -e "${YELLOW}Please run: sudo apt-get update && sudo apt-get install ${packages[*]}${NC}" + return 1 + fi + fi + elif command -v yum >/dev/null 2>&1 || command -v dnf >/dev/null 2>&1; then + # RHEL/CentOS/Fedora + local packages=() + for dep in "${missing_deps[@]}"; do + case "$dep" in + webkit2gtk-4.1) + packages+=("webkit2gtk4") + ;; + gtk-3) + packages+=("gtk3") + ;; + appindicator3) + packages+=("libappindicator-gtk3") + ;; + esac + done + + if [ ${#packages[@]} -gt 0 ]; then + echo -e "${BLUE}Installing: ${packages[*]}${NC}" + local pkg_manager="yum" + if command -v dnf >/dev/null 2>&1; then + pkg_manager="dnf" + fi + + if [ "$EUID" -eq 0 ]; then + $pkg_manager install -y "${packages[@]}" || { + echo -e "${YELLOW}Warning: Failed to install dependencies automatically${NC}" + echo -e "${YELLOW}Please install manually: sudo $pkg_manager install ${packages[*]}${NC}" + return 1 + } + else + echo -e "${YELLOW}Root privileges required to install dependencies${NC}" + echo -e "${YELLOW}Please run: sudo $pkg_manager install ${packages[*]}${NC}" + return 1 + fi + fi + elif command -v pacman >/dev/null 2>&1; then + # Arch Linux + local packages=() + for dep in "${missing_deps[@]}"; do + case "$dep" in + webkit2gtk-4.1) + packages+=("webkit2gtk-4.1") + ;; + gtk-3) + packages+=("gtk3") + ;; + appindicator3) + packages+=("libappindicator-gtk3") + ;; + esac + done + + if [ ${#packages[@]} -gt 0 ]; then + echo -e "${BLUE}Installing: ${packages[*]}${NC}" + if [ "$EUID" -eq 0 ]; then + pacman -S --noconfirm "${packages[@]}" || { + echo -e "${YELLOW}Warning: Failed to install dependencies automatically${NC}" + echo -e "${YELLOW}Please install manually: sudo pacman -S ${packages[*]}${NC}" + return 1 + } + else + echo -e "${YELLOW}Root privileges required to install dependencies${NC}" + echo -e "${YELLOW}Please run: sudo pacman -S ${packages[*]}${NC}" + return 1 + fi + fi + else + echo -e "${YELLOW}Warning: Could not detect package manager${NC}" + echo -e "${YELLOW}Please install the following runtime dependencies manually:${NC}" + echo -e "${YELLOW} - libwebkit2gtk-4.1-0 (or webkit2gtk-4.1)${NC}" + echo -e "${YELLOW} - libgtk-3-0 (or gtk3)${NC}" + echo -e "${YELLOW} - libappindicator3-1 (or libappindicator-gtk3)${NC}" + return 1 + fi + + echo -e "${GREEN}✓ Runtime dependencies installed${NC}" + return 0 +} + # Download and install install_binary() { local version="$1" @@ -246,6 +457,29 @@ install_binary() { local arch="$3" local ext="$4" + # Check Linux dependencies before installation (only for tar.gz, not AppImage) + if [ "$os" = "linux" ] && [ "$USE_APPIMAGE" != "true" ]; then + if ! check_linux_dependencies; then + echo -e "${YELLOW}Warning: Some runtime dependencies may be missing${NC}" + echo -e "${YELLOW}The application may not run correctly until dependencies are installed${NC}" + + # Only prompt if running interactively (stdin is a TTY) + if [ -t 0 ]; then + read -p "Continue with installation anyway? (y/N) " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo -e "${RED}Installation cancelled${NC}" + exit 1 + fi + else + echo -e "${YELLOW}Continuing with installation (non-interactive mode)${NC}" + echo -e "${YELLOW}Please install dependencies manually if the application fails to run${NC}" + fi + fi + elif [ "$os" = "linux" ] && [ "$USE_APPIMAGE" = "true" ]; then + echo -e "${GREEN}✓ Using AppImage (all dependencies bundled)${NC}" + fi + # Construct download URL (normalize GitHub URL first) local normalized_url=$(normalize_github_url "$REPO_URL") local archive_name="${BINARY_NAME}_${os}_${arch}_${version}.${ext}" @@ -290,7 +524,94 @@ install_binary() { # Verify checksum verify_checksum "$archive_path" "$archive_name" "$version" "$normalized_url" "$tmp_dir" - # Extract archive + # Handle AppImage specially (no extraction needed) + if [ "$ext" = "AppImage" ]; then + echo -e "${BLUE}Installing AppImage...${NC}" + + # Determine install location for AppImage + local appimage_dir="${INSTALL_DIR:-$HOME/.local/bin}" + local appimage_path="${appimage_dir}/${BINARY_NAME}.AppImage" + + # Create directory if needed + mkdir -p "$appimage_dir" + + # Check if we need sudo + local needs_sudo=false + if [ ! -w "$appimage_dir" ] && [ "$EUID" -ne 0 ]; then + needs_sudo=true + fi + + # Remove existing AppImage if present + if [ -f "$appimage_path" ]; then + echo -e "${YELLOW}Removing existing AppImage...${NC}" + if [ "$needs_sudo" = true ]; then + sudo rm -f "$appimage_path" + else + rm -f "$appimage_path" + fi + fi + + # Install AppImage + if [ "$needs_sudo" = true ]; then + sudo cp "$archive_path" "$appimage_path" + sudo chmod +x "$appimage_path" + else + cp "$archive_path" "$appimage_path" + chmod +x "$appimage_path" + fi + + # Create symlink for easier command-line access + local symlink_path="${appimage_dir}/${BINARY_NAME}" + if [ -L "$symlink_path" ] || [ -f "$symlink_path" ]; then + if [ "$needs_sudo" = true ]; then + sudo rm -f "$symlink_path" + else + rm -f "$symlink_path" + fi + fi + if [ "$needs_sudo" = true ]; then + sudo ln -sf "$appimage_path" "$symlink_path" + else + ln -sf "$appimage_path" "$symlink_path" + fi + + echo -e "${GREEN}✓ Successfully installed Pub/Sub GUI AppImage to ${appimage_path}${NC}" + echo -e "${GREEN}✓ Symlink created at ${symlink_path}${NC}" + + # Verify installation + if [ -f "$appimage_path" ] && [ -x "$appimage_path" ]; then + if command -v "$BINARY_NAME" >/dev/null 2>&1; then + echo -e "${GREEN}✓ Binary is available in PATH${NC}" + else + echo -e "${YELLOW}⚠ Binary is not in PATH${NC}" + echo -e "${YELLOW}Add ${appimage_dir} to your PATH, or run directly: ${appimage_path}${NC}" + + # Suggest adding to PATH + local shell_rc="" + if [ -n "${ZSH_VERSION:-}" ] || [ -f "$HOME/.zshrc" ]; then + shell_rc="$HOME/.zshrc" + elif [ -n "${BASH_VERSION:-}" ] || [ -f "$HOME/.bashrc" ]; then + shell_rc="$HOME/.bashrc" + fi + + if [ -n "$shell_rc" ] && ! grep -q "$appimage_dir" "$shell_rc" 2>/dev/null; then + echo -e "${BLUE}Add this to your ${shell_rc}:${NC}" + echo -e "${BLUE}export PATH=\"\$PATH:$appimage_dir\"${NC}" + fi + fi + + # Show version if available + if "$appimage_path" --version >/dev/null 2>&1; then + echo -e "${GREEN}Version: $("$appimage_path" --version)${NC}" + fi + else + echo -e "${RED}Error: Installation verification failed${NC}" >&2 + exit 1 + fi + return + fi + + # Extract archive (for non-AppImage formats) echo -e "${BLUE}Extracting archive...${NC}" cd "$tmp_dir" @@ -379,7 +700,8 @@ install_binary() { if [ "$os" = "windows" ]; then extracted_binary=$(find . -name "${BINARY_NAME}.exe" -type f | head -1) else - extracted_binary=$(find . -name "$BINARY_NAME" -type f -perm +111 | head -1) + # /111 means "any execute bit is set" (executable by owner, group, or others) + extracted_binary=$(find . -name "$BINARY_NAME" -type f -perm /111 | head -1) if [ -z "$extracted_binary" ]; then extracted_binary=$(find . -name "$BINARY_NAME" -type f | head -1) fi @@ -462,6 +784,13 @@ main() { # Detect platform detect_platform + # Show platform info + local format_info="$ARCHIVE_EXT" + if [ "$USE_APPIMAGE" = "true" ]; then + format_info="AppImage (self-contained)" + fi + echo -e "${BLUE}Platform: ${PLATFORM_OS}/${PLATFORM_ARCH} (${format_info})${NC}" + # Get version local install_version="$VERSION" if [ "$VERSION" = "latest" ]; then