536594a56e
Enhance the .github workflow that trims macOS .app bundles by robustly detecting the library directory (with fallbacks) and performing deeper cleanup. Removes screenshots, Qt translations, many Qt plugin folders, specific image/icon libs, bulk Qt6 libraries, and several Python build/tool modules; adapts for macOS dylib naming. Adds logging and a warning if the lib directory cannot be determined to avoid accidental deletes.
342 lines
11 KiB
YAML
342 lines
11 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 'os.py' or 'ytsage' folder to be sure
|
|
lib_dir=$(find "$app_path/Contents" -type d -name "ytsage" | head -n1 | xargs dirname)
|
|
|
|
if [ -z "$lib_dir" ]; then
|
|
# Fallback standard cx_Freeze locations
|
|
if [ -d "$app_path/Contents/MacOS/lib" ]; then
|
|
lib_dir="$app_path/Contents/MacOS/lib"
|
|
elif [ -d "$app_path/Contents/Resources/lib" ]; then
|
|
lib_dir="$app_path/Contents/Resources/lib"
|
|
fi
|
|
fi
|
|
|
|
echo "Detected Lib Directory: $lib_dir"
|
|
|
|
if [ -d "$lib_dir" ]; then
|
|
# 1. Remove screenshots
|
|
rm -rf "$lib_dir/assets/branding/screenshots"
|
|
echo "Removed screenshots folder"
|
|
|
|
# 2. Remove unused Qt translations
|
|
rm -rf "$lib_dir/PySide6/translations"
|
|
echo "Removed Qt translations"
|
|
|
|
# 3. Remove unused Qt plugins
|
|
plugins_dir="$lib_dir/PySide6/plugins"
|
|
for plugin in designer pdf svg 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 folder: $plugin"
|
|
fi
|
|
done
|
|
|
|
# 3.1. Explicitly remove specific input contexts and image formats
|
|
rm -rf "$plugins_dir/platforminputcontexts"
|
|
# Note: macOS libs usually have .dylib extension or no extension in bundles, or .so for python modules
|
|
# We try removing common variants
|
|
rm -f "$plugins_dir/imageformats/libqpdf.dylib" "$plugins_dir/imageformats/libqsvg.dylib"
|
|
rm -f "$plugins_dir/imageformats/qpdf.dylib" "$plugins_dir/imageformats/qsvg.dylib"
|
|
rm -f "$plugins_dir/iconengines/libqsvgicon.dylib" "$plugins_dir/iconengines/qsvgicon.dylib"
|
|
|
|
# 4. Remove any duplicate or unnecessary Qt Libraries (DLLs/dylibs)
|
|
# Targeting root lib and PySide6 internal lib
|
|
# Patterns match both .so, .dylib, and .framework style naming
|
|
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: Library directory could not be precisely determined. Skipping deep trim."
|
|
fi
|
|
|
|
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 }}
|