Skip to content

Enhance Python 3.13 installation process in workflow and formula #73

Enhance Python 3.13 installation process in workflow and formula

Enhance Python 3.13 installation process in workflow and formula #73

Workflow file for this run

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