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
5c2ec4b
Compare
|
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. |
`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.