Uh oh!
There was an error while loading. Please reload this page.
swift: gate the listener API so clients are not forced to iOS 18 / macOS 15 - #60
Conversation
…cOS 15 `Listener.state()` and `IncomingConnection.state()` return `any AsyncSequence<ListenerState, Never>`. 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.
6df621a to
5c2ec4bCompareprakashrj
commented
Aug 30, 2026
Ran this at runtime rather than only compiling it, since a framework can compile clean, stamp a lower floor, and still be refused at load — so I wanted to see dyld actually accept it. Built
So the lowered floor is real at load time, not just a declared number. Two caveats, so the table isn't read as more than it is:
One aside that may be useful if you act on the deployment-target note in the description: on macOS the Go archive floor has to move with the Swift one. Lowering only Happy to re-run any of this, or to test a specific configuration, if it would help the review. |
Uh oh!
There was an error while loading. Please reload this page.
`MACOS_TARGET := 15.0` is a simply-expanded assignment, so an environment variable does not override it. `MACOS_TARGET=14.0 make c-archive` builds 15.0 and reports success — you set the floor, make agrees, and you get the old one. Only `make MACOS_TARGET=14.0 c-archive` works. Measured with `make -n` against this Makefile, GOOS=darwin: before after env override 15.0 14.0 command-line 14.0 14.0 default 15.0 15.0 `?=` fixes the environment case and changes nothing else: a command-line override still wins, and the default is untouched for anyone not setting it. Found while lowering the macOS floor of a TailscaleKit.xcframework built from this repo. The failure is quiet in a way that matters here — nothing warns, the build succeeds, and the resulting binary is stamped with a floor its Go objects do not actually support. On macOS the only signal is an `ld: warning: object file ... was built for newer 'macOS' version`, which is easy to lose in build output. This is the same knob #60's "Aside" points at: if the project lowers its own deployment targets, whoever does it is likely to reach for the environment variable first. Not touched here: the comment above the line still says the wrapper requires macOS 15.0 features. That is a separate question and depends on #60, which measures macOS 14.0 as buildable once the listener API is gated.
Listener.state()andIncomingConnection.state()returnany AsyncSequence<ListenerState, Never>. That parameterised existential needs theFailureassociated 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
Xcode 26.1.1, by building at each floor rather than inferring:
ProxyConfigurationinURLSession+Tailscale.swiftProxyConfigurationfailsSo this moves the floor down a full major version on both platforms, and what remains is a different, far more central API.
Not a removal
The API is unchanged and still shipped: 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.shalready builds it-mios-version-min=12.0.Aside, not touched here
TailscaleKit.xcodeproj's own settings are higher than either number:IPHONEOS_DEPLOYMENT_TARGET = 18.1, andMACOSX_DEPLOYMENT_TARGET = 15.0in six places against15.6in two. Those look incidental rather than chosen. This PR leaves them alone, but lowering them would let the project ship the floor it can actually support.Context
Found while shipping an iOS/macOS app that embeds tsnet in-process via TailscaleKit. It is a pure client — zero references to
Listener,IncomingConnectionoraccept— and was nonetheless paying an iOS 18.1 / macOS 15.6 floor, which meant declaring support for OS versions the binary would refuse to load on.