From 011bc3c68591f391c2f22b2279e4e74959c2c662 Mon Sep 17 00:00:00 2001 From: pepaskopec-blip Date: Mon, 21 Sep 2026 12:18:32 +0200 Subject: [PATCH] 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 --- .github/workflows/build.yml | 71 +---- Makefile | 4 +- README.md | 89 +++--- macos/Maturita/Update/Updater.swift | 6 +- scripts/bundle-macos-swift.sh | 2 +- scripts/bundle-macos.sh | 408 +--------------------------- scripts/test-macos-bundle-commit.sh | 23 +- 7 files changed, 75 insertions(+), 528 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b66b0ac..8c1ce2e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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, diff --git a/Makefile b/Makefile index 806d15b..7d24bde 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 3049df0..814b02e 100644 --- a/README.md +++ b/README.md @@ -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= make bundle`. Samotné `make` nadále vytváří vývojový -build bez aktualizací. +`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= make bundle`. Plain `make` still produces a development -build without updates. +`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). diff --git a/macos/Maturita/Update/Updater.swift b/macos/Maturita/Update/Updater.swift index d03948f..522c11f 100644 --- a/macos/Maturita/Update/Updater.swift +++ b/macos/Maturita/Update/Updater.swift @@ -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 } diff --git a/scripts/bundle-macos-swift.sh b/scripts/bundle-macos-swift.sh index f672efb..09f5f0d 100755 --- a/scripts/bundle-macos-swift.sh +++ b/scripts/bundle-macos-swift.sh @@ -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 \ diff --git a/scripts/bundle-macos.sh b/scripts/bundle-macos.sh index 40d04ca..36dc22d 100755 --- a/scripts/bundle-macos.sh +++ b/scripts/bundle-macos.sh @@ -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" < - - - - CFBundleName - maturita.c - CFBundleDisplayName - maturita.c - CFBundleIdentifier - org.maturita.Maturita - CFBundleVersion - ${VERSION:-1.0} - CFBundleShortVersionString - ${VERSION:-1.0} - CFBundleExecutable - maturita - CFBundlePackageType - APPL - CFBundleIconFile - AppIcon - LSMinimumSystemVersion - 12.0 - NSHighResolutionCapable - - - -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" "$@" diff --git a/scripts/test-macos-bundle-commit.sh b/scripts/test-macos-bundle-commit.sh index 5ecada7..5f86a82 100755 --- a/scripts/test-macos-bundle-commit.sh +++ b/scripts/test-macos-bundle-commit.sh @@ -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' }