Files
oop7 030b37c505 Adjust PySide6 components and plugin cleanup
Add PySide6.QtDBus and PySide6.QtSvg to the list of required modules and remove PySide6.QtSvg from the secondary module list. Stop removing the svg plugin, platforminputcontexts and svg-related image/icon libraries during the cleanup pass (only libqpdf.so is still removed). These changes ensure QtSvg and D-Bus components are preserved in the Linux build and prevent stripping SVG-related functionality.
2026-02-03 17:19:44 +02:00

628 lines
21 KiB
YAML

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('pyproject.toml') }}
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 .
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: |
# Create entry point script
cat > ytsage_entry.py <<'ENTRY'
from ytsage.main import main
if __name__ == "__main__":
main()
ENTRY
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 = [
("ytsage/assets/Icon", "lib/assets/Icon"),
("ytsage/assets/sound", "lib/assets/sound"),
("ytsage/languages", "lib/languages"),
("branding/icons", "lib/assets/branding/icons"),
("ytsage.desktop", "share/applications/ytsage.desktop"),
("branding/icons/icon.png", "share/pixmaps/ytsage.png"),
]
build_exe_options = dict(
optimize=2,
silent=True,
packages=[
"ytsage",
"PySide6.QtCore",
"PySide6.QtGui",
"PySide6.QtWidgets",
"PySide6.QtMultimedia",
"PySide6.QtNetwork",
"PySide6.QtDBus",
"PySide6.QtSvg",
"requests",
"PIL",
"packaging",
"markdown",
"loguru",
"setuptools",
],
zip_include_packages=[
"PySide6",
"shiboken6",
"requests",
"PIL",
"packaging",
],
excludes=[
"PySide6.QtBluetooth",
"PySide6.QtOpenGL",
"PySide6.QtPrintSupport",
"PySide6.QtTest",
"PySide6.QtXml",
"PySide6.QtSql",
"PySide6.QtHelp",
"PySide6.QtQml",
"PySide6.QtQuick",
"PySide6.QtWebEngineCore",
"PIL.ImageDraw",
"PIL.ImageFont",
"numpy",
"scipy",
"wx",
"pandas",
"tkinter",
"yt_dlp",
"unittest",
"test",
"tests",
"pydoc",
"doctest",
"email",
],
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="ytsage_entry.py",
target_name="ytsage",
icon="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 with -OO
python -OO setup_cxfreeze.py build_exe
# Clean unnecessary files from build directory before packaging
build_dir=$(ls -d build/exe.* 2>/dev/null | head -n1 || true)
if [ -n "$build_dir" ]; then
echo "Trimming files in $build_dir..."
lib_dir="$build_dir/lib"
if [ -d "$lib_dir" ]; then
# 1. Remove screenshots
if [ -d "$lib_dir/assets/branding/screenshots" ]; then
rm -rf "$lib_dir/assets/branding/screenshots"
echo "Removed screenshots folder"
fi
# 2. Remove unused Qt translations
if [ -d "$lib_dir/PySide6/translations" ]; then
rm -rf "$lib_dir/PySide6/translations"
echo "Removed Qt translations"
fi
# 3. Remove unused Qt plugins
plugins_dir="$lib_dir/PySide6/plugins"
if [ -d "$plugins_dir" ]; then
for plugin in designer pdf sql help qml quick webengine bluetooth opengl printsupport test xml; do
if [ -d "$plugins_dir/$plugin" ]; then
rm -rf "$plugins_dir/$plugin"
echo "Removed plugin: $plugin"
fi
done
# Specific cleanups (imageformats)
rm -f "$plugins_dir/imageformats/libqpdf.so"
fi
# 4. Remove bloat shared libraries (Recursively finds in lib/ and lib/PySide6/)
# Matches libQt6Qml.so.6, Qt6Qml.abi3.so, etc.
echo "Removing bloat Qt libraries..."
find "$lib_dir" -name "*Qt6Web*" -delete
find "$lib_dir" -name "*Qt6Pdf*" -delete
find "$lib_dir" -name "*Qt6Qml*" -delete
find "$lib_dir" -name "*Qt6Quick*" -delete
find "$lib_dir" -name "*Qt6VirtualKeyboard*" -delete
find "$lib_dir" -name "*Qt6OpenGL*" -delete
# 5. Remove build tools and unused python modules
for tool in setuptools wheel pkg_resources _distutils_hack curses _pyrepl; do
rm -rf "$lib_dir/$tool"
echo "Removed build tool/module: $tool"
done
else
echo "Warning: lib directory not found inside build_dir"
fi
else
echo "Warning: Build directory not found, cannot trim files"
fi
# Build AppImage manually to avoid cx_Freeze rebuilding (and untrimming) the artifacts
echo "Creating AppImage manually..."
mkdir -p dist
# Download appimagetool
wget -q -O appimagetool https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool
# Setup AppDir structure (Root layout matches cx_Freeze build output exactly)
rm -rf AppDir
mkdir -p AppDir
# Copy trimmed build content DIRECTLY to AppDir root
# This keeps 'lib' adjacent to 'ytsage', preventing 'ModuleNotFoundError: encodings'
cp -r "$build_dir"/* AppDir/
# Setup metadata
cp ytsage.desktop AppDir/
# Fix Exec path for AppImage context (binary is in root, not /usr/bin)
sed -i 's|Exec=/usr/bin/ytsage|Exec=ytsage|g' AppDir/ytsage.desktop
cp branding/icons/icon.png AppDir/ytsage.png
# .DirIcon for file managers
cp branding/icons/icon.png AppDir/.DirIcon
# Create AppRun as a script to correctly set environment
cat > AppDir/AppRun <<'APPRUN'
#!/bin/sh
SELF=$(readlink -f "$0")
HERE=$(dirname "$SELF")
export LD_LIBRARY_PATH="$HERE/lib:$LD_LIBRARY_PATH"
exec "$HERE/ytsage" "$@"
APPRUN
chmod +x AppDir/AppRun
# Build AppImage (using --appimage-extract-and-run to avoid FUSE issues in CI)
# We explicitly set ARCH for appimagetool
ARCH=x86_64 ./appimagetool --appimage-extract-and-run AppDir "dist/ytsage-v${version}.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 <<SPEC
%global __requires_exclude_from ^/opt/ytsage/.*$
%global __provides_exclude_from ^/opt/ytsage/.*$
%define _build_id_links none
%undefine _missing_build_ids_terminate_build
Name: ytsage
Version: ${version//-/.}
Release: 1%{?dist}
Summary: YTSage - A modern YouTube downloader
License: MIT
URL: https://github.com/oop7/YTSage
AutoReqProv: no
%description
YTSage is a modern YouTube downloader with a PySide6 interface.
Download videos, extract audio, fetch subtitles, and more.
All dependencies are bundled within the package.
%install
rm -rf %{buildroot}
mkdir -p %{buildroot}/opt/ytsage
mkdir -p %{buildroot}/usr/bin
mkdir -p %{buildroot}/usr/share/applications
mkdir -p %{buildroot}/usr/share/pixmaps
# Copy application files
cp -a ${build_dir_abs}/* %{buildroot}/opt/ytsage/
# Create wrapper script
cat > %{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}/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 <noreply@example.com> - ${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 branding/icons/icon.png "$pkgroot/usr/share/pixmaps/ytsage.png"
# Control file
cat > "$pkgroot/DEBIAN/control" <<CONTROL
Package: ytsage
Version: ${version}
Section: utils
Priority: optional
Architecture: ${deb_arch}
Maintainer: YTSage Maintainers <noreply@example.com>
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 <<EOF
[Desktop Entry]
Type=Application
Name=YTSage
Comment=YouTube downloader
Exec=ytsage
Icon=$APP_ID
Terminal=false
Categories=AudioVideo;Network;Utility;
StartupWMClass=YTSage
EOF
# Create manifest manually
cat > ytsage_flatpak.json <<EOF
{
"app-id": "$APP_ID",
"runtime": "org.kde.Platform",
"runtime-version": "6.10",
"sdk": "org.kde.Sdk",
"command": "ytsage",
"finish-args": [
"--share=ipc",
"--socket=x11",
"--socket=wayland",
"--socket=pulseaudio",
"--device=dri",
"--share=network",
"--filesystem=host",
"--env=YTDLP_APP_BIN_PATH=/var/data/yt-dlp",
"--env=DENO_APP_BIN_PATH=/var/data/deno"
],
"modules": [
{
"name": "ytsage",
"buildsystem": "simple",
"build-commands": [
"mkdir -p /app/bin /app/share/ytsage",
"cp -r dist/ytsage-v${version}-*/* /app/share/ytsage/",
"ln -s /app/share/ytsage/ytsage /app/bin/ytsage",
"install -D flatpak.desktop /app/share/applications/$APP_ID.desktop",
"install -D branding/icons/icon.png /app/share/icons/hicolor/128x128/apps/$APP_ID.png"
],
"sources": [
{
"type": "dir",
"path": "."
}
]
}
]
}
EOF
# Build the Flatpak
# Note: In a real environment, building from source is preferred.
# Here we are "bundling" the already built binaries from the previous step (cx_Freeze) for simplicity in CI.
# This requires the 'dist/' folder to be populated by the previous 'Create cx_Freeze setup script' + build steps.
# Wait, the previous steps built into 'build/exe.linux-...' not 'dist/'. Let's find it.
build_dir=$(ls -d build/exe.* 2>/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 }}