Build macOS Release #46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build macOS 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-macos: | |
| runs-on: macos-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 | |
| ~/Library/Caches/pip | |
| key: ${{ runner.os }}-pip-${{ hashFiles('pyproject.toml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pip- | |
| - 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 dmgbuild | |
| - name: Prepare build variables | |
| shell: bash | |
| run: | | |
| version="${{ steps.get_version.outputs.VERSION }}" | |
| echo "VERSION=$version" >> "$GITHUB_ENV" | |
| arch="$(uname -m)" # arm64 or x86_64 | |
| echo "ARCH=$arch" >> "$GITHUB_ENV" | |
| if [ "$arch" = "arm64" ]; then | |
| echo "ARCH_SUFFIX=arm64" >> "$GITHUB_ENV" | |
| else | |
| echo "ARCH_SUFFIX=x64" >> "$GITHUB_ENV" | |
| fi | |
| echo "Prepared build variables for version: $version on $(uname -a)" | |
| - name: Create cx_Freeze setup script | |
| 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 | |
| from cx_Freeze import setup, Executable | |
| version = os.environ.get("VERSION", "0.0.0") | |
| build_exe_options = dict( | |
| optimize=2, | |
| silent=True, | |
| packages=[ | |
| "ytsage", | |
| "PySide6.QtCore", | |
| "PySide6.QtGui", | |
| "PySide6.QtWidgets", | |
| "PySide6.QtMultimedia", | |
| "PySide6.QtNetwork", | |
| "requests", | |
| "PIL", | |
| "packaging", | |
| "markdown", | |
| "loguru", | |
| "setuptools", | |
| ], | |
| zip_include_packages=[ | |
| "PySide6", | |
| "shiboken6", | |
| "requests", | |
| "PIL", | |
| "packaging", | |
| ], | |
| excludes=[ | |
| "PySide6.QtBluetooth", | |
| "PySide6.QtOpenGL", | |
| "PySide6.QtPrintSupport", | |
| "PySide6.QtSvg", | |
| "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=[ | |
| ("ytsage/assets/Icon", "lib/assets/Icon"), | |
| ("ytsage/assets/sound", "lib/assets/sound"), | |
| ("ytsage/languages", "lib/languages"), | |
| ("branding/icons", "lib/assets/branding/icons"), | |
| ], | |
| ) | |
| executables = [ | |
| Executable( | |
| script="ytsage_entry.py", | |
| target_name=f"YTSage-v{version}", | |
| icon="branding/icons/icon.icns", | |
| ) | |
| ] | |
| setup( | |
| name="YTSage", | |
| version=version, | |
| description="YTSage", | |
| options={ | |
| "build_exe": build_exe_options, | |
| "bdist_mac": { | |
| "iconfile": "branding/icons/icon.icns", | |
| "bundle_name": f"YTSage-v{version}", | |
| }, | |
| "bdist_dmg": { | |
| "volume_label": f"YTSage v{version}", | |
| "applications_shortcut": True, | |
| "format": "UDZO", | |
| "filesystem": "HFS+", | |
| "default_view": "icon-view", | |
| }, | |
| }, | |
| executables=executables, | |
| ) | |
| PY | |
| - name: Build .app bundle (bdist_mac) | |
| shell: bash | |
| run: | | |
| source venv/bin/activate | |
| python -OO setup_cxfreeze.py bdist_mac | |
| - name: Trim unnecessary files from App bundle | |
| shell: bash | |
| run: | | |
| version="${{ steps.get_version.outputs.VERSION }}" | |
| # Locate the built .app | |
| app_path="" | |
| for cand in "dist/YTSage-v${version}.app" "build/dist/YTSage-v${version}.app" "build/YTSage-v${version}.app"; do | |
| if [ -d "$cand" ]; then app_path="$cand"; break; fi | |
| done | |
| if [ -z "$app_path" ]; then | |
| app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true) | |
| fi | |
| if [ -n "$app_path" ]; then | |
| echo "Processing App Bundle at: $app_path" | |
| # Determine Lib directory (standard location vs customized) | |
| # We look for all 'lib' directories that contain 'PySide6' or 'ytsage' to be thorough | |
| # resulting in potentially multiple lib dirs to clean (e.g. MacOS/lib and Resources/lib) | |
| lib_dirs=$(find "$app_path/Contents" -type d -name "lib" | while read -r d; do if [ -d "$d/ytsage" ] || [ -d "$d/PySide6" ]; then echo "$d"; fi; done) | |
| if [ -z "$lib_dirs" ]; then | |
| echo "Warning: No valid Library directory found. Skipping deep trim." | |
| fi | |
| for lib_dir in $lib_dirs; do | |
| echo "Processing Lib Directory: $lib_dir" | |
| # 1. Remove screenshots | |
| rm -rf "$lib_dir/assets/branding/screenshots" | |
| echo "Removed screenshots folder" | |
| # 2. Remove unused Qt translations (Path varies: PySide6/translations or PySide6/Qt/translations) | |
| rm -rf "$lib_dir/PySide6/translations" | |
| rm -rf "$lib_dir/PySide6/Qt/translations" | |
| echo "Removed Qt translations" | |
| # 3. Remove unused Qt plugins | |
| # Path varies: PySide6/plugins or PySide6/Qt/plugins | |
| for plugins_base in "$lib_dir/PySide6/plugins" "$lib_dir/PySide6/Qt/plugins"; do | |
| if [ -d "$plugins_base" ]; then | |
| for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do | |
| if [ -d "$plugins_base/$plugin" ]; then | |
| rm -rf "$plugins_base/$plugin" | |
| echo "Removed plugin folder: $plugin from $plugins_base" | |
| fi | |
| done | |
| # 3.1. Explicitly remove specific input contexts and image formats | |
| rm -rf "$plugins_base/platforminputcontexts" | |
| rm -f "$plugins_base/imageformats/libqpdf.dylib" "$plugins_base/imageformats/libqsvg.dylib" | |
| rm -f "$plugins_base/imageformats/libqpdf.so" "$plugins_base/imageformats/libqsvg.so" # just in case | |
| rm -f "$plugins_base/iconengines/libqsvgicon.dylib" | |
| fi | |
| done | |
| # 4. Remove any duplicate or unnecessary Qt Libraries (DLLs/dylibs/frameworks) | |
| # On macOS, these often appear as files with no extension (e.g. 'QtQml') or .dylib | |
| # We remove both variants and also the 'Qt6' prefixed ones just in case | |
| echo "Removing bloat Qt libraries..." | |
| # Standard patterns (Linux/Windows style) | |
| 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 | |
| find "$lib_dir" -name "*Qt6DBus*" -delete | |
| # macOS Framework style (No '6' in name usually, e.g. QtQml, QtQuick) | |
| # Be careful not to delete QtQuickWidgets if used (though usually safe if Quick is gone) | |
| # We use known specific lists to avoid over-deleting | |
| for bloat in QtQml QtQmlMeta QtQmlModels QtQmlWorkerScript QtQuick QtQuickControls2 QtQuickTemplates2 QtWebEngine QtWebEngineCore QtVirtualKeyboard QtVirtualKeyboardQml QtOpenGL QtOpenGLWidgets QtPdf QtPdfWidgets QtDBus; do | |
| find "$lib_dir" -maxdepth 1 -name "$bloat" -delete | |
| find "$lib_dir" -maxdepth 1 -name "${bloat}.*" -delete # Matches versions or extensions | |
| done | |
| # 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 | |
| done | |
| else | |
| echo "Error: Could not find .app bundle to trim!" | |
| exit 1 | |
| fi | |
| - name: Build DMG (bdist_dmg) | |
| shell: bash | |
| run: | | |
| source venv/bin/activate | |
| python setup_cxfreeze.py bdist_dmg | |
| - name: Package and prepare release artifacts (.app.zip and .dmg) | |
| shell: bash | |
| run: | | |
| version="${{ steps.get_version.outputs.VERSION }}" | |
| echo "Preparing artifacts for version: $version" | |
| mkdir -p artifacts | |
| # Find the .app bundle | |
| app_path="" | |
| for cand in "dist/YTSage-v${version}.app" "build/dist/YTSage-v${version}.app" "build/YTSage-v${version}.app"; do | |
| if [ -d "$cand" ]; then app_path="$cand"; break; fi | |
| done | |
| if [ -z "$app_path" ]; then | |
| app_path=$(ls -d dist/*.app build/dist/*.app build/*.app 2>/dev/null | head -n1 || true) | |
| fi | |
| if [ -n "$app_path" ] && [ -d "$app_path" ]; then | |
| app_base="$(basename "$app_path")" | |
| app_parent="$(dirname "$app_path")" | |
| (cd "$app_parent" && zip -r "${GITHUB_WORKSPACE}/artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" "$app_base") | |
| echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.app.zip" | |
| else | |
| echo "Warning: .app bundle not found in dist/ or build/" | |
| fi | |
| # Try to find the generated DMG | |
| dmg_src="" | |
| for cand in "dist/YTSage-v${version}.dmg"; do | |
| if [ -f "$cand" ]; then dmg_src="$cand"; break; fi | |
| done | |
| if [ -z "$dmg_src" ]; then | |
| dmg_src=$(ls dist/*.dmg build/*.dmg build/dist/*.dmg 2>/dev/null | head -n1 || true) | |
| fi | |
| if [ -n "$dmg_src" ] && [ -f "$dmg_src" ]; then | |
| cp "$dmg_src" "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" | |
| echo "Copied DMG to artifacts" | |
| else | |
| echo "Warning: No DMG found" | |
| fi | |
| 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 }} |