From 57e7f9fe2515d91bb2c81fb8ec79d33390fec845 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 12 Jun 2026 14:34:45 +0000 Subject: [PATCH] =?UTF-8?q?feat(release):=20production=20Apple=20builds=20?= =?UTF-8?q?=E2=80=94=20notarized=20macOS=20dmg=20+=20iOS=20TestFlight?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit release.yml (v* tags / dispatch, macos-arm64 runner): universal mac + iOS xcframework -> xcodebuild archive -> Developer ID export -> notarytool + staple -> dmg on the Gitea release; iOS archive uploads to TestFlight (app-store-connect/upload). Per-run throwaway keychain; ASC API key authenticates notarization, upload, and automatic-signing profile fetch. macOS App Store lane deferred (needs App Sandbox); tvOS deferred (tier-3 Rust targets). All app targets now share bundle ID io.unom.punktfunk — ONE App Store listing with universal purchase (decided pre-submission; effectively unchangeable after). ITSAppUsesNonExemptEncryption=false declared (standard-algorithm AES-GCM, exempt). build-xcframework.sh resolves Apple toolchains itself: cargo's HOST artifacts (proc-macros, build scripts) are loaded by the running OS, and a newer-than-OS beta Xcode ld emits LINKEDIT layouts dyld rejects ("mis-aligned LINKEDIT string pool" -> misleading E0463) — so prefer a non-beta Xcode for everything, fall back to CLT for mac-only slices (env untouched: an explicit DEVELOPER_DIR= trips xcrun's license check), refuse iOS/tvOS without a real Xcode (CLT has no iOS SDK). The runner plist no longer injects DEVELOPER_DIR for the same reason. punktfunk_Logo.icon: dropped the Xcode-27-beta-only Icon Composer features (refractivity, specular-location) — 26.5's actool crashes on them, and store builds must use release Xcode. Visual delta is the refraction/specular nuance only; re-author when 27 ships. Validated on home-mac-mini-1 with Xcode 26.5: mac+iOS xcframework slices, unified bundle IDs, signing-free app build. Co-Authored-By: Claude Fable 5 --- .gitea/workflows/release.yml | 214 ++++++++++++++++++ .../apple/App/punktfunk_Logo.icon/icon.json | 125 +++++----- clients/apple/Config/Info.plist | 5 + .../apple/Punktfunk.xcodeproj/project.pbxproj | 8 +- docs-site/content/docs/ci.md | 28 +++ scripts/build-xcframework.sh | 54 ++++- scripts/ci/setup-macos-runner.sh | 27 +-- 7 files changed, 363 insertions(+), 98 deletions(-) create mode 100644 .gitea/workflows/release.yml diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..d069f48 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,214 @@ +# Production Apple client builds — runs on the macos-arm64 runner (home-mac-mini-1). +# +# Tag v* (or workflow_dispatch): +# macOS -> Developer ID signed + notarized + stapled .dmg, attached to a Gitea +# release on tag pushes +# iOS -> archive + upload straight to TestFlight (App Store Connect) +# tvOS -> not built: the Rust core needs tier-3 targets (nightly -Zbuild-std) +# macOS App Store/TestFlight -> deferred: needs App Sandbox entitlements first +# (network client + Bonjour); the Developer ID build covers macOS today. +# +# One App Store listing for all platforms (universal purchase): every target shares the +# bundle ID io.unom.punktfunk. +# +# Secrets: DEVID_CERT_P12_B64 / DEVID_CERT_PASSWORD (Developer ID Application cert), +# ASC_API_KEY_P8 / ASC_API_KEY_ID / ASC_API_ISSUER_ID (App Store Connect API key — +# notarization, TestFlight upload, and automatic-signing profile fetch). +# +# Needs a RELEASE Xcode on the runner (App Store rejects beta-SDK builds); the workflow +# picks the first non-beta /Applications/Xcode*.app and only falls back to a beta with a +# loud warning. +name: release + +on: + push: + tags: ['v*'] + workflow_dispatch: + inputs: + testflight: + description: "Upload the iOS build to TestFlight (true/false)" + required: false + default: "true" + +jobs: + apple: + runs-on: macos-arm64 + timeout-minutes: 120 + env: + TEAM_ID: F4H37KF6WC + PROJECT: clients/apple/Punktfunk.xcodeproj + steps: + - uses: actions/checkout@v4 + + - name: Select release Xcode + run: | + DEV_DIR="" + for app in /Applications/Xcode.app /Applications/Xcode_*.app /Applications/Xcode-*.app; do + case "$app" in *beta*|*Beta*) continue;; esac + [ -x "$app/Contents/Developer/usr/bin/xcodebuild" ] && DEV_DIR="$app/Contents/Developer" && break + done + if [ -z "$DEV_DIR" ]; then + for app in /Applications/Xcode*.app; do + [ -x "$app/Contents/Developer/usr/bin/xcodebuild" ] && DEV_DIR="$app/Contents/Developer" && break + done + echo "::warning::No release Xcode found — using $DEV_DIR. TestFlight/App Store REJECTS beta-SDK builds." + fi + [ -n "$DEV_DIR" ] || { echo "no usable Xcode found" >&2; exit 1; } + # Scoped to xcodebuild steps only (XCODE_DEV_DIR, not DEVELOPER_DIR): cargo must + # keep the system-default linker — a newer-than-OS Xcode's ld produces dylibs the + # running dyld rejects, killing proc-macro loads (see build-xcframework.sh). + echo "XCODE_DEV_DIR=$DEV_DIR" >> "$GITHUB_ENV" + DEVELOPER_DIR="$DEV_DIR" xcodebuild -version + + - name: Version from tag + run: | + case "$GITHUB_REF" in + refs/tags/v*) V="${GITHUB_REF_NAME#v}" ;; + *) V="0.0.${GITHUB_RUN_NUMBER}" ;; + esac + echo "VERSION=$V" >> "$GITHUB_ENV" + echo "BUILD_NUM=$GITHUB_RUN_NUMBER" >> "$GITHUB_ENV" + echo "version $V build $GITHUB_RUN_NUMBER" + + - name: Rust toolchain (mac + iOS slices) + run: | + RUSTUP="$(command -v rustup || echo "$HOME/.cargo/bin/rustup")" + dirname "$RUSTUP" >> "$GITHUB_PATH" + "$RUSTUP" target add aarch64-apple-darwin x86_64-apple-darwin \ + aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios + + - name: Build PunktfunkCore.xcframework (mac + iOS) + run: BUILD_IOS=1 bash scripts/build-xcframework.sh + + - name: Import Developer ID certificate (throwaway keychain) + env: + P12_B64: ${{ secrets.DEVID_CERT_P12_B64 }} + P12_PASSWORD: ${{ secrets.DEVID_CERT_PASSWORD }} + run: | + KEYCHAIN="$RUNNER_TEMP/punktfunk-ci.keychain-db" + KEYCHAIN_PASS="$(uuidgen)" + echo "KEYCHAIN=$KEYCHAIN" >> "$GITHUB_ENV" + security create-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" + security set-keychain-settings -lut 7200 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PASS" "$KEYCHAIN" + printf '%s' "$P12_B64" | base64 -d > "$RUNNER_TEMP/devid.p12" + security import "$RUNNER_TEMP/devid.p12" -k "$KEYCHAIN" -P "$P12_PASSWORD" \ + -T /usr/bin/codesign -T /usr/bin/security + rm -f "$RUNNER_TEMP/devid.p12" + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PASS" "$KEYCHAIN" >/dev/null + security list-keychains -d user -s "$KEYCHAIN" login.keychain-db + security find-identity -v -p codesigning "$KEYCHAIN" + + - name: Stage App Store Connect API key + env: + ASC_P8: ${{ secrets.ASC_API_KEY_P8 }} + run: | + printf '%s' "$ASC_P8" > "$RUNNER_TEMP/asc.p8" + chmod 600 "$RUNNER_TEMP/asc.p8" + + - name: Archive macOS + run: | + DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \ + -project "$PROJECT" -scheme Punktfunk \ + -destination 'generic/platform=macOS' \ + -archivePath "$RUNNER_TEMP/Punktfunk-macos.xcarchive" \ + MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \ + -allowProvisioningUpdates \ + -authenticationKeyPath "$RUNNER_TEMP/asc.p8" \ + -authenticationKeyID "${{ secrets.ASC_API_KEY_ID }}" \ + -authenticationKeyIssuerID "${{ secrets.ASC_API_ISSUER_ID }}" + + - name: Export macOS (Developer ID) + run: | + cat > "$RUNNER_TEMP/export-devid.plist" < + + + + methoddeveloper-id + teamID$TEAM_ID + destinationexport + + + EOF + DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild -exportArchive \ + -archivePath "$RUNNER_TEMP/Punktfunk-macos.xcarchive" \ + -exportOptionsPlist "$RUNNER_TEMP/export-devid.plist" \ + -exportPath "$RUNNER_TEMP/export-devid" \ + -allowProvisioningUpdates \ + -authenticationKeyPath "$RUNNER_TEMP/asc.p8" \ + -authenticationKeyID "${{ secrets.ASC_API_KEY_ID }}" \ + -authenticationKeyIssuerID "${{ secrets.ASC_API_ISSUER_ID }}" + + - name: Notarized DMG + run: | + STAGE="$RUNNER_TEMP/dmg-stage" + mkdir -p "$STAGE" + cp -R "$RUNNER_TEMP/export-devid/Punktfunk.app" "$STAGE/" + ln -s /Applications "$STAGE/Applications" + DMG="$RUNNER_TEMP/Punktfunk-$VERSION.dmg" + hdiutil create -volname "Punktfunk" -srcfolder "$STAGE" -ov -format UDZO "$DMG" + DEVELOPER_DIR="$XCODE_DEV_DIR" xcrun notarytool submit "$DMG" --wait \ + --key "$RUNNER_TEMP/asc.p8" \ + --key-id "${{ secrets.ASC_API_KEY_ID }}" \ + --issuer "${{ secrets.ASC_API_ISSUER_ID }}" + DEVELOPER_DIR="$XCODE_DEV_DIR" xcrun stapler staple "$DMG" + echo "DMG=$DMG" >> "$GITHUB_ENV" + + - name: Attach DMG to Gitea release + if: startsWith(gitea.ref, 'refs/tags/') + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + API="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" + # Create the release (409 -> already exists, fetch it instead). + ID=$(curl -sf -X POST "$API/releases" \ + -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \ + -d "{\"tag_name\":\"$GITHUB_REF_NAME\",\"name\":\"$GITHUB_REF_NAME\"}" \ + | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])' \ + || curl -sf "$API/releases/tags/$GITHUB_REF_NAME" -H "Authorization: token $TOKEN" \ + | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])') + curl -sf -X POST "$API/releases/$ID/assets?name=Punktfunk-$VERSION.dmg" \ + -H "Authorization: token $TOKEN" \ + -F "attachment=@$DMG" >/dev/null + echo "attached Punktfunk-$VERSION.dmg to release $GITHUB_REF_NAME" + + - name: Archive iOS + upload to TestFlight + if: gitea.event_name != 'workflow_dispatch' || inputs.testflight == 'true' + run: | + DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild archive \ + -project "$PROJECT" -scheme Punktfunk-iOS \ + -destination 'generic/platform=iOS' \ + -archivePath "$RUNNER_TEMP/Punktfunk-ios.xcarchive" \ + MARKETING_VERSION="$VERSION" CURRENT_PROJECT_VERSION="$BUILD_NUM" \ + -allowProvisioningUpdates \ + -authenticationKeyPath "$RUNNER_TEMP/asc.p8" \ + -authenticationKeyID "${{ secrets.ASC_API_KEY_ID }}" \ + -authenticationKeyIssuerID "${{ secrets.ASC_API_ISSUER_ID }}" + cat > "$RUNNER_TEMP/export-appstore.plist" < + + + + methodapp-store-connect + destinationupload + teamID$TEAM_ID + + + EOF + DEVELOPER_DIR="$XCODE_DEV_DIR" xcodebuild -exportArchive \ + -archivePath "$RUNNER_TEMP/Punktfunk-ios.xcarchive" \ + -exportOptionsPlist "$RUNNER_TEMP/export-appstore.plist" \ + -exportPath "$RUNNER_TEMP/export-appstore" \ + -allowProvisioningUpdates \ + -authenticationKeyPath "$RUNNER_TEMP/asc.p8" \ + -authenticationKeyID "${{ secrets.ASC_API_KEY_ID }}" \ + -authenticationKeyIssuerID "${{ secrets.ASC_API_ISSUER_ID }}" + + - name: Clean up keychain + API key + if: always() + run: | + [ -n "${KEYCHAIN:-}" ] && security delete-keychain "$KEYCHAIN" 2>/dev/null || true + security list-keychains -d user -s login.keychain-db + rm -f "$RUNNER_TEMP/asc.p8" diff --git a/clients/apple/App/punktfunk_Logo.icon/icon.json b/clients/apple/App/punktfunk_Logo.icon/icon.json index 7ecdcab..80022bd 100644 --- a/clients/apple/App/punktfunk_Logo.icon/icon.json +++ b/clients/apple/App/punktfunk_Logo.icon/icon.json @@ -1,108 +1,87 @@ { - "features" : [ - "refractivity", - "specular-location" - ], - "fill" : { - "automatic-gradient" : "display-p3:0.39502,0.30640,0.96338,1.00000" + "fill": { + "automatic-gradient": "display-p3:0.39502,0.30640,0.96338,1.00000" }, - "groups" : [ + "groups": [ { - "layers" : [ + "layers": [ { - "image-name" : "punktfunk_Minimal_Icon-Composer_Layer-3.svg", - "name" : "punktfunk_Minimal_Icon-Composer_Layer-3" + "image-name": "punktfunk_Minimal_Icon-Composer_Layer-3.svg", + "name": "punktfunk_Minimal_Icon-Composer_Layer-3" } ], - "name" : "Group", - "refractivity" : { - "depth" : 0.0419921875, - "enabled" : true, - "strength" : 0.5463671875 + "name": "Group", + "shadow": { + "kind": "neutral", + "opacity": 0.6 }, - "shadow" : { - "kind" : "neutral", - "opacity" : 0.6 - }, - "translucency" : { - "enabled" : true, - "value" : 0.6 + "translucency": { + "enabled": true, + "value": 0.6 } }, { - "blur-material" : null, - "layers" : [ + "blur-material": null, + "layers": [ { - "image-name" : "punktfunk_Minimal_Icon-Composer_Layer-2 2.svg", - "name" : "punktfunk_Minimal_Icon-Composer_Layer-2" + "image-name": "punktfunk_Minimal_Icon-Composer_Layer-2 2.svg", + "name": "punktfunk_Minimal_Icon-Composer_Layer-2" } ], - "lighting" : "individual", - "name" : "Group", - "refractivity" : { - "depth" : 0.1, - "enabled" : false, - "strength" : 0.57 + "lighting": "individual", + "name": "Group", + "shadow": { + "kind": "layer-color", + "opacity": 0.56 }, - "shadow" : { - "kind" : "layer-color", - "opacity" : 0.56 - }, - "specular" : true, - "translucency" : { - "enabled" : true, - "value" : 1 + "specular": true, + "translucency": { + "enabled": true, + "value": 1 } }, { - "blur-material" : 0, - "layers" : [ + "blur-material": 0, + "layers": [ { - "fill-specializations" : [ + "fill-specializations": [ { - "appearance" : "dark", - "value" : { - "automatic-gradient" : "display-p3:0.44238,0.34595,0.99951,1.00000" + "appearance": "dark", + "value": { + "automatic-gradient": "display-p3:0.44238,0.34595,0.99951,1.00000" } } ], - "image-name" : "punktfunk_Minimal_Icon-Composer_Layer-1 2.svg", - "name" : "punktfunk_Minimal_Icon-Composer_Layer-1" + "image-name": "punktfunk_Minimal_Icon-Composer_Layer-1 2.svg", + "name": "punktfunk_Minimal_Icon-Composer_Layer-1" } ], - "refractivity" : { - "depth" : 0.5808984375, - "enabled" : true, - "strength" : 0.2508984375 + "shadow": { + "kind": "neutral", + "opacity": 0.6 }, - "shadow" : { - "kind" : "neutral", - "opacity" : 0.6 - }, - "specular" : "outside", - "translucency" : { - "enabled" : true, - "value" : 0.53 + "specular": true, + "translucency": { + "enabled": true, + "value": 0.53 } }, { - "layers" : [ - - ], - "shadow" : { - "kind" : "neutral", - "opacity" : 0.6 + "layers": [], + "shadow": { + "kind": "neutral", + "opacity": 0.6 }, - "translucency" : { - "enabled" : true, - "value" : 0.2 + "translucency": { + "enabled": true, + "value": 0.2 } } ], - "supported-platforms" : { - "circles" : [ + "supported-platforms": { + "circles": [ "watchOS" ], - "squares" : "shared" + "squares": "shared" } -} \ No newline at end of file +} diff --git a/clients/apple/Config/Info.plist b/clients/apple/Config/Info.plist index 2a8bac2..4ea7cf7 100644 --- a/clients/apple/Config/Info.plist +++ b/clients/apple/Config/Info.plist @@ -11,5 +11,10 @@ _punktfunk._udp + + ITSAppUsesNonExemptEncryption + diff --git a/clients/apple/Punktfunk.xcodeproj/project.pbxproj b/clients/apple/Punktfunk.xcodeproj/project.pbxproj index c610c3f..83cdd4a 100644 --- a/clients/apple/Punktfunk.xcodeproj/project.pbxproj +++ b/clients/apple/Punktfunk.xcodeproj/project.pbxproj @@ -441,7 +441,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk.ios; + PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -479,7 +479,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk.ios; + PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -510,7 +510,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk.tvos; + PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; @@ -539,7 +539,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 0.1; - PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk.tvos; + PRODUCT_BUNDLE_IDENTIFIER = io.unom.punktfunk; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SUPPORTED_PLATFORMS = "appletvos appletvsimulator"; diff --git a/docs-site/content/docs/ci.md b/docs-site/content/docs/ci.md index 49ba9f1..befa4c2 100644 --- a/docs-site/content/docs/ci.md +++ b/docs-site/content/docs/ci.md @@ -13,6 +13,7 @@ CI runs on **Gitea Actions** (`git.unom.io`, org `unom`). Three workflows in | `ci.yml` | push to `main`, PRs | `ubuntu-24.04` | Rust workspace (fmt · clippy `-D warnings` · build · test · C-ABI harness · generated-header drift) inside the `punktfunk-rust-ci` image; `web/` and `docs-site/` build + typecheck in `oven/bun:1` | | `docker.yml` | push to `main`, `v*` tags, manual | `ubuntu-24.04` | Builds + pushes the three images below (`latest` + `sha-` tags) | | `apple.yml` | push to `main`, PRs, manual | `macos-arm64` | Rust core → `PunktfunkCore.xcframework` → `swift build` + `swift test` in `clients/apple` | +| `release.yml` | `v*` tags, manual | `macos-arm64` | Production Apple builds: Developer-ID-signed, notarized, stapled macOS `.dmg` attached to the Gitea release + iOS archive uploaded to TestFlight | ## Dockerized pieces @@ -55,6 +56,33 @@ ssh enricobuehler@192.168.1.135 GITEA_RUNNER_TOKEN= bash -s \ < scripts/ci/setup-macos-runner.sh ``` +## Apple releases + +`release.yml` produces the production client builds on the Mac runner. All three app +targets share the bundle ID **`io.unom.punktfunk`** (one App Store listing, universal +purchase — effectively unchangeable after first submission). Secrets: +`DEVID_CERT_P12_B64`/`DEVID_CERT_PASSWORD` (Developer ID Application certificate, only +creatable by the account holder) and `ASC_API_KEY_P8`/`ASC_API_KEY_ID`/`ASC_API_ISSUER_ID` +(App Store Connect API key — notarization, TestFlight upload, automatic-signing profile +fetch). Signing uses a per-run throwaway keychain; nothing persists on the runner. +Per-platform state: + +- **macOS** — Developer ID export → `notarytool` → stapled `.dmg` on the Gitea release. + The Mac **App Store** lane is deferred: it requires App Sandbox entitlements + (network client + Bonjour) the app doesn't declare yet. +- **iOS** — archive + upload to TestFlight (`method: app-store-connect`, + `destination: upload`). Crypto is declared exempt (`ITSAppUsesNonExemptEncryption`, + `Config/Info.plist`) so builds don't stall on the compliance question. +- **tvOS** — not built: the Rust core needs tier-3 targets (nightly `-Zbuild-std`). + +The runner needs a **release (non-beta) Xcode** — App Store processing rejects beta-SDK +builds, and a beta is unusable for the Rust side too: a newer-than-OS ld emits dylibs the +running dyld rejects ("mis-aligned LINKEDIT string pool"), killing every proc-macro build +with a misleading `E0463 can't find crate`. `build-xcframework.sh` therefore resolves +toolchains itself: non-beta Xcode for everything; with only CLT + a beta present it +builds macOS slices against CLT (packaging via any Xcode — `-create-xcframework` does no +linking) and **refuses iOS/tvOS slices** (CLT has no iOS SDK). + ## Deployment `docker.yml`'s `deploy-docs` job ships this docs site after every image push: it syncs diff --git a/scripts/build-xcframework.sh b/scripts/build-xcframework.sh index 8f763fd..664a054 100644 --- a/scripts/build-xcframework.sh +++ b/scripts/build-xcframework.sh @@ -14,6 +14,45 @@ TARGETS_MAC=(aarch64-apple-darwin x86_64-apple-darwin) BUILD_IOS="${BUILD_IOS:-0}" # BUILD_IOS=1 adds iOS device + simulator slices (rustup targets aarch64-apple-ios{,-sim}) BUILD_TVOS="${BUILD_TVOS:-0}" # BUILD_TVOS=1 adds tvOS slices — TIER-3 Rust targets: needs `rustup toolchain install nightly` + `rustup component add rust-src --toolchain nightly` +# Toolchain resolution. Cargo's HOST artifacts (proc-macros, build scripts) are loaded by +# the RUNNING OS, so their linker must not be newer than it: a beta Xcode's ld emits +# LINKEDIT layouts the current dyld rejects ("mis-aligned LINKEDIT string pool"), and +# every proc-macro then dies with a misleading E0463 "can't find crate" — with the bad +# artifacts cached (cargo doesn't fingerprint the linker; rm -rf target after fixing). +# CLT is always dyld-safe but ships no iOS/tvOS SDKs. Resolution: a NON-BETA full Xcode +# for everything; with only a beta installed, macOS slices build against CLT and +# iOS/tvOS slices are refused. +pick_nonbeta_xcode() { + local app + for app in /Applications/Xcode.app /Applications/Xcode*.app; do + case "$app" in *[Bb]eta*) continue ;; esac + [ -x "$app/Contents/Developer/usr/bin/xcodebuild" ] && { echo "$app/Contents/Developer"; return; } + done +} +case "${DEVELOPER_DIR:-}" in *[Bb]eta*) unset DEVELOPER_DIR ;; esac # never let a beta in via env +if [[ -z "${DEVELOPER_DIR:-}" ]]; then + DEFAULT_DIR="$(xcode-select -p 2>/dev/null || true)" + case "$DEFAULT_DIR" in + *[Bb]eta*|*CommandLineTools*|'') + NONBETA="$(pick_nonbeta_xcode || true)" + if [[ -n "$NONBETA" ]]; then + export DEVELOPER_DIR="$NONBETA" + elif [[ "$BUILD_IOS" == "1" || "$BUILD_TVOS" == "1" ]]; then + echo "ERROR: iOS/tvOS slices need a full NON-BETA Xcode in /Applications" >&2 + echo " (CLT has no iOS SDK; a beta's ld breaks host proc-macro dylibs)." >&2 + exit 1 + elif [[ "$DEFAULT_DIR" != *CommandLineTools* ]]; then + echo "ERROR: xcode-select default is a beta (or missing) and no non-beta Xcode/CLT" >&2 + echo " fallback exists — install CLT or a release Xcode." >&2 + exit 1 + fi + # else: the default IS CLT — dyld-safe for the mac slices; deliberately leave the + # env untouched (an EXPLICIT DEVELOPER_DIR= export trips xcrun's Xcode + # license check when a full Xcode is also installed). + ;; + esac # a non-beta xcode-select default is fine as-is +fi + # Deployment targets must match Package.swift's platforms, or every consumer link emits # "object file was built for newer macOS version" warnings. for t in "${TARGETS_MAC[@]}"; do @@ -91,6 +130,19 @@ for obj in "$STAGE"/macos/libpunktfunk_core.a; do fi done +# -create-xcframework needs a full Xcode (CLT has no xcodebuild) but does NO linking — +# it only copies the libs and writes the bundle plist, so a beta Xcode is safe here. +XCODEBUILD=(xcodebuild) +if ! xcodebuild -version >/dev/null 2>&1; then + for app in /Applications/Xcode.app /Applications/Xcode*.app; do + if DEVELOPER_DIR="$app/Contents/Developer" xcodebuild -version >/dev/null 2>&1; then + XCODEBUILD=(env DEVELOPER_DIR="$app/Contents/Developer" xcodebuild) + echo "==> using $app for -create-xcframework" + break + fi + done +fi + rm -rf clients/apple/PunktfunkCore.xcframework -xcodebuild -create-xcframework "${ARGS[@]}" -output clients/apple/PunktfunkCore.xcframework +"${XCODEBUILD[@]}" -create-xcframework "${ARGS[@]}" -output clients/apple/PunktfunkCore.xcframework echo "OK: clients/apple/PunktfunkCore.xcframework" diff --git a/scripts/ci/setup-macos-runner.sh b/scripts/ci/setup-macos-runner.sh index 056bf0d..1b1355a 100644 --- a/scripts/ci/setup-macos-runner.sh +++ b/scripts/ci/setup-macos-runner.sh @@ -91,20 +91,10 @@ fi # runner ships as a root LaunchDaemon; installing it needs sudo once. Without sudo this # script still leaves a working (but reboot-volatile) nohup daemon behind. # PATH must carry the CLT tools, cargo, node and act_runner itself; jobs inherit it. -# If the system developer dir is CLT-only but a full Xcode is installed, hand jobs a -# DEVELOPER_DIR override — the per-process equivalent of `xcode-select -s`, no sudo needed. -DEVELOPER_DIR_XML="" -DEV_DIR="" -if ! /usr/bin/xcodebuild -version >/dev/null 2>&1; then - for app in /Applications/Xcode.app /Applications/Xcode*.app; do - if DEVELOPER_DIR="$app/Contents/Developer" /usr/bin/xcodebuild -version >/dev/null 2>&1; then - DEV_DIR="$app/Contents/Developer" - DEVELOPER_DIR_XML="DEVELOPER_DIR$DEV_DIR" - echo "==> using full Xcode at $app via DEVELOPER_DIR" - break - fi - done -fi +# Deliberately NO DEVELOPER_DIR here: cargo (rust ld) must stay on the system default — +# a newer-than-OS Xcode's ld emits dylibs the running dyld rejects ("mis-aligned +# LINKEDIT string pool"), breaking every proc-macro build. Steps that need a full Xcode +# (xcodebuild) resolve it themselves (build-xcframework.sh, release.yml). PLIST_STAGE="$RUNNER_HOME/io.gitea.act_runner.plist" PLIST_SYSTEM="/Library/LaunchDaemons/io.gitea.act_runner.plist" @@ -128,7 +118,6 @@ cat > "$PLIST_STAGE" <PATH $HOME/.cargo/bin:$BIN_DIR:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin HOME$HOME - $DEVELOPER_DIR_XML RunAtLoad KeepAlive @@ -150,7 +139,6 @@ else echo "==> no sudo: starting an interim daemon (dies on reboot)" (cd "$RUNNER_HOME" && \ PATH="$HOME/.cargo/bin:$BIN_DIR:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \ - ${DEV_DIR:+DEVELOPER_DIR="$DEV_DIR"} \ nohup "$BIN_DIR/act_runner" daemon --config config.yaml >> runner.log 2>&1 &) fi echo "==> for the permanent (reboot-safe) runner, run once on the Mac:" @@ -161,9 +149,8 @@ fi sleep 2 tail -5 "$RUNNER_HOME/runner.log" 2>/dev/null || true -if ! /usr/bin/xcodebuild -version >/dev/null 2>&1 && [ -z "$DEVELOPER_DIR_XML" ]; then - echo "WARNING: xcodebuild not usable (Command Line Tools only, no full Xcode found) —" - echo " apple.yml's xcframework step needs a full Xcode in /Applications, with" - echo " its license accepted once: sudo xcodebuild -license accept" +if ! /usr/bin/xcodebuild -version >/dev/null 2>&1 && ! ls -d /Applications/Xcode*.app >/dev/null 2>&1; then + echo "WARNING: no full Xcode found — the xcframework/release steps need one in" + echo " /Applications, with its license accepted once: sudo xcodebuild -license accept" fi echo "OK: runner '$RUNNER_NAME' labels=$LABELS instance=$INSTANCE"