Fixed Formula #71
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: macOS DMG | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: [master] | |
| tags: ["[0-9]*.[0-9]*.[0-9]*"] | |
| pull_request: | |
| paths: | |
| - ".github/workflows/macos-dmg.yml" | |
| - "meson.build" | |
| - "data/**" | |
| - "src/**" | |
| - "macos/**" | |
| jobs: | |
| build-dmg: | |
| strategy: | |
| matrix: | |
| os: [macos-14] | |
| runs-on: ${{ matrix.os }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Show runner info | |
| run: | | |
| uname -a | |
| sw_vers | |
| sysctl -n machdep.cpu.brand_string || true | |
| - name: Setup Homebrew and dependencies | |
| run: | | |
| brew update-reset | |
| brew install meson ninja pkg-config glib gtk4 libadwaita pygobject3 gtksourceview5 python@3.13 | |
| brew --version | |
| brew list --versions | |
| - name: Build blueprint-compiler from git (v0.18.0) | |
| run: | | |
| set -euxo pipefail | |
| git clone --depth 1 --branch v0.18.0 https://gitlab.gnome.org/GNOME/blueprint-compiler.git | |
| cd blueprint-compiler | |
| meson setup build --prefix "$PWD/../bp_prefix" --buildtype=release | |
| meson compile -C build | |
| meson install -C build | |
| echo "$GITHUB_WORKSPACE/bp_prefix/bin" >> "$GITHUB_PATH" | |
| - name: Build project (Meson) | |
| run: | | |
| set -euxo pipefail | |
| PY_BIN="$(brew --prefix python@3.13)/bin/python3" | |
| echo "Using Python: ${PY_BIN}" | |
| export PYTHON="${PY_BIN}" | |
| meson setup build --prefix "$PWD/stage" --buildtype=release | |
| meson compile -C build | |
| meson install -C build | |
| - name: Prepare macOS bundle metadata | |
| run: | | |
| mkdir -p macos | |
| BREW_PREFIX="$(brew --prefix)" | |
| export BREW_PREFIX | |
| echo "BREW_PREFIX=${BREW_PREFIX}" >> "$GITHUB_ENV" | |
| echo "Detected Homebrew prefix: $BREW_PREFIX" | |
| - name: Build self-contained .app (vendor Python + GTK) | |
| run: | | |
| set -euxo pipefail | |
| APP="dist/SSH Studio.app" | |
| APPROOT="$APP/Contents" | |
| MACOS="$APPROOT/MacOS" | |
| RES="$APPROOT/Resources" | |
| FRAMEWORKS="$APPROOT/Frameworks" | |
| mkdir -p "$MACOS" "$RES/python" "$FRAMEWORKS" | |
| # 1) Copy your Python sources | |
| rsync -a src/ "$RES/python/ssh_studio/" | |
| # 2) Copy compiled GResource from Meson install | |
| APP_ID="io.github.BuddySirJava.SSH-Studio" | |
| RES_GRES="$RES/ssh-studio-resources.gresource" | |
| SRC_GRES="" | |
| for CAND in \ | |
| "stage/share/$APP_ID/ssh-studio-resources.gresource" \ | |
| "build/data/ssh-studio-resources.gresource" \ | |
| "_build/data/ssh-studio-resources.gresource"; do | |
| if [ -f "$CAND" ]; then | |
| SRC_GRES="$CAND" | |
| break | |
| fi | |
| done | |
| if [ -n "$SRC_GRES" ]; then | |
| install -m 0644 "$SRC_GRES" "$RES_GRES" | |
| else | |
| echo "ERROR: Could not find ssh-studio-resources.gresource in expected locations" >&2 | |
| ls -la stage/share "$PWD"/build/data "$PWD"/_build/data || true | |
| exit 1 | |
| fi | |
| ls -la "$RES" || true | |
| # 2b) Build macOS .icns app icon from our 512px PNG | |
| ICON_SRC="data/media/icon_512.png" | |
| if [ -f "$ICON_SRC" ]; then | |
| ICONSET_DIR="macos/Icon.iconset" | |
| rm -rf "$ICONSET_DIR" | |
| mkdir -p "$ICONSET_DIR" | |
| for sz in 16 32 64 128 256 512; do | |
| sips -s format png "$ICON_SRC" --resampleWidth $sz --out "$ICONSET_DIR/icon_${sz}x${sz}.png" >/dev/null | |
| done | |
| # @2x variants | |
| for sz in 16 32 128 256; do | |
| db=$(($sz*2)) | |
| sips -s format png "$ICON_SRC" --resampleWidth $db --out "$ICONSET_DIR/icon_${sz}x${sz}@2x.png" >/dev/null | |
| done | |
| iconutil -c icns "$ICONSET_DIR" -o "$RES/SSHStudio.icns" | |
| else | |
| echo "WARN: Icon source $ICON_SRC not found; bundle will lack .icns" >&2 | |
| fi | |
| # 3) Vendor Python.framework | |
| BREW_PREFIX="$(brew --prefix)" | |
| # Dereference symlinks so the framework is self-contained inside the app bundle | |
| rsync -aL "$BREW_PREFIX/Frameworks/Python.framework" "$FRAMEWORKS/" | |
| # Ensure a bin/python3 exists inside the vendored framework (Homebrew's may omit it) | |
| PYFW="$FRAMEWORKS/Python.framework" | |
| if [ -d "$PYFW/Versions/3.13" ]; then | |
| PYHOME_DIR="$PYFW/Versions/3.13" | |
| else | |
| PYHOME_DIR="$PYFW/Versions/Current" | |
| fi | |
| if [ ! -x "$PYHOME_DIR/bin/python3" ]; then | |
| mkdir -p "$PYHOME_DIR/bin" | |
| if [ -x "$PYHOME_DIR/Resources/Python.app/Contents/MacOS/Python" ]; then | |
| ln -sf "../Resources/Python.app/Contents/MacOS/Python" "$PYHOME_DIR/bin/python3" | |
| fi | |
| fi | |
| # 4) Vendor GTK & friends (most-used libs) | |
| for p in glib gtk4 libadwaita gtksourceview5 gdk-pixbuf pango cairo harfbuzz fribidi graphite2 libpng jpeg libtiff libepoxy libffi gettext; do | |
| if [ -d "$BREW_PREFIX/opt/$p/lib" ]; then | |
| mkdir -p "$FRAMEWORKS/$p/lib" | |
| # Copy only runtime libraries; exclude static archives and dev files | |
| rsync -a --prune-empty-dirs \ | |
| --include '*/' \ | |
| --include '*.dylib' --include '*.dylib.*' \ | |
| --include '*.so' --include '*.so.*' \ | |
| --exclude '*' \ | |
| "$BREW_PREFIX/opt/$p/lib/" "$FRAMEWORKS/$p/lib/" | |
| fi | |
| if [ -d "$BREW_PREFIX/opt/$p/lib/girepository-1.0" ]; then | |
| mkdir -p "$RES/girepository-1.0" | |
| rsync -a "$BREW_PREFIX/opt/$p/lib/girepository-1.0/" "$RES/girepository-1.0/" | |
| fi | |
| if [ -d "$BREW_PREFIX/opt/$p/share" ]; then | |
| mkdir -p "$RES/share/$p" | |
| rsync -a "$BREW_PREFIX/opt/$p/share/" "$RES/share/" | |
| fi | |
| done | |
| # Ensure no static archives slipped in (can break codesign) | |
| find "$FRAMEWORKS" \( -name '*.a' -o -name '*.la' \) -delete || true | |
| # 4b) Vendor PyGObject (gi) and PyCairo into bundled Python path | |
| for SITE in \ | |
| "$BREW_PREFIX/lib/python3.13/site-packages" \ | |
| "$BREW_PREFIX/opt/pygobject3/lib/python3.13/site-packages" \ | |
| "$BREW_PREFIX/opt/py3cairo/lib/python3.13/site-packages"; do | |
| if [ -d "$SITE/gi" ]; then | |
| rsync -a "$SITE/gi" "$RES/python/" | |
| fi | |
| if [ -d "$SITE/cairo" ]; then | |
| rsync -a "$SITE/cairo" "$RES/python/" | |
| fi | |
| done | |
| # 5) Schemas needed by GSettings | |
| mkdir -p "$RES/share/glib-2.0/schemas" | |
| rsync -a "$BREW_PREFIX/opt/glib/share/glib-2.0/schemas/" "$RES/share/glib-2.0/schemas/" | |
| glib-compile-schemas "$RES/share/glib-2.0/schemas" | |
| # 6) Launcher that sets env so app uses bundled runtimes | |
| cat > "$MACOS/ssh-studio" <<'SH' | |
| #!/bin/bash | |
| SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" | |
| RES="$SCRIPT_DIR/../Resources" | |
| FW="$SCRIPT_DIR/../Frameworks" | |
| export RES | |
| if [ -d "$FW/Python.framework/Versions/3.13" ]; then | |
| export PYTHONHOME="$FW/Python.framework/Versions/3.13" | |
| else | |
| export PYTHONHOME="$FW/Python.framework/Versions/Current" | |
| fi | |
| export PYTHONPATH="$RES/python" | |
| export DYLD_FALLBACK_LIBRARY_PATH="$FW:$FW/glib/lib:$FW/gtk4/lib:$FW/libadwaita/lib:$FW/pango/lib:$FW/cairo/lib:$FW/gtksourceview5/lib" | |
| export GI_TYPELIB_PATH="$RES/girepository-1.0" | |
| export XDG_DATA_DIRS="$RES/share" | |
| export GSETTINGS_SCHEMA_DIR="$RES/share/glib-2.0/schemas" | |
| export GTK_DATA_PREFIX="$RES" | |
| # Register GResource then run app | |
| PYBIN="$PYTHONHOME/bin/python3" | |
| if [ ! -x "$PYBIN" ]; then | |
| # Fallback to framework embedded app binary if bin/python3 is absent | |
| if [ -x "$PYTHONHOME/Resources/Python.app/Contents/MacOS/Python" ]; then | |
| PYBIN="$PYTHONHOME/Resources/Python.app/Contents/MacOS/Python" | |
| fi | |
| fi | |
| "$PYBIN" - <<'PY' | |
| import os, sys | |
| from gi.repository import Gio | |
| res_dir = os.environ.get('RES') | |
| candidates = [] | |
| if res_dir: | |
| candidates.append(os.path.join(res_dir, 'ssh-studio-resources.gresource')) | |
| candidates.append(os.path.join(res_dir, 'share', 'io.github.BuddySirJava.SSH-Studio', 'ssh-studio-resources.gresource')) | |
| res_path = next((c for c in candidates if os.path.exists(c)), None) | |
| if not res_path: | |
| raise SystemExit('ssh-studio-resources.gresource not found in expected locations') | |
| Gio.resources_register(Gio.Resource.load(res_path)) | |
| if res_dir: | |
| sys.path.insert(0, os.path.join(res_dir, 'python')) | |
| from ssh_studio import main as _main | |
| sys.exit(_main.main()) | |
| PY | |
| SH | |
| chmod 0755 "$MACOS/ssh-studio" | |
| # 7) Minimal Info.plist (if you’re not generating it already) | |
| cat > "$APPROOT/Info.plist" <<'PLIST' | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"><dict> | |
| <key>CFBundleIdentifier</key><string>io.github.BuddySirJava.SSH-Studio</string> | |
| <key>CFBundleName</key><string>SSH Studio</string> | |
| <key>CFBundleExecutable</key><string>ssh-studio</string> | |
| <key>CFBundleIconFile</key><string>SSHStudio</string> | |
| <key>CFBundlePackageType</key><string>APPL</string> | |
| <key>LSMinimumSystemVersion</key><string>11.0</string> | |
| </dict></plist> | |
| PLIST | |
| - name: List .app contents | |
| run: | | |
| set -euxo pipefail | |
| echo 'self-contained app:' || true | |
| ls -R 'dist/SSH Studio.app/Contents' || true | |
| - name: Ad-hoc codesign app bundle | |
| run: | | |
| set -euxo pipefail | |
| APP="dist/SSH Studio.app" | |
| # Ensure files are writable and clear any quarantine attrs | |
| chmod -R u+rw "$APP" | |
| xattr -cr "$APP" || true | |
| # Sign only Mach-O binaries and libraries to avoid codesign errors on non-bundles | |
| find "$APP/Contents" -type f \ | |
| -exec sh -c 'file -b "$1" | grep -q "Mach-O" && codesign --force --sign - --timestamp=none "$1" || true' _ {} \; | |
| # Finally sign the app wrapper (no --deep) | |
| codesign --force --sign - --timestamp=none "$APP" | |
| codesign --verify --verbose=2 "$APP" || (codesign --display --verbose=5 "$APP"; exit 1) | |
| - name: Verify permissions and Info.plist | |
| run: | | |
| set -euxo pipefail | |
| APP="dist/SSH Studio.app" | |
| chmod +x "$APP/Contents/MacOS/ssh-studio" | |
| plutil -lint "$APP/Contents/Info.plist" | |
| # Show signature and linkage (non-fatal) | |
| codesign -dv --verbose=4 "$APP" || true | |
| otool -L "$APP/Contents/MacOS/ssh-studio" || true | |
| - name: Gatekeeper assessment (non-fatal) | |
| run: | | |
| set -euxo pipefail | |
| APP="dist/SSH Studio.app" | |
| spctl --assess --type execute -v "$APP" || true | |
| - name: Developer ID sign app (optional) | |
| env: | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_DEVELOPER_CERT_BASE64: ${{ secrets.APPLE_DEVELOPER_CERT_BASE64 }} | |
| APPLE_DEVELOPER_CERT_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERT_PASSWORD }} | |
| run: | | |
| set -euxo pipefail | |
| APP="dist/SSH Studio.app" | |
| if [ -z "${APPLE_SIGNING_IDENTITY:-}" ] || [ -z "${APPLE_DEVELOPER_CERT_BASE64:-}" ] || [ -z "${APPLE_DEVELOPER_CERT_PASSWORD:-}" ]; then | |
| echo "Signing secrets not provided; skipping Developer ID signing."; exit 0; fi | |
| KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" | |
| KEYCHAIN_PWD="$(openssl rand -hex 12)" | |
| security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH" | |
| echo "$APPLE_DEVELOPER_CERT_BASE64" | base64 --decode > "$RUNNER_TEMP/dev_cert.p12" | |
| security import "$RUNNER_TEMP/dev_cert.p12" -k "$KEYCHAIN_PATH" -P "$APPLE_DEVELOPER_CERT_PASSWORD" -A | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db | |
| # Re-sign Mach-O components, then the app wrapper (no --deep) | |
| find "$APP/Contents" -type f \ | |
| -exec sh -c 'file -b "$1" | grep -q "Mach-O" && codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$1" || true' _ {} \; | |
| codesign --force --options runtime --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$APP" | |
| codesign --verify --strict --verbose=2 "$APP" | |
| - name: Create DMG | |
| run: | | |
| set -euxo pipefail | |
| VER=$(sed -n "s/.*version: '\([^']*\)'.*/\1/p" meson.build | head -n1) | |
| ARCH=$(uname -m) | |
| mkdir -p dmgroot | |
| # Prefer self-contained app for distribution | |
| cp -R "dist/SSH Studio.app" "dmgroot/SSH Studio.app" | |
| ln -s /Applications dmgroot/Applications | |
| hdiutil create -volname "SSH Studio" -srcfolder dmgroot -ov -fs HFS+ "ssh-studio-${VER}-${ARCH}.dmg" | |
| - name: Developer ID sign DMG (optional) | |
| env: | |
| APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} | |
| APPLE_DEVELOPER_CERT_BASE64: ${{ secrets.APPLE_DEVELOPER_CERT_BASE64 }} | |
| APPLE_DEVELOPER_CERT_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERT_PASSWORD }} | |
| run: | | |
| set -euxo pipefail | |
| if [ -z "${APPLE_SIGNING_IDENTITY:-}" ] || [ -z "${APPLE_DEVELOPER_CERT_BASE64:-}" ] || [ -z "${APPLE_DEVELOPER_CERT_PASSWORD:-}" ]; then | |
| echo "Signing secrets not provided; skipping DMG signing."; exit 0; fi | |
| VER=$(sed -n "s/.*version: '\([^']*\)'.*/\1/p" meson.build | head -n1) | |
| ARCH=$(uname -m) | |
| DMG="ssh-studio-${VER}-${ARCH}.dmg" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain-db" | |
| if [ ! -f "$RUNNER_TEMP/dev_cert.p12" ]; then | |
| echo "$APPLE_DEVELOPER_CERT_BASE64" | base64 --decode > "$RUNNER_TEMP/dev_cert.p12" | |
| fi | |
| if [ ! -f "$KEYCHAIN_PATH" ]; then | |
| KEYCHAIN_PWD="$(openssl rand -hex 12)" | |
| security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN_PATH" | |
| security import "$RUNNER_TEMP/dev_cert.p12" -k "$KEYCHAIN_PATH" -P "$APPLE_DEVELOPER_CERT_PASSWORD" -A | |
| security list-keychain -d user -s "$KEYCHAIN_PATH" login.keychain-db | |
| fi | |
| codesign --force --timestamp --sign "$APPLE_SIGNING_IDENTITY" "$DMG" | |
| spctl --assess --type open -v "$DMG" || true | |
| - name: Notarize DMG with notarytool (API key) (optional) | |
| env: | |
| NOTARYTOOL_KEY_ID: ${{ secrets.NOTARYTOOL_KEY_ID }} | |
| NOTARYTOOL_ISSUER_ID: ${{ secrets.NOTARYTOOL_ISSUER_ID }} | |
| NOTARYTOOL_PRIVATE_KEY: ${{ secrets.NOTARYTOOL_PRIVATE_KEY }} | |
| run: | | |
| set -euxo pipefail | |
| if [ -z "${NOTARYTOOL_KEY_ID:-}" ] || [ -z "${NOTARYTOOL_ISSUER_ID:-}" ] || [ -z "${NOTARYTOOL_PRIVATE_KEY:-}" ]; then | |
| echo "Notary API key secrets not provided; skipping API-key notarization."; exit 0; fi | |
| VER=$(sed -n "s/.*version: '\([^']*\)'.*/\1/p" meson.build | head -n1) | |
| ARCH=$(uname -m) | |
| DMG="ssh-studio-${VER}-${ARCH}.dmg" | |
| KEYFILE="$RUNNER_TEMP/AuthKey.p8" | |
| echo "$NOTARYTOOL_PRIVATE_KEY" > "$KEYFILE" | |
| xcrun notarytool submit "$DMG" \ | |
| --key "$KEYFILE" \ | |
| --key-id "$NOTARYTOOL_KEY_ID" \ | |
| --issuer "$NOTARYTOOL_ISSUER_ID" \ | |
| --wait | |
| xcrun stapler staple "$DMG" | |
| - name: Notarize DMG with notarytool (Apple ID) (optional) | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| run: | | |
| set -euxo pipefail | |
| # Only run if API key secrets are missing but Apple ID-based secrets are present | |
| if [ -n "${NOTARYTOOL_KEY_ID:-}" ] && [ -n "${NOTARYTOOL_ISSUER_ID:-}" ] && [ -n "${NOTARYTOOL_PRIVATE_KEY:-}" ]; then | |
| echo "API key provided; skipping Apple ID notarization path."; exit 0; fi | |
| if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ] || [ -z "${APPLE_APP_SPECIFIC_PASSWORD:-}" ]; then | |
| echo "Apple ID notarization secrets not provided; skipping."; exit 0; fi | |
| VER=$(sed -n "s/.*version: '\([^']*\)'.*/\1/p" meson.build | head -n1) | |
| ARCH=$(uname -m) | |
| DMG="ssh-studio-${VER}-${ARCH}.dmg" | |
| xcrun notarytool submit "$DMG" \ | |
| --apple-id "$APPLE_ID" \ | |
| --team-id "$APPLE_TEAM_ID" \ | |
| --password "$APPLE_APP_SPECIFIC_PASSWORD" \ | |
| --wait | |
| xcrun stapler staple "$DMG" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ssh-studio-dmg-${{ matrix.os }} | |
| path: | | |
| *.dmg |