You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When this dev machine moves to macOS 27 and we build apfel on the newer Xcode/SDK, and especially when we start adopting OS-27-only FoundationModels APIs (e.g. the server-side LanguageModelSession / LanguageModelExecutor provider protocol that anthropics/ClaudeForFoundationModels conforms to, explored on the experiment/anthropic-messages-api branch), we must not drop macOS 26 (Tahoe) support. This issue records the researched best-practice strategy so the move is deliberate, not accidental. No code change yet - this is the plan.
TL;DR (the strategy)
Ship one universal binary whose deployment floor stays at macOS 26, built on the newest SDK, with every OS-27-only API guarded by @available(macOS 27, *) + if #available(macOS 27, *) { new } else { honest macOS-26 path }. FoundationModels exists on both 26 and 27 (only the APIs differ), so this is a runtime-availability problem (#available), not a module-existence problem (canImport). Weak linking resolves the OS-27 symbols to null on macOS 26; the #available guard guarantees we never touch them there.
Why this is the right approach (and what to avoid)
One binary, not two. Every surveyed Swift project (Sparkle, Ice, Rectangle, Stats, Kingfisher, supabase-swift) ships a single artifact targeting the oldest supported macOS and branches at runtime. Two binaries = distribution drift; avoid.
Build on newest SDK, deploy to oldest target.Package.swiftplatforms: is the deployment floor, not the SDK. Build with the macOS 27 Xcode/SDK (only it has the OS-27 declarations) while the floor stays at macOS 26. Weak linking handles the rest. (Apple: Frameworks and Weak Linking)
platforms: is a single floor, NOT a list. Do not write platforms: [.macOS(.v26), .macOS(.v27)] - that is not how it works. Keep exactly platforms: [.macOS(.v26)] and let the floor never drift up when the build SDK changes.
Use if #available, not #if canImport(FoundationModels) for the 26-vs-27 split. The framework imports on both OSes; only specific APIs are new. canImport is only for a platform that lacks the framework entirely (e.g. a future Linux ApfelCore, where it already pairs with the existing import guards).
Do NOT use @backDeployed to get Apple's OS-27 types onto macOS 26 - SE-0376 only back-deploys your own function bodies, never new Apple types/protocols (their implementation lives in the OS). (SE-0376)
@available is a promise to the compiler, not a runtime guard. The actual OS check must be an if #available on the call path, or it crashes on macOS 26.
Sources/TokenCounter.swift already uses the exact pattern: if #available(macOS 26.4, *) { real token count } else { chars/4 fallback } - extend the same way for macOS 27.
/health already reports runtime model state via SystemLanguageModel.default.availability mapped to ModelAvailability (.available / .appleIntelligenceNotEnabled / .deviceNotEligible / .modelNotReady) - this capability check is orthogonal to the OS-version gate and stays as-is.
Homebrew depends_on macos: :tahoe means ">= macOS 26", so it already admits macOS 27; one notarized tarball already serves both. No distribution change needed.
Action checklist (when we adopt OS-27 APIs)
Keep the floor pinned. Leave Package.swift at platforms: [.macOS(.v26)]. Add a make preflight assertion that the built binary's min-OS load command is still 26.0 (e.g. vtool -show / otool -l | grep -A4 LC_BUILD_VERSION) so a release built on the macOS 27 SDK can never silently ship a 27 floor.
Gate every OS-27 API. Annotate new types/functions @available(macOS 27, *); guard call sites with if #available(macOS 27, *) { ... } else { ...honest macOS-26 path... }. The else aligns with apfel's "honest about limitations" principle (degrade or honest-501, never crash).
Isolate the OS-27 surface behind a protocol + factory (Kingfisher DisplayLinkCompatible / supabase-swift _Clock pattern): an always-available protocol, an @available(macOS 27, *) conforming type, a macOS-26 fallback type, and one if #available factory. Keep ApfelCore and the CLI version-blind - no @available sprinkled across the codebase. (Note: gate the conforming type, never the conformance - conformances cannot be made conditional on OS.)
CI matrix for N and N-1. Adopt Sparkle's include: matrix shape (pair each runs-on: macos-NN image with a pinned DEVELOPER_DIR/Xcode), adding macos-27 alongside macos-26 once the GitHub-hosted image exists. Buys build + unit-test coverage on both; model-dependent integration tests remain local-only (hosted runners have no Apple Intelligence) - unchanged from today.
Document the OS support policy. A short README/SUPPORTED.md line: "apfel supports macOS 26+; OS-27-only features are additive and runtime-gated" (cf. Sparkle/Rectangle README policies). If we ever raise the floor, keep a pinned older release for macOS 26 users.
Real-hardware verification (the only way to prove it): run the branch-selection on a macOS 26 machine (legacy path runs, OS-27 path never entered, no crash) AND a macOS 27 machine (new path selected and works). GitHub CI cannot exercise this.
Filed from research on the experiment/anthropic-messages-api work, where the macOS-27 blocker first surfaced: the ClaudeForFoundationModels library requires OS 27 and cannot build on macOS 26, so apfel's /v1/messages wire support is verified at the protocol level today and the full library round-trip waits on macOS 27. No code change in this issue - planning only.
Problem
When this dev machine moves to macOS 27 and we build apfel on the newer Xcode/SDK, and especially when we start adopting OS-27-only FoundationModels APIs (e.g. the server-side
LanguageModelSession/LanguageModelExecutorprovider protocol thatanthropics/ClaudeForFoundationModelsconforms to, explored on theexperiment/anthropic-messages-apibranch), we must not drop macOS 26 (Tahoe) support. This issue records the researched best-practice strategy so the move is deliberate, not accidental. No code change yet - this is the plan.TL;DR (the strategy)
Ship one universal binary whose deployment floor stays at macOS 26, built on the newest SDK, with every OS-27-only API guarded by
@available(macOS 27, *)+if #available(macOS 27, *) { new } else { honest macOS-26 path }. FoundationModels exists on both 26 and 27 (only the APIs differ), so this is a runtime-availability problem (#available), not a module-existence problem (canImport). Weak linking resolves the OS-27 symbols to null on macOS 26; the#availableguard guarantees we never touch them there.Why this is the right approach (and what to avoid)
Package.swiftplatforms:is the deployment floor, not the SDK. Build with the macOS 27 Xcode/SDK (only it has the OS-27 declarations) while the floor stays at macOS 26. Weak linking handles the rest. (Apple: Frameworks and Weak Linking)platforms:is a single floor, NOT a list. Do not writeplatforms: [.macOS(.v26), .macOS(.v27)]- that is not how it works. Keep exactlyplatforms: [.macOS(.v26)]and let the floor never drift up when the build SDK changes.if #available, not#if canImport(FoundationModels)for the 26-vs-27 split. The framework imports on both OSes; only specific APIs are new.canImportis only for a platform that lacks the framework entirely (e.g. a future LinuxApfelCore, where it already pairs with the existingimportguards).@backDeployedto get Apple's OS-27 types onto macOS 26 - SE-0376 only back-deploys your own function bodies, never new Apple types/protocols (their implementation lives in the OS). (SE-0376)@availableis a promise to the compiler, not a runtime guard. The actual OS check must be anif #availableon the call path, or it crashes on macOS 26.apfel is already ~80% set up for this
Package.swift:7already pinsplatforms: [.macOS(.v26)](single floor - correct).Sources/TokenCounter.swiftalready uses the exact pattern:if #available(macOS 26.4, *) { real token count } else { chars/4 fallback }- extend the same way formacOS 27./healthalready reports runtime model state viaSystemLanguageModel.default.availabilitymapped toModelAvailability(.available/.appleIntelligenceNotEnabled/.deviceNotEligible/.modelNotReady) - this capability check is orthogonal to the OS-version gate and stays as-is.depends_on macos: :tahoemeans ">= macOS 26", so it already admits macOS 27; one notarized tarball already serves both. No distribution change needed.Action checklist (when we adopt OS-27 APIs)
Package.swiftatplatforms: [.macOS(.v26)]. Add amake preflightassertion that the built binary's min-OS load command is still26.0(e.g.vtool -show/otool -l | grep -A4 LC_BUILD_VERSION) so a release built on the macOS 27 SDK can never silently ship a 27 floor.@available(macOS 27, *); guard call sites withif #available(macOS 27, *) { ... } else { ...honest macOS-26 path... }. Theelsealigns with apfel's "honest about limitations" principle (degrade or honest-501, never crash).DisplayLinkCompatible/ supabase-swift_Clockpattern): an always-available protocol, an@available(macOS 27, *)conforming type, a macOS-26 fallback type, and oneif #availablefactory. KeepApfelCoreand the CLI version-blind - no@availablesprinkled across the codebase. (Note: gate the conforming type, never the conformance - conformances cannot be made conditional on OS.)include:matrix shape (pair eachruns-on: macos-NNimage with a pinnedDEVELOPER_DIR/Xcode), addingmacos-27alongsidemacos-26once the GitHub-hosted image exists. Buys build + unit-test coverage on both; model-dependent integration tests remain local-only (hosted runners have no Apple Intelligence) - unchanged from today.References
@available+guard #available)Filed from research on the
experiment/anthropic-messages-apiwork, where the macOS-27 blocker first surfaced: theClaudeForFoundationModelslibrary requires OS 27 and cannot build on macOS 26, so apfel's/v1/messageswire support is verified at the protocol level today and the full library round-trip waits on macOS 27. No code change in this issue - planning only.