SE-0554: Deployment target conditional compilation

Hi all,

The review of SE-0554: Deployment target conditional compilation beings now and runs through October 12, 2026.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager via the forum messaging feature. When contacting the review manager directly, please keep the proposal link at the top of the message.

Trying it out

Toolchains with a prototype implementation are currently available for macOS and Windows.
(The Linux toolchain build has been stuck, but perhaps will be unstuck sometime.)

What goes into a review?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:

  • What is your evaluation of the proposal?
  • Is the problem being addressed significant enough to warrant a change to Swift?
  • Does this proposal fit well with the feel and direction of Swift?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

More information about the Swift evolution process is available here.

Thank you,

Xiaodi Wu
Review Manager

12 Likes

I have a question regarding the spelling of the deploymentTargetAtLeast() condition:

The term “deployment target” has of course long tradition on Apple platforms and it seems to be used a lot in the documentation for the SupportedPlatform API of the package description, but it’s not actually used in the spelling of the package description API itself. The proposed compilation condition uses it, though, which makes this inconsistent. For example:

// Package.swift
let package = Package(
    name: "MyExecutable",
    platforms: [.macOS(.v15)], // “platforms”
    targets: […]
)

// Code in package
#if deploymentTargetAtLeast(macOS 15, *) // “deployment target”
…
#endif

Wouldn’t it be more consistent if it were spelled platformAtLeast() so it would be consistent? Like this:

// Code in package
#if platformAtLeast(macOS 15, iOS 18, *)
…
#endif

To be clear: I’m not opposed to the spelling in the proposal since “minimum deployment target” is exactly how I (and I guess most developers for Apple platforms) think about this. So it might rather be that it’s the platform spelling in the package description that’s a bit odd here, but I guess that won’t change anytime soon (or maybe that’s something a future redesign of the package description could address?).

Forgive me if I’m missing some nuance here or if I’m mixing something up – I’m not following SPM development closely.

1 Like

+1.

This is generally a massive issue in the server-side (or generally non-Apple-platforms) space where you can freely use whatever you want just based on Swift version, until you realize you have to support Apple platforms as well which add deployment target versions to the mix.

Every type newly introduced to Swift is practically unusable in existing packages, specially the backbone ones, that can't just release major versions every other year (thus can't just bump their required Apple platforms versions which is a breaking change).
Even if they could release new major versions frequently, they'd still need to support at least 2-3 macOS versions back. This is all extremely painful to see new great types being introduced but that we can't actually use them. InlineArray, UniqueArray (at least can get it from swift-collections), ByteOrder, even Mutex (Synchronization module) /UInt128 which merely require macOS 15. Sometimes not even the Clock types since they require macOS 13.
These are not theoretical to me either. I've needed all of those types at some point.

This proposal addresses part of that problem by letting you decide what types to use based on the deployment target. This is a very appreciated step towards solving this problem.

What's the rest of the problem?

The remaining part of the problem is that Swift should also allow having different stored properties based on if #available(..., *)s, in a way as performant as possible, but I do realize that comes with its own restrictions due to ABI compatibility concerns.

For example Swift will have to have some way to resolve the full type metadata at runtime because it doesn't know about the type's full memory layout until runtime, but then I was also thinking perhaps we could require those ifs to not result in different memory layouts, or simply automatically pad the layout or such unconditionally so we have the same memory layout on runtime regardless of deployment targets on Darwin platforms.

Just heap-boxing the values is not an option IMO as it'd be too big of a perf hit for my usecases, personally.

I expect there will be some perf hits with such dynamicisms (See 35% of performance going to `#available(..., *)` (`__isPlatformVersionAtLeast`) · Issue #84787 · swiftlang/swift · GitHub for example, but note that the numbers are no longer accurate as Apple took measures to make things better).

Alternatively, perhaps Swift can instead let users opt into vending the new types in their own executables so they don't need to rely on the deployment target to actually support it.

2 Likes

Package.swift uses platforms to declare supported versions, but this condition checks the minimum deployment version of the module being compiled. That's why I used deploymentTargetAtLeast. platformAtLeast could also sound like a runtime OS check. I'm open to other spellings if there's a clearer one.

2 Likes

That is a very good argument! :+1:

1 Like

FWIW, the fact that this is spelled #if condition instead of if #condition should sufficiently indicate that it's a compile-time and not a run-time check, but I'm also sympathetic to claims that the position of a single # might be a bit subtle.

I'm not crazy about the spelling deploymentTargetAtLeast but I do feel like "deployment target" is a term that specifically nails this down as a build-time thing.

The best alternative that I could come up with right now is #if minimumPlatform(iOS 26.0, ...), or #if minimumRequiredPlatform(iOS 26.0, ...). That aligns us with the use of platforms in SwiftPM and sounds similar to the equivalent symbols in Apple's headers (e.g., __ENVIRONMENT_IPHONE_OS_VERSION_MIN_REQUIRED__).

4 Likes

Thanks, I’m open to those spellings too. I’d like to hear what others think before settling on a name.

But is not the terminology Apple-platform developers are actually familiar with, making it less discoverable and requiring a bit of reasoning every time you see it to understand what it actually means. Generally, if a name's meaning will typically be understood as just a synonym of something else, we should just use the other term if at all possible. (This was my main issue with Ref as well.)

A simple deploymentTarget seems fine.

3 Likes

If developers know this as "minimum deployment target," why not just call it minDeploymentTarget?

5 Likes

Because they're deployment targets.


Xcode also refers to them as "Minimum Deployments" in the "General" target info, but the settings have always been deployment targets. The build settings even reflect that: IPHONEOS_DEPLOYMENT_TARGET.

To me, just spelling it deploymentTarget makes it possible to misinterpret as an == comparison. The build settings and UI you're referencing don't say minimum because they are specifying an exact value rather than describing a predicate. I like Xiaodi's suggestion of minDeploymentTarget better because it makes the nature of the predicate clearer. I suppose we could take inspiration from #if swift(>=6.0) or #if compiler(>=6.4) and spell it like #if deploymentTarget(>= macOS 15, iOS 18, *), though.

2 Likes

Except that's not how deployment targets work. Plus, there's no such thing as a maxDeploymentTarget or any other relative calculation. There is just the single value (per platform).

Edit: It's like saying we need Int literals to use +1 to indicate positive numbers: that's just not how Ints work. (Cue mathematical explanation for why that's not quite right.)

1 Like

But that's how this feature would work, no? Like, if I set the "iOS Deployment Target" to 26, then #if deploymentTargetAtLeast(iOS 18, *) should evaluate to true?

...in this proposal, but one could imagine such a feature, no?

3 Likes

I suppose I can imagine it, but it's unprecedented and not how Apple's platforms have ever worked. I'm not sure it makes sense to declare a maximum supported OS version in the first place, nor how it would be useful as a compile time condition, since that's not how API availability works either.

The argument that this is how deployment targets "work" isn't convincing to me. The expressivity available when specifying a deployment target doesn't have to inherently limit what makes sense when testing for the deployment target in code. For the same reason we have if #unavailable as a counterpart to if #available, it's reasonable to want to express a < deployment target predicate:

@available(macOS, obsoleted: 26)
public func someAPI() { }

The natural way to conditionalize use of someAPI() would be something like this:

#if deploymentTarget(< macOS 26)
someAPI()
#else
// do something else on newer deployment targets
#endif
4 Likes

I agree it would be useful to be able to express this both ways {>= and <). But I don't think it's apt to use a prefix operator, for several reasons:

  • There can generally be multiple arguments to the function, so should one operator apply to all as in deploymentTarget(< iOS 18, macOS 15) or should it be repeated as in deploymentTarget(< iOS 18, < macOS 15) (but surely not mixed with >=)?
  • Either way, it looks unprecedented, especially if deploymentTarget(>= iOS 18, macOS 15, *) requires the *.

OTOH, we already have a similar attribute @backDeployed(before: iOS 18, macOS 15) with its single argument label before: applying to all platforms listed. Why not use that instead of <?

#if deploymentTarget(before: iOS 18, macOS 15)
someAPI()
#else
// ...
#endif

For the opposite, after: wouldn't quite be right (because the condition should be >= not >). But atLeast: seems like an ok choice:

#if deploymentTarget(atLeast: iOS 18, macOS 15, *)
// ...
#endif

And the string deploymentTarget atLeast should be a pretty good search term for documentation, too (as to why the * is required, for instance).


Finally a question: Will this be made to work with umbrella platforms like anyAppleOS as well?

4 Likes

Yes, anyAppleOS is already supported in the prototype.

1 Like

I'm fairly neutral to perhaps slightly negative on this proposal.

Applicability and usefulness

I agree it's useful, but I question whether it's widely useful, because today a deployment target is a fixed property of a module rather than a floating value. Sure, you could dynamically raise the value of a module's deployment target to match that of its downstream dependencies as a sort of "optimization", and I believe that's been proposed for SwiftPM before, but it's not how Apple's developer and build tools have ever worked historically, so it's not a common scenario that you can take advantage of this today.

I would find a #if sdk construct more generally applicable in today's ecosystem since any given module is often built with a wider range of different SDK versions (as opposed to only ever using a single deployment target), and the only way around this is to fall back to C.

Perfect example here: Use pipe2/dup3 O_CLOEXEC atomically where available (#373) · swiftlang/swift-subprocess@8f099c6 · GitHub - I needed both a compile time check (#if defined(__MAC_27_0) in C, or hypothetically #if sdk(...) in Swift) in the case that the code is building with an SDK older than macOS 27, and a runtime check (if (__builtin_available(...)) in C if #available in Swift) in the case that the code is building with an SDK at least macOS 27, but with a lower deployment target.

Usage examples

This section should help explain why I'm slightly negative on this proposal.

This usage example is good:

#if deploymentTargetAtLeast(macOS 15, iOS 18, *)
import Synchronization
typealias Lock<State> = Mutex<State>
#else
import os
typealias Lock<State> = OSAllocatedUnfairLock<State>
#endif

because it's about representing a case where the behavior can't vary based on the runtime OS version (a typealias).

But I'd like to see the proposal also include a second example showing how there are cases where you'd want to combine both #if deploymentTargetAtLeast and #available together, like this:

#if deploymentTargetAtLeast(macOS 15, iOS 18, *)
useNewImplementation()
#else
if #available(macOS 15, iOS 18, *) {
  useNewImplementation()
} else {
  useFallbackImplementation()
}
#endif

In the above example this is the most correct expression, because if you've compiled with a new-enough SDK, you should be calling the new implementation whenever possible.

The alternative:

#if deploymentTargetAtLeast(macOS 15, iOS 18, *)
useNewImplementation()
#else
useFallbackImplementation()
#endif

would NOT be correct from the perspective of how deployment targets are expected to be used on Apple platforms (you'd always compile the else case until you drop the older deployment target, and then you don't need the condition in the first place), and I worry developers will incorrectly apply the second form above when if #available would be the correct in this case instead.

I think it would be preferable to explore compiler features where:

if #available(macOS 15, iOS 18, *) {
  useNewImplementation()
} else {
  useFallbackImplementation()
}

could be dynamically optimized to remove the runtime check if the code is compiled with a deployment target where the check becomes a no-op. And in fact, is it not doing that already?

Naming

Also, as several folks have touched on in the thread, I do NOT think we should be changing the spelling of this construct to align with SwiftPM's platforms property.

A user might be using another build system such as CMake or Bazel instead of SwiftPM, and language constructs should be named in a way that continues to make sense in those other contexts, since build tools are independent of the language itself. "deployment target" is the right name for this, since that's what the underlying concept is called on Apple platforms and what Apple developers are familiar with.

2 Likes

The compiler does already do this.

2 Likes

Thanks for the detailed review. The lock typealias is one example of a broader need: choosing types and storage that can’t vary at runtime. Other examples would be a parser using InlineArray instead of an array-backed buffer, or a numeric library using UInt128 instead of a compatibility type. Mahdi mentioned needing these types in his review.

A module’s deployment target is fixed for each build, but shared sources can be rebuilt for different targets, as Tony described in the pitch. Those builds can use the same SDK. Having the condition read the deployment target directly would avoid maintaining a separate build flag for these choices.

For calls that should use newer APIs when running on a newer OS, I’d still use if #available. I’ll clarify that distinction in the proposal.