c11a4a0280
Update the macOS build workflow to robustly locate and clean multiple library directories inside the .app bundle. Instead of a single lib path, the script now finds all 'lib' dirs containing PySide6 or ytsage and iterates over them. Cleanup expanded to handle alternate PySide6 paths (PySide6/Qt/translations, PySide6/Qt/plugins), more plugin/imageformat variants (.dylib/.so), additional Qt library name patterns (Qt6*, framework-style names) and Qt6DBus, and removal of common build tools/modules. Adds logging and a warning when no library dirs are found.
349 lines
12 KiB
YAML
349 lines
12 KiB
YAML
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 }}
|