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: 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")" # Create ZIP (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" # Create DMG (Manual) echo "Creating DMG from $app_path..." mkdir -p dmg_stage cp -R "$app_path" "dmg_stage/" ln -s /Applications "dmg_stage/Applications" hdiutil create -volname "YTSage v${version}" -srcfolder "dmg_stage" -ov -format UDZO "artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" echo "Created: artifacts/YTSage-v${version}-${ARCH_SUFFIX}.dmg" rm -rf dmg_stage else echo "Error: .app bundle not found in dist/ or build/" exit 1 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 }}