Ship only the native Swift app on macOS.

GTK packaging stays for Linux and Windows; the Mac installer and updates use the Swift zip.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
pepaskopec-blip 2026-09-21 12:18:32 +02:00
commit 011bc3c685
7 changed files with 71 additions and 524 deletions

View file

@ -86,14 +86,10 @@ jobs:
macos:
name: macOS app
runs-on: macos-14
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
brew install gtk4 pkg-config
# Developer ID lives in repo secrets. Without it the bundle is only
# ad-hoc signed and Gatekeeper still blocks downloaded copies.
# Do not use `secrets` in `if:` — GitHub rejects the whole workflow.
@ -123,9 +119,10 @@ jobs:
$(security list-keychains -d user | tr -d '"')
rm -f "$cert"
- name: Build .app zip
- name: Build Swift .app zip
env:
COMMIT: ${{ github.sha }}
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
@ -134,8 +131,8 @@ jobs:
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
chmod +x scripts/bundle-macos.sh
./scripts/bundle-macos.sh
chmod +x scripts/bundle-macos-swift.sh
./scripts/bundle-macos-swift.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
@ -215,62 +212,9 @@ jobs:
name: maturita-ios
path: dist-ios/maturita-ios.ipa
macos-swift:
name: macOS Swift app
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- name: Import Developer ID certificate
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
run: |
set -euo pipefail
if [ -z "${MACOS_CERTIFICATE:-}" ]; then
echo "No Developer ID certificate; the bundle will be ad-hoc signed."
exit 0
fi
cert="$RUNNER_TEMP/certificate.p12"
keychain="$RUNNER_TEMP/signing.keychain-db"
pass="$(openssl rand -base64 32)"
echo "$MACOS_CERTIFICATE" | base64 --decode > "$cert"
security create-keychain -p "$pass" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$pass" "$keychain"
security import "$cert" -k "$keychain" \
-P "$MACOS_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s \
-k "$pass" "$keychain"
security list-keychains -d user -s "$keychain" \
$(security list-keychains -d user | tr -d '"')
rm -f "$cert"
- name: Build Swift .app zip
env:
COMMIT: ${{ github.sha }}
DEVELOPER_DIR: /Applications/Xcode.app/Contents/Developer
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
run: |
chmod +x scripts/bundle-macos-swift.sh
./scripts/bundle-macos-swift.sh
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: maturita-macos-swift
path: dist/maturita-macos-swift-*.zip
publish:
name: Publish to builds branch
needs: [linux, windows, macos, android, ios, macos-swift]
needs: [linux, windows, macos, android, ios]
# One broken platform must not freeze the others: publish whatever was
# built and keep the previous package for anything that failed.
if: ${{ !cancelled() && contains(needs.*.result, 'success') }}
@ -296,6 +240,7 @@ jobs:
run: |
set -euo pipefail
cp -f fresh/* staged/ 2>/dev/null || true
rm -f staged/maturita-macos-swift-*.zip staged/VERSION-macos-swift
chmod +x installers/pack-installer-zips.sh
./installers/pack-installer-zips.sh
cp installers/maturita-installer-linux.sh staged/
@ -307,7 +252,7 @@ jobs:
# here, so a platform that failed this run is not offered as new.
[ -f fresh/maturita-android.apk ] && echo "${{ github.sha }}" > staged/VERSION-android
[ -f fresh/maturita-ios.ipa ] && echo "${{ github.sha }}" > staged/VERSION-ios
ls fresh/maturita-macos-swift-*.zip >/dev/null 2>&1 && echo "${{ github.sha }}" > staged/VERSION-macos-swift
ls fresh/maturita-macos-*.zip >/dev/null 2>&1 && echo "${{ github.sha }}" > staged/VERSION-macos
ls fresh staged
# The branch is rewritten from scratch every time rather than added to,

View file

@ -99,8 +99,8 @@ bundle: $(TARGET)
@echo "Bundled into dist/ - run dist/$(TARGET)"
else ifeq ($(UNAME_S),Darwin)
bundle:
@chmod +x scripts/bundle-macos.sh
@./scripts/bundle-macos.sh
@chmod +x scripts/bundle-macos-swift.sh
@./scripts/bundle-macos-swift.sh
else
bundle:
@chmod +x scripts/bundle-linux.sh

View file

@ -1,6 +1,6 @@
# maturita.c
Vzdělávací GTK 4 aplikace k přípravě na maturitu. / An educational GTK 4 app for the Czech maturita exam.
Vzdělávací aplikace k přípravě na maturitu. / An educational app for the Czech maturita exam.
**[Česky](#česky)** · **[English](#english)**
@ -53,10 +53,9 @@ spolehlivější instalátor výše, nebo stažení přímo z té stránky větv
- [maturita-linux-x86_64.AppImage](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-linux-x86_64.AppImage)
- [maturita-android.apk](https://github.com/pepaskopec-blip/maturita.c/raw/refs/heads/builds/maturita-android.apk)
- [maturita-ios.ipa](https://github.com/pepaskopec-blip/maturita.c/raw/refs/heads/builds/maturita-ios.ipa)
- [maturita-macos-swift-arm64.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-macos-swift-arm64.zip)
Postup a nastavení se ukládají vedle `.app` / složky / AppImage do `progress/`.
Na Androidu a iOS do úložiště aplikace (stejné statistiky, témata a jazyk).
Na Androidu, iOS a macOS do úložiště aplikace (stejné statistiky, témata a jazyk).
Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#sestavení-ze-zdroje).
@ -81,9 +80,12 @@ Intel Mac, jiná architektura nebo úpravy kódu → [sestavení ze zdroje](#ses
### Sestavení ze zdroje
macOS je nativní SwiftUI aplikace (Xcode 26, macOS 26+). Linux a Windows
zůstávají GTK 4.
```bash
# macOS
brew install gtk4 gcc make pkg-config
# Xcode 26 z App Storu; žádné GTK
# Debian / Ubuntu
sudo apt install build-essential pkg-config libgtk-4-dev
@ -98,17 +100,28 @@ sudo pacman -S base-devel pkgconf gtk4
```bash
git clone https://github.com/pepaskopec-blip/maturita.c.git
cd maturita.c
make
make run # nebo ./maturita
make bundle # AppImage / .app zip / Windows složka s DLL
```
Volitelně CMake: `cmake -S . -B build && cmake --build build`.
Na Macu:
```bash
open Maturita.xcworkspace # scheme Maturita z macos/Maturita.xcodeproj
# nebo: make bundle # zip v dist/maturita-macos-arm64.zip
```
Na Linuxu a Windows:
```bash
make
make run # nebo ./maturita
make bundle # AppImage / Windows složka s DLL
```
Volitelně CMake (GTK): `cmake -S . -B build && cmake --build build`.
Na macOS `make bundle` automaticky vloží aktuální Git commit, aby fungovala
kontrola aktualizací. Při balení ze zdrojů bez `.git` zadejte
`COMMIT=<zdrojový-commit> make bundle`. Samotné `make` nadále vytváří vývojový
build bez aktualizací.
`COMMIT=<zdrojový-commit> make bundle`.
Aby stažená aplikace na macOS šla otevřít bez potvrzení v Nastavení, CI
potřebuje **Apple Developer Program** a GitHub Secrets:
@ -139,7 +152,7 @@ make bundle # dist/maturita.exe jde spustit i z Průzkumník
### Struktura
```
src/ C zdroje a maturita.h
src/ C zdroje (GTK přehrávač pro Linux a Windows)
android/ Jetpack Compose přehrávač (stejný obsah)
ios/ SwiftUI přehrávač (stejný obsah)
macos/ nativní SwiftUI aplikace pro Mac (stejný obsah)
@ -177,17 +190,9 @@ open Maturita.xcworkspace # v kořeni repozitáře
# nebo: make ios # unsigned IPA v dist-ios/maturita-ios.ipa
```
Nativní macOS ze zdroje (Xcode 26, macOS 26+):
```bash
open Maturita.xcworkspace # scheme Maturita z macos/Maturita.xcodeproj
# nebo: make macos # zip v dist/maturita-macos-swift-arm64.zip
```
Workspace odkazuje na `ios/Maturita.xcodeproj` a `macos/Maturita.xcodeproj`.
Projekty nepřesouvejte – build čte `../src` a `../scripts` a v kořeni by
`Maturita/` kolidovalo se zkompilovaným binárem `maturita`. Nativní Mac
aplikace sdílí cvičení s iOS přehrávačem; GTK verze (`make bundle`) zůstává.
`Maturita/` kolidovalo se zkompilovaným binárem `maturita`.
Licence: [GPL-3.0](LICENSE).
@ -240,10 +245,9 @@ above, or download from that branch page:
- [maturita-linux-x86_64.AppImage](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-linux-x86_64.AppImage)
- [maturita-android.apk](https://github.com/pepaskopec-blip/maturita.c/raw/refs/heads/builds/maturita-android.apk)
- [maturita-ios.ipa](https://github.com/pepaskopec-blip/maturita.c/raw/refs/heads/builds/maturita-ios.ipa)
- [maturita-macos-swift-arm64.zip](https://github.com/pepaskopec-blip/maturita.c/raw/builds/maturita-macos-swift-arm64.zip)
Progress and settings live in `progress/` next to the `.app` / folder /
AppImage. On Android and iOS they stay in app storage (same stats, themes, language).
AppImage. On Android, iOS and macOS they stay in app storage (same stats, themes, language).
Intel Mac, another architecture, or hacking on the code →
[build from source](#build-from-source).
@ -269,9 +273,12 @@ Intel Mac, another architecture, or hacking on the code →
### Build from source
macOS is a native SwiftUI app (Xcode 26, macOS 26+). Linux and Windows
stay on GTK 4.
```bash
# macOS
brew install gtk4 gcc make pkg-config
# Xcode 26 from the App Store; no GTK
# Debian / Ubuntu
sudo apt install build-essential pkg-config libgtk-4-dev
@ -286,17 +293,28 @@ sudo pacman -S base-devel pkgconf gtk4
```bash
git clone https://github.com/pepaskopec-blip/maturita.c.git
cd maturita.c
make
make run # or ./maturita
make bundle # AppImage / .app zip / Windows DLL folder
```
Optional CMake: `cmake -S . -B build && cmake --build build`.
On a Mac:
```bash
open Maturita.xcworkspace # scheme Maturita from macos/Maturita.xcodeproj
# or: make bundle # zip at dist/maturita-macos-arm64.zip
```
On Linux and Windows:
```bash
make
make run # or ./maturita
make bundle # AppImage / Windows DLL folder
```
Optional CMake (GTK): `cmake -S . -B build && cmake --build build`.
On macOS, `make bundle` embeds the current Git commit so the installed app
can check for updates. When packaging a source export without `.git`, use
`COMMIT=<source-commit> make bundle`. Plain `make` still produces a development
build without updates.
`COMMIT=<source-commit> make bundle`.
Skipping Gatekeeper on a downloaded macOS build needs an
**Apple Developer Program** membership and these GitHub Secrets:
@ -327,7 +345,7 @@ make bundle # dist/maturita.exe can be double-clicked
### Layout
```
src/ C sources and maturita.h
src/ C sources (GTK player for Linux and Windows)
android/ Jetpack Compose player (same content)
ios/ SwiftUI player (same content)
macos/ native SwiftUI Mac app (same content)
@ -365,17 +383,8 @@ open Maturita.xcworkspace # at the repo root
# or: make ios # unsigned IPA at dist-ios/maturita-ios.ipa
```
Native macOS from source (Xcode 26, macOS 26+):
```bash
open Maturita.xcworkspace # scheme Maturita from macos/Maturita.xcodeproj
# or: make macos # zip at dist/maturita-macos-swift-arm64.zip
```
The workspace points at `ios/Maturita.xcodeproj` and `macos/Maturita.xcodeproj`.
Do not move the projects – the build reads `../src` and `../scripts`, and at
the root `Maturita/` would collide with the compiled `maturita` binary. The
native Mac app shares exercises with the iOS player; the GTK build
(`make bundle`) stays.
the root `Maturita/` would collide with the compiled `maturita` binary.
License: [GPL-3.0](LICENSE).

View file

@ -24,7 +24,7 @@ final class Updater {
let atom = try self.get("https://github.com/\(self.repo)/commits/\(self.branch).atom")
let remote: String
if let tip = self.atomTip(atom),
let marker = try? self.get("https://raw.githubusercontent.com/\(self.repo)/\(tip)/VERSION-macos-swift")
let marker = try? self.get("https://raw.githubusercontent.com/\(self.repo)/\(tip)/VERSION-macos")
.trimmingCharacters(in: .whitespacesAndNewlines),
marker.count >= 7 {
remote = marker
@ -90,9 +90,9 @@ final class Updater {
private static func assetName() -> String {
#if arch(arm64)
return "maturita-macos-swift-arm64.zip"
return "maturita-macos-arm64.zip"
#else
return "maturita-macos-swift-x86_64.zip"
return "maturita-macos-x86_64.zip"
#endif
}

View file

@ -27,7 +27,7 @@ case "$arch" in
esac
dd="$root/macos/DerivedData"
out_zip="maturita-macos-swift-${arch_tag}.zip"
out_zip="maturita-macos-${arch_tag}.zip"
mkdir -p "$dd" "$root/dist"
xcodebuild \

View file

@ -1,407 +1,5 @@
#!/usr/bin/env bash
# Build a self-contained macOS .app with GTK dylibs relocated into the bundle.
# Requires: make, pkg-config, GTK4 (Homebrew).
# Official macOS package is the native Swift app.
# The GTK bundler used to live here; Linux/Windows still use GTK.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
# An installed .app needs a source identity for the updater, including when
# packaged locally. CI can still supply COMMIT explicitly. Plain `make`
# builds keep their "dev" default.
if [[ -z "${COMMIT:-}" ]]; then
if [[ ! -e "$ROOT/.git" ]] ||
! COMMIT="$(git -C "$ROOT" rev-parse --verify HEAD 2>/dev/null)"; then
echo "error: cannot determine the source commit; set COMMIT when bundling without Git metadata" >&2
exit 1
fi
fi
export COMMIT
ARCH="$(uname -m)"
case "$ARCH" in
arm64|aarch64) ARCH_TAG=arm64 ;;
x86_64) ARCH_TAG=x86_64 ;;
*)
echo "Unsupported architecture: $ARCH" >&2
exit 1
;;
esac
APP_NAME="Maturita.app"
APP="$ROOT/dist/$APP_NAME"
MACOS="$APP/Contents/MacOS"
RES="$APP/Contents/Resources"
FW="$APP/Contents/Frameworks"
ICON_512="$ROOT/data/share/icons/hicolor/512x512/apps/maturita.png"
OUT_ZIP="maturita-macos-${ARCH_TAG}.zip"
BREW_PREFIX="$(brew --prefix 2>/dev/null || echo /opt/homebrew)"
echo "==> Building maturita"
make -C "$ROOT" clean
make -C "$ROOT"
echo "==> Creating $APP_NAME"
rm -rf "$APP"
mkdir -p "$MACOS" "$RES/icons/hicolor/512x512/apps" "$RES/share" "$FW"
cp -f "$ROOT/maturita" "$MACOS/maturita-bin"
cp -f "$ROOT/data/style.css" "$RES/style.css"
cp -f "$ROOT/data/changelog.txt" "$RES/changelog.txt"
cp -f "$ICON_512" "$RES/icons/hicolor/512x512/apps/maturita.png"
cp -R "$ROOT/data/share/." "$RES/share/"
# Resolve an absolute Homebrew-style dependency to a real file on disk.
resolve_lib() {
local dep="$1"
local candidate
if [[ -f "$dep" ]]; then
realpath "$dep"
return 0
fi
# Try common Homebrew layouts for the basename.
local base
base="$(basename "$dep")"
for candidate in \
"$BREW_PREFIX/lib/$base" \
"$BREW_PREFIX/opt"/*/lib/"$base" \
"$BREW_PREFIX/Cellar"/*/*/lib/"$base"; do
if [[ -f "$candidate" ]]; then
realpath "$candidate"
return 0
fi
done
return 1
}
# Recursively copy non-system dylibs into Frameworks and rewrite load paths.
# Unlike dylibbundler, this never prompts interactively and fails loudly.
bundle_deps() {
local target="$1"
local line dep name dest real
while IFS= read -r line; do
dep="$(echo "$line" | awk '{print $1}')"
case "$dep" in
/System/*|/usr/lib/*|@executable_path/*|@loader_path/*)
continue
;;
@rpath/*)
name="$(basename "$dep")"
if ! real="$(resolve_lib "$BREW_PREFIX/lib/$name")"; then
echo "error: cannot resolve @rpath dependency '$dep' needed by $target" >&2
exit 1
fi
;;
/*)
name="$(basename "$dep")"
case "$name" in
libjpeg*.dylib) name="libjpeg.8.dylib" ;;
esac
if [[ -f "$dep" ]]; then
real="$(realpath "$dep")"
elif ! real="$(resolve_lib "$dep")"; then
echo "error: cannot resolve dependency '$dep' needed by $target" >&2
exit 1
fi
;;
*)
continue
;;
esac
dest="$FW/$name"
if [[ ! -f "$dest" ]]; then
echo " + $name (from $real)"
cp -f "$real" "$dest"
chmod u+w "$dest"
install_name_tool -id "@executable_path/../Frameworks/$name" "$dest" 2>/dev/null || true
codesign --force --sign - "$dest" >/dev/null 2>&1 || true
bundle_deps "$dest"
fi
# Rewrite matching absolute / @rpath references on the target.
while IFS= read -r changed; do
changed="$(echo "$changed" | awk '{print $1}')"
case "$changed" in
/*|@rpath/*)
if [[ "$(basename "$changed")" == "$name" ]] || \
[[ "$(basename "$changed")" == "$(basename "$real")" ]] || \
{ [[ "$name" == "libjpeg.8.dylib" ]] && [[ "$(basename "$changed")" == libjpeg* ]]; }; then
install_name_tool -change "$changed" \
"@executable_path/../Frameworks/$name" "$target" 2>/dev/null || true
fi
;;
esac
done < <(otool -L "$target" | tail -n +2)
done < <(otool -L "$target" | tail -n +2)
}
echo "==> Bundling GTK / GLib dylibs into Frameworks"
bundle_deps "$MACOS/maturita-bin"
codesign --force --sign - "$MACOS/maturita-bin" >/dev/null 2>&1 || true
echo "==> Verifying Frameworks completeness"
MISSING=0
check_file_deps() {
local file="$1"
local line dep name
while IFS= read -r line; do
dep="$(echo "$line" | awk '{print $1}')"
case "$dep" in
@executable_path/../Frameworks/*)
name="$(basename "$dep")"
if [[ ! -f "$FW/$name" ]]; then
echo "MISSING: $name (required by $(basename "$file"))" >&2
MISSING=1
fi
;;
@rpath/*)
echo "UNRESOLVED @rpath: $dep (in $(basename "$file"))" >&2
MISSING=1
;;
/opt/homebrew/*|/usr/local/*)
echo "UNBUNDLED absolute dep still present: $dep (in $(basename "$file"))" >&2
MISSING=1
;;
esac
done < <(otool -L "$file" | tail -n +2)
}
check_file_deps "$MACOS/maturita-bin"
for f in "$FW"/*.dylib; do
check_file_deps "$f"
done
if [[ "$MISSING" -ne 0 ]]; then
echo "error: macOS bundle is incomplete" >&2
exit 1
fi
echo " Frameworks: $(ls "$FW" | wc -l | tr -d ' ') dylibs"
# Copy GLib / GTK runtime data (schemas, modules, icon themes).
copy_pkg_share() {
local prefix
prefix="$(pkg-config --variable=prefix "$1" 2>/dev/null || true)"
[[ -n "$prefix" && -d "$prefix/share" ]] || return 0
mkdir -p "$RES/share"
for sub in glib-2.0 gtk-4.0 icons themes locale; do
if [[ -d "$prefix/share/$sub" ]]; then
rsync -a "$prefix/share/$sub" "$RES/share/" 2>/dev/null \
|| cp -R "$prefix/share/$sub" "$RES/share/"
fi
done
}
copy_pkg_share gtk4
copy_pkg_share glib-2.0
if [[ -d "$RES/share/glib-2.0/schemas" ]] && command -v glib-compile-schemas >/dev/null 2>&1; then
glib-compile-schemas "$RES/share/glib-2.0/schemas" || true
fi
# Launcher sets GTK search paths then execs the real binary.
cat > "$MACOS/maturita" <<'EOF'
#!/bin/bash
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
export DYLD_LIBRARY_PATH="$ROOT/Frameworks${DYLD_LIBRARY_PATH:+:$DYLD_LIBRARY_PATH}"
export XDG_DATA_DIRS="$ROOT/Resources/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export GSETTINGS_SCHEMA_DIR="$ROOT/Resources/share/glib-2.0/schemas"
export GTK_DATA_PREFIX="$ROOT/Resources"
export GTK_EXE_PREFIX="$ROOT"
export XDG_DATA_HOME="$ROOT/Resources"
exec "$ROOT/MacOS/maturita-bin" "$@"
EOF
chmod +x "$MACOS/maturita"
cat > "$APP/Contents/Info.plist" <<EOF
<?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>CFBundleName</key>
<string>maturita.c</string>
<key>CFBundleDisplayName</key>
<string>maturita.c</string>
<key>CFBundleIdentifier</key>
<string>org.maturita.Maturita</string>
<key>CFBundleVersion</key>
<string>${VERSION:-1.0}</string>
<key>CFBundleShortVersionString</key>
<string>${VERSION:-1.0}</string>
<key>CFBundleExecutable</key>
<string>maturita</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleIconFile</key>
<string>AppIcon</string>
<key>LSMinimumSystemVersion</key>
<string>12.0</string>
<key>NSHighResolutionCapable</key>
<true/>
</dict>
</plist>
EOF
if command -v sips >/dev/null 2>&1 && command -v iconutil >/dev/null 2>&1; then
ICONSET="$RES/AppIcon.iconset"
mkdir -p "$ICONSET"
for sz in 16 32 64 128 256 512; do
sips -z "$sz" "$sz" "$ICON_512" --out "$ICONSET/icon_${sz}x${sz}.png" >/dev/null
sips -z $((sz * 2)) $((sz * 2)) "$ICON_512" \
--out "$ICONSET/icon_${sz}x${sz}@2x.png" >/dev/null
done
iconutil -c icns -o "$RES/AppIcon.icns" "$ICONSET" && rm -rf "$ICONSET" || true
fi
# Developer ID + notarization is what lets a downloaded app open without
# System Settings. Without a certificate the bundle stays ad-hoc signed, which
# is enough for local runs but Gatekeeper still blocks internet downloads.
ENTITLEMENTS="$ROOT/data/macos/Maturita.entitlements"
pick_sign_identity() {
if [[ -n "${MACOS_SIGN_IDENTITY:-}" ]]; then
printf '%s\n' "$MACOS_SIGN_IDENTITY"
return
fi
local found
found="$(security find-identity -v -p codesigning 2>/dev/null \
| awk -F'"' '/Developer ID Application/ { print $2; exit }')"
if [[ -n "$found" ]]; then
printf '%s\n' "$found"
return
fi
printf '%s\n' "-"
}
sign_one() {
local path="$1"
local use_entitlements="${2:-}"
local args=(--force --sign "$IDENTITY")
if [[ "$IDENTITY" != "-" ]]; then
args+=(--options runtime --timestamp)
fi
if [[ "$use_entitlements" == "entitlements" && -f "$ENTITLEMENTS" ]]; then
args+=(--entitlements "$ENTITLEMENTS")
fi
codesign "${args[@]}" "$path"
}
sign_bundle() {
local f
echo "==> Signing $APP_NAME"
IDENTITY="$(pick_sign_identity)"
if [[ "$IDENTITY" == "-" ]]; then
echo " identity: ad-hoc (downloaded copies will still need Gatekeeper approval)"
else
echo " identity: $IDENTITY"
fi
shopt -s nullglob
for f in "$FW"/*.dylib; do
sign_one "$f"
done
shopt -u nullglob
sign_one "$MACOS/maturita-bin" entitlements
sign_one "$MACOS/maturita" entitlements
sign_one "$APP" entitlements
codesign --verify --strict --verbose=2 "$APP"
}
can_notarize() {
[[ "$IDENTITY" != "-" ]] || return 1
if [[ -n "${APPLE_API_KEY:-}" && -n "${APPLE_API_KEY_ID:-}" &&
-n "${APPLE_API_ISSUER:-}" ]]; then
return 0
fi
if [[ -n "${APPLE_ID:-}" && -n "${APPLE_APP_SPECIFIC_PASSWORD:-}" &&
-n "${APPLE_TEAM_ID:-}" ]]; then
return 0
fi
return 1
}
notarize_bundle() {
local zip notary_args keyfile
if [[ "$IDENTITY" == "-" ]]; then
return 0
fi
if ! can_notarize; then
if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then
echo "error: Developer ID is present but notarization secrets are missing." >&2
echo "Set APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER, or" >&2
echo "APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID." >&2
exit 1
fi
echo " warning: signed, but not notarized — Gatekeeper will still block downloads"
return 0
fi
echo "==> Notarizing $APP_NAME"
zip="$ROOT/dist/.notarize-$OUT_ZIP"
rm -f "$zip"
ditto -c -k --keepParent "$APP" "$zip"
notary_args=(submit "$zip" --wait)
keyfile=""
if [[ -n "${APPLE_API_KEY:-}" ]]; then
keyfile="$(mktemp)"
printf '%s\n' "$APPLE_API_KEY" > "$keyfile"
notary_args+=(--key "$keyfile" --key-id "$APPLE_API_KEY_ID"
--issuer "$APPLE_API_ISSUER")
else
notary_args+=(--apple-id "$APPLE_ID"
--password "$APPLE_APP_SPECIFIC_PASSWORD"
--team-id "$APPLE_TEAM_ID")
fi
xcrun notarytool "${notary_args[@]}"
[[ -n "$keyfile" ]] && rm -f "$keyfile"
rm -f "$zip"
xcrun stapler staple "$APP"
echo " stapled notarization ticket"
}
sign_bundle
echo "==> Smoke-testing launch"
if [[ -n "${GITHUB_ACTIONS:-}${CI:-}" ]]; then
echo " (CI: skipping GUI smoke test; Frameworks verification already passed)"
else
SMOKE_LOG="$(mktemp)"
"$MACOS/maturita" >"$SMOKE_LOG" 2>&1 &
SMOKE_PID=$!
sleep 3
if ! kill -0 "$SMOKE_PID" 2>/dev/null; then
if grep -q "Library not loaded" "$SMOKE_LOG"; then
echo "error: Maturita.app missing bundled libraries" >&2
cat "$SMOKE_LOG" >&2 || true
exit 1
fi
echo "error: Maturita.app exited during smoke test" >&2
cat "$SMOKE_LOG" >&2 || true
exit 1
fi
kill "$SMOKE_PID" 2>/dev/null || true
wait "$SMOKE_PID" 2>/dev/null || true
rm -f "$SMOKE_LOG"
echo " smoke test OK (process stayed up)"
fi
notarize_bundle
echo "==> Zipping"
rm -f "$ROOT/dist/$OUT_ZIP"
(
cd "$ROOT/dist"
zip -qry "$OUT_ZIP" "$APP_NAME"
)
echo "Created dist/$OUT_ZIP"
exec "$(cd "$(dirname "$0")" && pwd)/bundle-macos-swift.sh" "$@"

View file

@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Exercise build identity at the real bundler entry point without building GTK.
# Exercise build identity at the real bundler entry point without Xcode.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
@ -12,28 +12,23 @@ fail() {
}
mkdir -p "$TMP/bin"
cat > "$TMP/bin/make" <<'EOF'
cat > "$TMP/bin/xcodebuild" <<'EOF'
#!/usr/bin/env bash
printf '%s\n' "${COMMIT-UNSET}" >> "$MATURITA_TEST_MAKE_LOG"
if [[ "$*" == *" clean" ]]; then
exit 0
fi
# Stop before packaging, after observing the real build's environment.
exit 97
EOF
cat > "$TMP/bin/brew" <<'EOF'
#!/bin/sh
echo /unused-homebrew
EOF
cat > "$TMP/bin/uname" <<'EOF'
#!/bin/sh
echo arm64
EOF
chmod +x "$TMP/bin/make" "$TMP/bin/brew" "$TMP/bin/uname"
chmod +x "$TMP/bin/xcodebuild" "$TMP/bin/uname"
new_fixture() {
mkdir -p "$1/scripts"
cp "$ROOT/scripts/bundle-macos.sh" "$1/scripts/bundle-macos.sh"
cp "$ROOT/scripts/bundle-macos-swift.sh" "$1/scripts/bundle-macos-swift.sh"
chmod +x "$1/scripts/bundle-macos.sh" "$1/scripts/bundle-macos-swift.sh"
}
run_bundle() {
@ -64,15 +59,15 @@ expect_build_commit() {
cat "$OUTPUT_LOG" >&2
fail "expected the stubbed build to run (status $STATUS)"
}
[[ -f "$MAKE_LOG" ]] || fail "make was not called"
[[ -f "$MAKE_LOG" ]] || fail "xcodebuild was not called"
actual="$(cat "$MAKE_LOG")"
[[ "$actual" == "$expected"$'\n'"$expected" ]] ||
fail "clean/build did not both receive COMMIT=$expected (got '$actual')"
[[ "$actual" == "$expected" ]] ||
fail "build did not receive COMMIT=$expected (got '$actual')"
}
expect_missing_identity() {
[[ "$STATUS" -ne 0 ]] || fail 'missing identity unexpectedly succeeded'
[[ ! -e "$MAKE_LOG" ]] || fail 'missing identity reached make'
[[ ! -e "$MAKE_LOG" ]] || fail 'missing identity reached xcodebuild'
grep -q 'COMMIT' "$OUTPUT_LOG" || fail 'missing identity has no COMMIT guidance'
}