From 923a939a461a945eb603688ed305686c57e0bad2 Mon Sep 17 00:00:00 2001 From: wanghao20 Date: Tue, 30 Jun 2026 14:44:51 +0800 Subject: [PATCH 1/2] fix(mac): target macOS 14 so the menubar app runs on Sonoma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The packaged app set Package.swift to .macOS(.v15), producing a binary with minos 15.0 that LaunchServices refuses on macOS 14.x with kLSIncompatibleSystemVersionErr (-10825) — even though Info.plist, the CLI install guard (MIN_MACOS_MAJOR=14), and the README all advertise macOS 14+. The .v15 bump was attributed to NSAttributedString(attachment:), which is actually AppKit since macOS 10.0, so the floor must not exclude Sonoma. Separately, a stock macOS-15-SDK CI build hard-links libswift_errno.dylib (macOS 15 only), which dyld cannot resolve on Sonoma even once minos passes. Building against the macOS 14 SDK avoids that dependency. - Package.swift: .macOS(.v15) -> .macOS(.v14) - Scripts/build-local.sh: build on a Sonoma machine (which only has the macOS 14 SDK, lacking the SwiftUI @MainActor inference the macOS 15 SDK adds to the View protocol) using a standalone swift.org Swift 6.x toolchain, patching @MainActor onto views in a scratch copy so repo sources stay clean. - README: document the Sonoma local-build path. Co-Authored-By: Claude Opus 4.8 (1M context) --- mac/Package.swift | 6 +- mac/README.md | 22 ++++++- mac/Scripts/build-local.sh | 122 +++++++++++++++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) create mode 100755 mac/Scripts/build-local.sh diff --git a/mac/Package.swift b/mac/Package.swift index 51898da4..9c6ba17a 100644 --- a/mac/Package.swift +++ b/mac/Package.swift @@ -4,7 +4,11 @@ import PackageDescription let package = Package( name: "CodeBurnMenubar", platforms: [ - .macOS(.v15) + // macOS 14 (Sonoma) is the floor: matches Info.plist LSMinimumSystemVersion, + // the CLI install guard (MIN_MACOS_MAJOR=14), and mac/README. The earlier .v15 + // bump for NSAttributedString(attachment:) was a misdiagnosis — that initializer + // is AppKit since macOS 10.0, so the binary's minos must not exclude Sonoma users. + .macOS(.v14) ], products: [ .executable(name: "CodeBurnMenubar", targets: ["CodeBurnMenubar"]) diff --git a/mac/README.md b/mac/README.md index b12b836d..6b217c14 100644 --- a/mac/README.md +++ b/mac/README.md @@ -30,6 +30,23 @@ swift build -c release .build/release/CodeBurnMenubar # launch ``` +#### On macOS 14 (Sonoma) without Xcode 16 + +`swift build` above assumes the macOS 15 SDK, whose SwiftUI marks the `View` +protocol `@MainActor`. The Sonoma SDK (shipped with Command Line Tools) lacks +that annotation, so a plain build fails with ~80 `main actor-isolated ... from a +nonisolated context` errors, and a stock CI build links the macOS-15-only +`libswift_errno.dylib` (the root of the `-10825` launch failure on Sonoma). + +Use the helper, which builds against the local macOS 14 SDK with a standalone +[swift.org](https://www.swift.org/install/macos/) Swift 6.x toolchain and +adds explicit `@MainActor` to the views in a scratch copy (repo sources stay +clean), producing a `minos = 14.0` bundle installed to `~/Applications`: + +```bash +mac/Scripts/build-local.sh # then: codeburn menubar +``` + ## Build & run (dev against a local CLI checkout) ```bash @@ -52,7 +69,10 @@ Release installs record a persistent absolute CLI path in `~/Library/Application ``` mac/ -├── Package.swift SwiftPM manifest +├── Package.swift SwiftPM manifest (deployment target: macOS 14) +├── Scripts/ +│ ├── package-app.sh CI: universal signed .app + zip + checksum +│ └── build-local.sh Local macOS 14 build (Sonoma SDK + @MainActor patch) ├── Sources/CodeBurnMenubar/ │ ├── CodeBurnApp.swift @main + MenuBarExtra scene │ ├── AppStore.swift @Observable store + enums diff --git a/mac/Scripts/build-local.sh b/mac/Scripts/build-local.sh new file mode 100755 index 00000000..228d0e80 --- /dev/null +++ b/mac/Scripts/build-local.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# ============================================================================ +# build-local.sh — Build CodeBurnMenubar.app on a macOS 14 (Sonoma) machine. +# ============================================================================ +# Why this exists +# --------------- +# The released .app is built in CI with the macOS 15 SDK + Swift 6. That binary +# hard-links /usr/lib/swift/libswift_errno.dylib, which only ships in macOS 15, +# so `codeburn menubar` fails on Sonoma with: +# kLSIncompatibleSystemVersionErr (-10825) +# +# This script builds an arm64 bundle locally against the machine's macOS 14 SDK +# (no libswift_errno dependency, minos = 14.0) using a swift.org Swift 6.2 +# toolchain. Because the macOS 14 SDK's SwiftUI does NOT carry the @MainActor +# annotations that the macOS 15 SDK added to the `View` protocol, the sources +# are copied to a scratch dir and every `View`/`App` struct is given an explicit +# `@MainActor` there — the repo sources stay untouched. +# +# Prerequisites +# - Command Line Tools (provides the macOS 14 SDK + sips/iconutil/codesign) +# - A swift.org Swift 6.x toolchain in ~/Library/Developer/Toolchains/ +# download: https://www.swift.org/install/macos/ (Swift 6.2 recommended) +# +# Usage: mac/Scripts/build-local.sh [] (defaults to "dev") +# ---------------------------------------------------------------------------- +set -euo pipefail + +VERSION="${1:-dev}" +BUNDLE_ID="org.agentseal.codeburn-menubar" +EXE="CodeBurnMenubar" +MIN_MACOS="14.0" + +ROOT=$(cd "$(dirname "$0")/../.." && pwd) +MAC_DIR="${ROOT}/mac" +ICON_SOURCE="${ROOT}/assets/menubar-logo.png" +SCRATCH="$(mktemp -d /tmp/codeburn-local-build.XXXXXX)" +APPS="${HOME}/Applications" +BUNDLE="${APPS}/${EXE}.app" + +trap 'rm -rf "${SCRATCH}"' EXIT + +# --- locate a Swift 6.x toolchain ------------------------------------------- +TC="" +for cand in "${HOME}/Library/Developer/Toolchains/swift-6.2-RELEASE.xctoolchain" \ + "${HOME}/Library/Developer/Toolchains/swift-latest.xctoolchain" \ + /Library/Developer/Toolchains/swift-latest.xctoolchain; do + [[ -x "${cand}/usr/bin/swift" ]] && { TC="${cand}"; break; } +done +if [[ -z "${TC}" ]]; then + echo "✗ No swift.org Swift 6.x toolchain found in ~/Library/Developer/Toolchains/." >&2 + echo " Install one from https://www.swift.org/install/macos/ (Swift 6.2)." >&2 + exit 1 +fi +SWIFT="${TC}/usr/bin/swift" +export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)" +echo "▸ Toolchain : $("${SWIFT}" --version | head -1)" +echo "▸ SDK : ${SDKROOT} ($(xcrun --sdk macosx --show-sdk-version))" + +# --- copy sources and add explicit @MainActor to SwiftUI views -------------- +echo "▸ Staging sources in ${SCRATCH}..." +# Tests/ is copied only so the manifest's testTarget path resolves; `swift build` +# (product only) never compiles it, so it needs no @MainActor patching. +cp -R "${MAC_DIR}/Sources" "${MAC_DIR}/Tests" "${MAC_DIR}/Package.swift" "${SCRATCH}/" +find "${SCRATCH}/Sources" -name "*.swift" -print0 | while IFS= read -r -d '' f; do + perl -i -pe 's/^((?:private |public |fileprivate )?struct \b.*: .*\bView\b.*\{)/\@MainActor\n$1/; s/^(struct \w+ *: *App\b.*\{)/\@MainActor\n$1/' "$f" + perl -0777 -i -pe 's/\@MainActor\n\@MainActor\n/\@MainActor\n/g' "$f" +done + +# --- build arm64 release (single-arch avoids xcbuild, absent from CLT) ------- +echo "▸ Building arm64 release..." +( cd "${SCRATCH}" && "${SWIFT}" build -c release ) +BIN="$(cd "${SCRATCH}" && "${SWIFT}" build -c release --show-bin-path)/${EXE}" +[[ -x "${BIN}" ]] || { echo "✗ build produced no binary" >&2; exit 1; } + +# --- assemble the .app bundle ------------------------------------------------ +echo "▸ Assembling ${BUNDLE}..." +pkill -f "${EXE}" 2>/dev/null || true; sleep 1 +rm -rf "${BUNDLE}" +mkdir -p "${BUNDLE}/Contents/MacOS" "${BUNDLE}/Contents/Resources" +cp "${BIN}" "${BUNDLE}/Contents/MacOS/${EXE}" +cp "${ICON_SOURCE}" "${BUNDLE}/Contents/Resources/menubar-logo.png" + +ICONSET="${SCRATCH}/AppIcon.iconset"; mkdir -p "${ICONSET}" +for spec in "16:16x16" "32:16x16@2x" "32:32x32" "64:32x32@2x" "128:128x128" \ + "256:128x128@2x" "256:256x256" "512:256x256@2x" "512:512x512"; do + sips -z "${spec%%:*}" "${spec%%:*}" "${ICON_SOURCE}" --out "${ICONSET}/icon_${spec##*:}.png" >/dev/null +done +cp "${ICON_SOURCE}" "${ICONSET}/icon_512x512@2x.png" +iconutil -c icns "${ICONSET}" -o "${BUNDLE}/Contents/Resources/AppIcon.icns" + +cat > "${BUNDLE}/Contents/Info.plist" < + + + + CFBundleDevelopmentRegionen + CFBundleDisplayNameCodeBurn Menubar + CFBundleExecutable${EXE} + CFBundleIconFileAppIcon + CFBundleIdentifier${BUNDLE_ID} + CFBundleInfoDictionaryVersion6.0 + CFBundleName${EXE} + CFBundlePackageTypeAPPL + CFBundleShortVersionString${VERSION} + CFBundleVersion${VERSION} + LSMinimumSystemVersion${MIN_MACOS} + LSUIElement + NSHighResolutionCapable + NSHumanReadableCopyright© AgentSeal + + +PLIST +printf 'APPL????' > "${BUNDLE}/Contents/PkgInfo" + +echo "▸ Ad-hoc signing..." +codesign --force --sign - --timestamp=none --deep "${BUNDLE}" +codesign --verify --deep --strict "${BUNDLE}" + +echo "" +echo "✓ Installed ${BUNDLE}" +vtool -show-build "${BUNDLE}/Contents/MacOS/${EXE}" 2>/dev/null | grep -iE "minos|sdk" | sed 's/^/ /' +echo " Launch with: codeburn menubar (or: open '${BUNDLE}')" From afe582e44268a72de34a52c5629d040b81da3e58 Mon Sep 17 00:00:00 2001 From: wanghao20 Date: Thu, 2 Jul 2026 00:36:31 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(mac):=20address=20review=20notes=20on?= =?UTF-8?q?=20PR=20#577=20=E2=80=94=20correct=20SDK/deployment-target=20wo?= =?UTF-8?q?rding,=20universal=20build-local.sh,=20CI=20minos=20guard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - README.md / build-local.sh: the -10825 fix is the Package.swift deployment target, not the SDK used to build (ld64's $ld$previous drops libswift_errno.dylib based on minos, so it already applies to the CI-distributed release too). build-local.sh exists only for building on a Sonoma machine with just the Command Line Tools. - build-local.sh: build arm64 and x86_64 separately and lipo them into a universal binary (`--arch arm64 --arch x86_64` together needs xcbuild, which CLT doesn't ship); assert the active SDK is actually 14.x instead of silently trusting `xcrun`; use `pkill -x` instead of `-f` to avoid matching unrelated processes; broaden the @MainActor patch regex to slurp mode so it also covers multi-line struct headers and `extension X: View`. - package-app.sh: fail the build if the packaged binary's minos isn't 14.0 for every arch slice, or if it links libswift_errno.dylib, so a future deployment-target regression is caught in CI instead of a user's crash report. Co-Authored-By: Claude Sonnet 5 --- mac/README.md | 11 +++++-- mac/Scripts/build-local.sh | 66 +++++++++++++++++++++++++++----------- mac/Scripts/package-app.sh | 14 ++++++++ 3 files changed, 70 insertions(+), 21 deletions(-) diff --git a/mac/README.md b/mac/README.md index 6b217c14..ce614ab7 100644 --- a/mac/README.md +++ b/mac/README.md @@ -35,8 +35,15 @@ swift build -c release `swift build` above assumes the macOS 15 SDK, whose SwiftUI marks the `View` protocol `@MainActor`. The Sonoma SDK (shipped with Command Line Tools) lacks that annotation, so a plain build fails with ~80 `main actor-isolated ... from a -nonisolated context` errors, and a stock CI build links the macOS-15-only -`libswift_errno.dylib` (the root of the `-10825` launch failure on Sonoma). +nonisolated context` errors. + +(The `-10825` launch failure itself is fixed by `Package.swift`'s `.macOS(.v14)` +deployment target: ld64 drops the macOS-15-only `libswift_errno.dylib` +dependency for any build with that target, regardless of which SDK built it — +including the CI-distributed release. This local-build path exists only for +the narrower case of building on a Sonoma machine with nothing but the +Command Line Tools, where the SDK's un-annotated `View` protocol needs the +`@MainActor` patch below.) Use the helper, which builds against the local macOS 14 SDK with a standalone [swift.org](https://www.swift.org/install/macos/) Swift 6.x toolchain and diff --git a/mac/Scripts/build-local.sh b/mac/Scripts/build-local.sh index 228d0e80..7498c902 100755 --- a/mac/Scripts/build-local.sh +++ b/mac/Scripts/build-local.sh @@ -4,17 +4,19 @@ # ============================================================================ # Why this exists # --------------- -# The released .app is built in CI with the macOS 15 SDK + Swift 6. That binary -# hard-links /usr/lib/swift/libswift_errno.dylib, which only ships in macOS 15, -# so `codeburn menubar` fails on Sonoma with: -# kLSIncompatibleSystemVersionErr (-10825) +# Package.swift's `.macOS(.v14)` deployment target already fixes the -10825 +# launch failure for every build, including the CI-distributed release: ld64 +# drops the macOS-15-only libswift_errno.dylib dependency based on the +# deployment target, not the SDK used to build. This script is not about that. # -# This script builds an arm64 bundle locally against the machine's macOS 14 SDK -# (no libswift_errno dependency, minos = 14.0) using a swift.org Swift 6.2 -# toolchain. Because the macOS 14 SDK's SwiftUI does NOT carry the @MainActor -# annotations that the macOS 15 SDK added to the `View` protocol, the sources -# are copied to a scratch dir and every `View`/`App` struct is given an explicit -# `@MainActor` there — the repo sources stay untouched. +# It exists for the narrower case of building on a Sonoma machine that only +# has the Command Line Tools (macOS 14 SDK). That SDK's SwiftUI does NOT carry +# the @MainActor annotations the macOS 15 SDK added to the `View` protocol, so +# a plain `swift build` there fails with ~80 `main actor-isolated ... from a +# nonisolated context` errors. This script copies the sources to a scratch +# dir, gives every `View`/`App` conformance an explicit `@MainActor` there +# (repo sources stay untouched), and builds a universal bundle with a +# swift.org Swift 6.2 toolchain against the local macOS 14 SDK. # # Prerequisites # - Command Line Tools (provides the macOS 14 SDK + sips/iconutil/codesign) @@ -53,8 +55,18 @@ if [[ -z "${TC}" ]]; then fi SWIFT="${TC}/usr/bin/swift" export SDKROOT="$(xcrun --sdk macosx --show-sdk-path)" +SDK_VERSION="$(xcrun --sdk macosx --show-sdk-version)" +case "${SDK_VERSION}" in + 14.*) ;; + *) + echo "✗ Active SDK is macOS ${SDK_VERSION}, not 14.x — xcode-select is likely" >&2 + echo " pointed at a newer Xcode instead of the Command Line Tools. Run:" >&2 + echo " sudo xcode-select -s /Library/Developer/CommandLineTools" >&2 + exit 1 + ;; +esac echo "▸ Toolchain : $("${SWIFT}" --version | head -1)" -echo "▸ SDK : ${SDKROOT} ($(xcrun --sdk macosx --show-sdk-version))" +echo "▸ SDK : ${SDKROOT} (${SDK_VERSION})" # --- copy sources and add explicit @MainActor to SwiftUI views -------------- echo "▸ Staging sources in ${SCRATCH}..." @@ -62,19 +74,34 @@ echo "▸ Staging sources in ${SCRATCH}..." # (product only) never compiles it, so it needs no @MainActor patching. cp -R "${MAC_DIR}/Sources" "${MAC_DIR}/Tests" "${MAC_DIR}/Package.swift" "${SCRATCH}/" find "${SCRATCH}/Sources" -name "*.swift" -print0 | while IFS= read -r -d '' f; do - perl -i -pe 's/^((?:private |public |fileprivate )?struct \b.*: .*\bView\b.*\{)/\@MainActor\n$1/; s/^(struct \w+ *: *App\b.*\{)/\@MainActor\n$1/' "$f" - perl -0777 -i -pe 's/\@MainActor\n\@MainActor\n/\@MainActor\n/g' "$f" + # Slurp mode ([^{]* spans newlines) so this also catches multi-line generic + # struct headers and `extension X: View` conformances, not just the + # single-line `struct X: View {` shape the current sources happen to use. + perl -0777 -i -pe ' + s/^((?:private |public |fileprivate |internal )?(?:struct|extension)\s+\w+(?:<[^{]*?>)?\s*:[^{]*\bView\b[^{]*\{)/\@MainActor\n$1/gm; + s/^(struct\s+\w+\s*:[^{]*\bApp\b[^{]*\{)/\@MainActor\n$1/gm; + s/\@MainActor\n\@MainActor\n/\@MainActor\n/g; + ' "$f" done -# --- build arm64 release (single-arch avoids xcbuild, absent from CLT) ------- -echo "▸ Building arm64 release..." -( cd "${SCRATCH}" && "${SWIFT}" build -c release ) -BIN="$(cd "${SCRATCH}" && "${SWIFT}" build -c release --show-bin-path)/${EXE}" -[[ -x "${BIN}" ]] || { echo "✗ build produced no binary" >&2; exit 1; } +# --- build each arch separately, then lipo into one universal binary -------- +# `swift build --arch arm64 --arch x86_64` together shells out to xcbuild, +# which the Command Line Tools doesn't ship — each arch alone stays on the +# plain SwiftPM build path, so build twice and merge with lipo instead. +BINS=() +for arch in arm64 x86_64; do + echo "▸ Building ${arch} release..." + ( cd "${SCRATCH}" && "${SWIFT}" build -c release --arch "${arch}" ) + bin="$(cd "${SCRATCH}" && "${SWIFT}" build -c release --arch "${arch}" --show-bin-path)/${EXE}" + [[ -x "${bin}" ]] || { echo "✗ ${arch} build produced no binary" >&2; exit 1; } + BINS+=("${bin}") +done +BIN="${SCRATCH}/${EXE}-universal" +lipo -create -output "${BIN}" "${BINS[@]}" # --- assemble the .app bundle ------------------------------------------------ echo "▸ Assembling ${BUNDLE}..." -pkill -f "${EXE}" 2>/dev/null || true; sleep 1 +pkill -x "${EXE}" 2>/dev/null || true; sleep 1 rm -rf "${BUNDLE}" mkdir -p "${BUNDLE}/Contents/MacOS" "${BUNDLE}/Contents/Resources" cp "${BIN}" "${BUNDLE}/Contents/MacOS/${EXE}" @@ -118,5 +145,6 @@ codesign --verify --deep --strict "${BUNDLE}" echo "" echo "✓ Installed ${BUNDLE}" +lipo -info "${BUNDLE}/Contents/MacOS/${EXE}" | sed 's/^/ /' vtool -show-build "${BUNDLE}/Contents/MacOS/${EXE}" 2>/dev/null | grep -iE "minos|sdk" | sed 's/^/ /' echo " Launch with: codeburn menubar (or: open '${BUNDLE}')" diff --git a/mac/Scripts/package-app.sh b/mac/Scripts/package-app.sh index 3041eda0..bc019f8f 100755 --- a/mac/Scripts/package-app.sh +++ b/mac/Scripts/package-app.sh @@ -120,6 +120,20 @@ else fi codesign --verify --deep --strict "${BUNDLE}" +echo "▸ Verifying deployment target and libswift_errno absence..." +BUILT_EXE="${BUNDLE}/Contents/MacOS/${EXECUTABLE_NAME}" +BAD_MINOS=$(vtool -show-build "${BUILT_EXE}" 2>/dev/null | awk '/minos/{print $2}' | grep -v '^14\.0$' || true) +if [[ -n "${BAD_MINOS}" ]]; then + echo "✗ Expected minos 14.0 for every arch slice, found: ${BAD_MINOS}" >&2 + echo " Did Package.swift's platforms: [.macOS(...)] regress past .v14?" >&2 + exit 1 +fi +if otool -L "${BUILT_EXE}" | grep -q libswift_errno; then + echo "✗ ${BUILT_EXE} links libswift_errno.dylib (macOS 15+ only) — would fail on Sonoma with -10825." >&2 + exit 1 +fi +echo " minos 14.0 confirmed, no libswift_errno dependency." + ZIP_NAME="CodeBurnMenubar-${ASSET_VERSION}.zip" ZIP_PATH="${DIST_DIR}/${ZIP_NAME}" echo "▸ Packaging ${ZIP_NAME}..."