name: Build Linux Release on: workflow_dispatch: inputs: version: description: 'Version name for the release (e.g., 1.0.0)' required: true type: string workflow_call: inputs: version: description: 'Version name for the release (e.g., 1.0.0)' required: true type: string permissions: contents: write env: PYTHON_VERSION: '3.13' jobs: build-linux: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 - name: Get version id: get_version shell: bash run: | version="${{ inputs.version || github.event.inputs.version }}" echo "Extracted version: $version" echo "VERSION=$version" >> "$GITHUB_OUTPUT" - name: Setup Python uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Cache Python dependencies uses: actions/cache@v4 with: path: | venv ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- - name: Install system dependencies shell: bash run: | sudo apt-get update -y # Tools commonly required by cx_Freeze on Linux packaging sudo apt-get install -y --no-install-recommends \ rpm alien fakeroot patchelf desktop-file-utils file xz-utils zsync - name: Create virtual environment and install dependencies shell: bash run: | python -m venv venv source venv/bin/activate python -m pip install --upgrade pip pip install --no-cache-dir -r requirements.txt pip install --no-cache-dir cx_Freeze - name: Prepare build variables shell: bash run: | version="${{ steps.get_version.outputs.VERSION }}" echo "VERSION=$version" >> "$GITHUB_ENV" arch="$(uname -m)" # x86_64 or aarch64 echo "ARCH=$arch" >> "$GITHUB_ENV" echo "Prepared build variables for version: $version on $(uname -a)" - name: Create cx_Freeze setup script (Linux) shell: bash run: | cat > setup_cxfreeze.py <<'PY' import os import sys from pathlib import Path from cx_Freeze import setup, Executable version = os.environ.get("VERSION", "0.0.0") # Prepare include_files list include_files_list = [ ("src", "src"), ("assets/branding/icons", "lib/assets/branding/icons"), ("assets/Icon", "lib/assets/Icon"), ("assets/sound", "lib/assets/sound"), ("languages", "lib/languages"), ("ytsage.desktop", "share/applications/ytsage.desktop"), ("assets/branding/icons/icon.png", "share/pixmaps/ytsage.png"), ] build_exe_options = dict( optimize=2, packages=[ "PySide6.QtCore", "PySide6.QtGui", "PySide6.QtWidgets", "requests", "PIL", "packaging", "markdown", "pyglet", "loguru", "setuptools", ], excludes=[ "PySide6.QtBluetooth", "PySide6.QtNetwork", "PySide6.QtOpenGL", "PySide6.QtPrintSupport", "PySide6.QtSvg", "PySide6.QtTest", "PySide6.QtXml", "PySide6.QtSql", "PySide6.QtHelp", "PySide6.QtMultimedia", "PySide6.QtQml", "PySide6.QtQuick", "PySide6.QtWebEngineCore", "PIL.ImageDraw", "PIL.ImageFont", "numpy", "scipy", "wx", "pandas", "tkinter", "yt_dlp", "unittest", "test", "tests", ], include_files=include_files_list, # Bundle all dependencies - avoid system library references bin_includes=[], bin_excludes=[], # Important: include system libs to avoid external dependencies replace_paths=[("*", "")], ) executables = [ Executable( script="main.py", target_name="ytsage", icon="assets/branding/icons/icon.png", ) ] setup( name="ytsage", version=version, description="YTSage", options={ "build_exe": build_exe_options, # AppImage packaging "bdist_appimage": { # If ends with .AppImage, use verbatim file name "target_name": f"ytsage-v{version}.AppImage", }, }, executables=executables, ) PY - name: Create desktop entry shell: bash run: | cat > ytsage.desktop <<'DESKTOP' [Desktop Entry] Type=Application Name=YTSage Comment=YouTube downloader Exec=/usr/bin/ytsage %U Icon=ytsage Terminal=false Categories=AudioVideo;Network;Utility; StartupWMClass=YTSage DESKTOP - name: Build AppImage and RPM shell: bash run: | set -e source venv/bin/activate # Ensure a clean build and build_exe first python setup_cxfreeze.py build_exe # Remove screenshots to reduce size before packaging build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1 || true) if [ -n "$build_dir" ] && [ -d "$build_dir/lib/assets/branding/screenshots" ]; then rm -rf "$build_dir/lib/assets/branding/screenshots" echo "Removed screenshots folder from $build_dir" fi # Build AppImage python setup_cxfreeze.py bdist_appimage # Build RPM manually with proper spec file version="${{ steps.get_version.outputs.VERSION }}" arch_uname="${ARCH}" workspace_root="$(pwd)" build_dir_abs="${workspace_root}/${build_dir}" # Create custom RPM spec file that doesn't auto-detect dependencies mkdir -p build/rpm/{BUILD,RPMS,SOURCES,SPECS,SRPMS} cat > build/rpm/SPECS/ytsage.spec < %{buildroot}/usr/bin/ytsage <<'WRAPPER' #!/usr/bin/env bash set -euo pipefail APPDIR="/opt/ytsage" cd "\$APPDIR" exec "\$APPDIR/ytsage" "\$@" WRAPPER chmod 0755 %{buildroot}/usr/bin/ytsage # Install desktop file and icon install -m 0644 ${workspace_root}/ytsage.desktop %{buildroot}/usr/share/applications/ytsage.desktop install -m 0644 ${workspace_root}/assets/branding/icons/icon.png %{buildroot}/usr/share/pixmaps/ytsage.png %files /opt/ytsage /usr/bin/ytsage /usr/share/applications/ytsage.desktop /usr/share/pixmaps/ytsage.png %post if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q /usr/share/applications || true fi %postun if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q /usr/share/applications || true fi %changelog * $(date +'%a %b %d %Y') YTSage Maintainers - ${version//-/.}-1 - Release ${version} SPEC # Build the RPM rpmbuild -bb build/rpm/SPECS/ytsage.spec \ --define "_topdir $(pwd)/build/rpm" \ --buildroot "$(pwd)/build/rpm/BUILDROOT" # Copy RPM to dist mkdir -p dist find build/rpm/RPMS -name "*.rpm" -exec cp {} dist/ \; echo "Post-build directory listing:" echo "-- dist --"; ls -lah dist || true echo "-- build --"; ls -lah build || true - name: Build native DEB package shell: bash run: | set -e version="${{ steps.get_version.outputs.VERSION }}" arch_uname="${ARCH}" # Map uname -m to Debian arch names case "$arch_uname" in x86_64) deb_arch=amd64 ;; aarch64) deb_arch=arm64 ;; *) deb_arch="$arch_uname" ;; esac # Locate build output build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1) if [ -z "$build_dir" ]; then echo "Error: build/exe.* directory not found" >&2 exit 1 fi # Staging root pkgroot=deb_pkg rm -rf "$pkgroot" mkdir -p "$pkgroot/DEBIAN" \ "$pkgroot/usr/bin" \ "$pkgroot/usr/share/applications" \ "$pkgroot/usr/share/pixmaps" \ "$pkgroot/opt/ytsage" # Install application payload under /opt/ytsage cp -a "$build_dir"/* "$pkgroot/opt/ytsage/" # Wrapper to ensure correct working directory cat > "$pkgroot/usr/bin/ytsage" <<'WRAP' #!/usr/bin/env bash set -euo pipefail APPDIR="/opt/ytsage" cd "$APPDIR" exec "$APPDIR/ytsage" "$@" WRAP chmod 0755 "$pkgroot/usr/bin/ytsage" # Desktop file and icon install -m 0644 ytsage.desktop "$pkgroot/usr/share/applications/ytsage.desktop" install -m 0644 assets/branding/icons/icon.png "$pkgroot/usr/share/pixmaps/ytsage.png" # Control file cat > "$pkgroot/DEBIAN/control" < Homepage: https://github.com/oop7/YTSage Description: YTSage - A modern YouTube downloader with a PySide6 interface Download videos, extract audio, fetch subtitles, and more. CONTROL # Post-install script to refresh desktop/menu caches (best-effort) cat > "$pkgroot/DEBIAN/postinst" <<'POSTINST' #!/bin/sh set -e if command -v update-desktop-database >/dev/null 2>&1; then update-desktop-database -q || true fi if command -v gtk-update-icon-cache >/dev/null 2>&1; then gtk-update-icon-cache -q /usr/share/icons/hicolor || true fi exit 0 POSTINST chmod 0755 "$pkgroot/DEBIAN/postinst" # Triggers so Debian updates caches cat > "$pkgroot/DEBIAN/triggers" <<'TRIGGERS' interest-noawait /usr/share/applications interest-noawait /usr/share/icons/hicolor TRIGGERS # Build the deb deb_out="YTSage-v${version}-${deb_arch}.deb" dpkg-deb --build "$pkgroot" "$deb_out" mkdir -p artifacts mv "$deb_out" artifacts/ echo "Built native DEB: artifacts/$deb_out" - name: Build Flatpak Bundle shell: bash run: | version="${{ steps.get_version.outputs.VERSION }}" # Install flatpak-builder sudo apt-get install -y flatpak-builder flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo # Define manifest ID APP_ID="io.github.oop7.YTSage" # Create a specific desktop file for Flatpak # - Exec must be 'ytsage' (in path), not /usr/bin/ytsage # - Icon must match the App ID (io.github.oop7.YTSage) cat > flatpak.desktop < ytsage_flatpak.json </dev/null | head -n1) if [ -z "$build_dir" ]; then echo "Error: build/exe.* directory not found for Flatpak build" >&2 exit 1 fi # Move build dir to a fixed location for the manifest source to catch mkdir -p dist/ytsage-v${version}-flatpak cp -r "$build_dir"/* "dist/ytsage-v${version}-flatpak/" # Install runtime/sdk flatpak install -y --user flathub org.kde.Platform//6.10 org.kde.Sdk//6.10 # Build flatpak-builder --user --install-deps-from=flathub --repo=repo --force-clean build-flatpak ytsage_flatpak.json # Bundle flatpak build-bundle repo artifacts/YTSage-v${version}-x86_64.flatpak $APP_ID echo "Built Flatpak: artifacts/YTSage-v${version}-x86_64.flatpak" - name: Package and prepare release artifacts (.AppImage, .rpm, .deb) shell: bash run: | version="${{ steps.get_version.outputs.VERSION }}" mkdir -p artifacts # Copy AppImage appimage_src=$(ls dist/ytsage-v*.AppImage 2>/dev/null | head -n1 || true) if [ -n "$appimage_src" ]; then cp "$appimage_src" "artifacts/YTSage-v${version}-${ARCH}.AppImage" echo "Copied AppImage: $appimage_src -> artifacts/YTSage-v${version}-${ARCH}.AppImage" else echo "Warning: No .AppImage found in dist/" fi # Copy RPM (normalize name) rpm_src=$(ls dist/ytsage-*.rpm 2>/dev/null | head -n1 || true) if [ -n "$rpm_src" ]; then cp "$rpm_src" "artifacts/YTSage-v${version}-${ARCH}.rpm" echo "Copied RPM: $rpm_src -> artifacts/YTSage-v${version}-${ARCH}.rpm" else echo "Warning: No .rpm found in dist/" fi # DEB is already staged in artifacts by the native packaging step ls -1 artifacts/*.deb 2>/dev/null || echo "Warning: No .deb found in artifacts/" # Check for Flatpak ls -1 artifacts/*.flatpak 2>/dev/null || echo "Warning: No .flatpak found in artifacts/" echo "Final artifacts:" ls -lh artifacts || true - name: Create/Update draft release uses: softprops/action-gh-release@v2 with: tag_name: v${{ steps.get_version.outputs.VERSION }} name: YTSage v${{ steps.get_version.outputs.VERSION }} draft: true prerelease: false append_body: true fail_on_unmatched_files: false body: | # YTSage v${{ steps.get_version.outputs.VERSION }} **Release Date**: ${{ github.event.head_commit.timestamp }} files: | artifacts/* env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}