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 }}