ac11cbd7bb
Replace complex multi-dir detection with a single find+while loop to identify library dirs and verify Python lib layout (asyncio/PySide6/ytsage). Consolidate cleanup steps: recursively remove PySide6 translations, normalize plugin path detection and remove specific plugin folders/files via find, and simplify removal of unwanted Qt modules using broader find patterns. Remove some verbose checks and add a final sanity listing of remaining lib contents. These changes make the trimming more robust to different .app layouts and reduce duplicate logic.
333 lines
12 KiB
YAML
333 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"
|
|
|
|
# Simplified discovery of library directories
|
|
# We search for 'lib' folders and then verify if they look like the python library folder
|
|
find "$app_path/Contents" -type d -name "lib" | while read -r lib_dir; do
|
|
# Check if this lib dir is a python library dir (contains asyncio or PySide6 or ytsage)
|
|
if [ -d "$lib_dir/asyncio" ] || [ -d "$lib_dir/PySide6" ] || [ -d "$lib_dir/ytsage" ]; then
|
|
echo "Cleaning Library Directory: $lib_dir"
|
|
|
|
# 1. Remove screenshots
|
|
rm -rf "$lib_dir/assets/branding/screenshots"
|
|
|
|
# 2. Remove unused Qt translations (Recursively find 'translations' folder inside PySide6)
|
|
find "$lib_dir" -type d -path "*/PySide6/*/translations" -exec rm -rf {} + 2>/dev/null || true
|
|
find "$lib_dir" -type d -path "*/PySide6/translations" -exec rm -rf {} + 2>/dev/null || true
|
|
|
|
# 3. Remove unused Qt plugins
|
|
# Determine plugins path
|
|
plugins_path=""
|
|
if [ -d "$lib_dir/PySide6/plugins" ]; then plugins_path="$lib_dir/PySide6/plugins"; fi
|
|
if [ -d "$lib_dir/PySide6/Qt/plugins" ]; then plugins_path="$lib_dir/PySide6/Qt/plugins"; fi
|
|
|
|
if [ -n "$plugins_path" ]; then
|
|
echo "Found plugins at: $plugins_path"
|
|
for plugin in designer pdf svg sql help qml quick webengine bluetooth opengl printsupport test xml; do
|
|
rm -rf "$plugins_path/$plugin"
|
|
done
|
|
|
|
# Specific cleanups
|
|
rm -rf "$plugins_path/platforminputcontexts"
|
|
find "$plugins_path" -name "libqpdf.*" -delete
|
|
find "$plugins_path" -name "libqsvg.*" -delete
|
|
find "$plugins_path" -name "libqsvgicon.*" -delete
|
|
fi
|
|
|
|
# 4. Remove bloat Qt libraries / Frameworks
|
|
# Iterate specifically over unwanted Qt modules
|
|
# Matches both 'QtQml' (file) and 'QtQml.abi3.so' etc
|
|
for bloat in QtQml QtQmlMeta QtQmlModels QtQmlWorkerScript QtQuick QtQuickControls2 QtQuickTemplates2 QtWebEngine QtWebEngineCore QtVirtualKeyboard QtVirtualKeyboardQml QtOpenGL QtOpenGLWidgets QtPdf QtPdfWidgets QtDBus; do
|
|
find "$lib_dir" -maxdepth 2 -name "$bloat" -delete
|
|
find "$lib_dir" -maxdepth 2 -name "${bloat}.*" -delete
|
|
find "$lib_dir" -maxdepth 2 -name "*$bloat*" -delete
|
|
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"
|
|
done
|
|
fi
|
|
done
|
|
|
|
# Final sanity verification
|
|
echo "Trim complete. Remaining contents of lib (first 2 levels):"
|
|
find "$app_path/Contents" -type d -name "lib" -exec ls -R {} \; | head -n 50 || true
|
|
|
|
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 }}
|