From 5c2ec4bc7035051d7e2ec3058424881d612df161 Mon Sep 17 00:00:00 2001 From: Prakash Rudraraju <1471544+prakashrj@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:52:39 -0700 Subject: [PATCH] swift: gate the listener API so clients are not forced to iOS 18 / macOS 15 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Listener.state()` and `IncomingConnection.state()` return `any AsyncSequence`. That parameterised existential needs the `Failure` associated type, which is iOS 18 / macOS 15 only — and because the requirement is unannotated, it propagates to the entire framework. Every consumer inherits an iOS 18 floor, including the many that only dial out and never accept an inbound connection. Nothing else in TailscaleKit needs it. Annotating the two listener actors confines the requirement to them. Measured on Xcode 26.1.1 by building at each floor: as-is iOS 18.0 / macOS 15.0 minimum with this change iOS 17.0 / macOS 14.0 <- ProxyConfiguration in URLSession+Tailscale at iOS 13.0 only ProxyConfiguration fails at iOS 12.0 Swift concurrency itself fails So this moves the floor down a full major version on both platforms, and the next constraint is a different, more central API. The API is not removed and not changed. It stays in the binary and in the .swiftinterface; callers on iOS 18 / macOS 15 see exactly what they see today. Callers below it now get a clear availability diagnostic on the listener types instead of an unexplained floor on the whole framework. The Go layer is not the constraint: swift/script/clangwrap-ios.sh already builds it -mios-version-min=12.0. Note that TailscaleKit.xcodeproj's own settings are higher than either number — IPHONEOS_DEPLOYMENT_TARGET 18.1, and MACOSX_DEPLOYMENT_TARGET 15.0 in six places against 15.6 in two. Those look incidental rather than chosen; this change does not touch them, but lowering them would let the project ship the floor it can actually support. --- swift/TailscaleKit/IncomingConnection.swift | 6 ++++++ swift/TailscaleKit/Listener.swift | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/swift/TailscaleKit/IncomingConnection.swift b/swift/TailscaleKit/IncomingConnection.swift index a67766a..9e16aa0 100644 --- a/swift/TailscaleKit/IncomingConnection.swift +++ b/swift/TailscaleKit/IncomingConnection.swift @@ -7,6 +7,12 @@ import Foundation /// IncomingConnection is use to read incoming message from an inbound /// connection. IncomingConnections are not instantiated directly, /// they are returned by Listener.accept +// Gated so the rest of TailscaleKit can be built for older systems. +// `state()` returns `any AsyncSequence`, whose `Failure` +// associated type is iOS 18 / macOS 15 only, and that requirement propagates to +// the whole framework even for clients that never listen for inbound +// connections. Annotating the two listener types confines it to them. +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *) public actor IncomingConnection { private let logger: LogSink? private var conn: TailscaleConnection = 0 diff --git a/swift/TailscaleKit/Listener.swift b/swift/TailscaleKit/Listener.swift index 67d89e3..2a59454 100644 --- a/swift/TailscaleKit/Listener.swift +++ b/swift/TailscaleKit/Listener.swift @@ -6,6 +6,12 @@ import Foundation /// A Listener is used to await incoming connections from another /// Tailnet node. +// Gated so the rest of TailscaleKit can be built for older systems. +// `state()` returns `any AsyncSequence`, whose `Failure` +// associated type is iOS 18 / macOS 15 only, and that requirement propagates to +// the whole framework even for clients that never listen for inbound +// connections. Annotating the two listener types confines it to them. +@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *) public actor Listener { private var tailscale: TailscaleHandle private var listener: TailscaleListener = 0