[Pitch] Feature Availability Conditions for Toolchain Libraries

Hi all,

I'd like to pitch a mechanism for toolchain-bundled libraries (Swift Testing, Foundation, etc.) to advertise feature availability that downstream code can query at compile time — something analogous to hasFeature() (SE-0362), but for library-level capabilities rather than language features.

Background

This came out of a conversation in the Swift Open Source Slack with @grynspan about Swift Testing's exit tests. When I tried to adopt exit tests, I asked whether there was a hasFeature(ExitTests)-style check or any other way to detect platform support at compile time. Jonathan confirmed there is no such mechanism today — SWT_NO_EXIT_TESTS is an internal define, and hasFeature() is a compiler-side feature that would require changes across multiple projects (the compiler, SwiftPM, swift-build, and Apple's side) to support library-level capabilities. He suggested it might be worth pitching the idea here on the forums.

Motivation

Swift Testing introduced exit tests in Swift 6.2 (ST-0008). Exit tests rely on process spawning, so they are only available on macOS, Linux, FreeBSD, OpenBSD, and Windows. Platforms that cannot spawn child processes — iOS, watchOS, tvOS, visionOS, WASI, Android, and Embedded Swift — do not support them.

Internally, swift-testing handles this through a build-time define:

// In swift-testing's Package.swift
.define("SWT_NO_EXIT_TESTS", .whenEmbedded(or: .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android]))),

This flag gates the entire exit test implementation behind #if !SWT_NO_EXIT_TESTS and marks the public API @available(*, unavailable) on excluded platforms.

The problem is that downstream consumers have no way to query this. Consider a package that runs its test suite across multiple platforms:

// In a downstream package's test file
#if compiler(>=6.2)
@Test
func testExitBehavior() async {
    await #expect(processExitsWith: .failure) {
        fatalError("expected failure")
    }
}
#endif

This compiles on macOS but fails on iOS, watchOS, and the other unsupported platforms — there is no #if condition to detect whether exit tests are available. The developer is left with two options, neither satisfactory:

Option A: Duplicate the platform list.

#if compiler(>=6.2) && (os(macOS) || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Windows))
@Test
func testExitBehavior() async {
    await #expect(processExitsWith: .failure) {
        fatalError("expected failure")
    }
}
#endif

This works today but is fragile. When swift-testing adds Android support (which is already in progress), every downstream consumer must independently discover the change and update their conditions. The platform list is an implementation detail of swift-testing, and downstream code should not need to track it.

Option B: Replicate the internal define in your own Package.swift.

// In the downstream package's Package.swift
.define("MY_NO_EXIT_TESTS", .when(platforms: [.iOS, .watchOS, .tvOS, .visionOS, .wasi, .android])),

This is marginally better but still duplicates the same logic and drifts out of sync for the same reasons.

Exit tests are the immediate motivation, but this is not unique to them. Any time a toolchain library offers a capability that varies by platform or configuration, downstream code faces the same problem. The library author knows exactly which platforms support the feature, but there is no way to communicate that knowledge through the compilation-condition system.

Possible Directions

I don't have a concrete proposal yet — I'm raising this to see if others share the pain and to explore what shape a solution might take. Here are a few directions I've been thinking about.

Direction A: Extend canImport with a feature parameter

One natural spelling would build on the existing canImport condition:

#if canImport(Testing, _feature: ExitTests)
@Test
func testExitBehavior() async {
    await #expect(processExitsWith: .failure) {
        fatalError("expected failure")
    }
}
#endif

This would check whether the Testing module is available and whether it advertises support for the ExitTests capability on the current target platform. On the library side, something in the build configuration — perhaps a new SwiftPM SwiftSetting API — would let the library declare which features are available under which conditions.

Direction B: A standalone hasLibraryFeature condition

#if hasLibraryFeature(Testing.ExitTests)

This is more explicit about what it does, though it introduces a new top-level condition rather than extending an existing one.

Direction C: Something else entirely

Maybe the right answer is not a compilation condition at all. Maybe libraries should be able to export compile-time constants that #if can evaluate, or maybe there is a way to make @available work for this. I'm genuinely not sure, which is why I'm posting this as a pitch rather than a proposal.

What I think would need to happen

Regardless of the spelling, the rough shape seems to involve:

  1. A way for libraries to declare named features along with the conditions under which they are available (platforms, configurations, etc.).
  2. A way for downstream code to query those features at compile time through #if.
  3. Backward compatibility: on older compilers that don't understand the new condition, it should degrade gracefully — ideally evaluating to false in unevaluated branches, following the precedent of hasFeature() (SE-0362).

This likely touches the compiler, SwiftPM (or swift-build), and possibly the module format — so it's not a small change. But the alternative is every downstream consumer of every platform-conditional library feature independently maintaining their own copy of the platform list, which does not scale.

Why not the existing alternatives?

A few things I considered and why they fall short:

  • hasFeature() (SE-0362) is scoped to language features — upcoming and experimental compiler features. Using it for library capabilities would blur that distinction.
  • @available is designed for API availability relative to OS deployment targets. Exit tests are not "available since iOS 18" — they are structurally unsupported on iOS regardless of version.
  • Exposing a runtime boolean (e.g., @_spi(ExitTests) public let exitTestsAvailable: Bool) does not help with conditional compilation. The macros simply don't exist on unsupported platforms — the code fails to compile, not at runtime.

Related discussions

Questions for the community

I'd appreciate thoughts on:

  1. Is this a problem worth solving at the compilation-condition level? Or is there a simpler approach I've overlooked?
  2. Which direction (if any) feels right? Extending canImport, a new standalone condition, or something different?
  3. Scope: I'm thinking about toolchain-bundled libraries (Swift Testing, Foundation, Observation, etc.) specifically, since they ship with the toolchain and their features are known at compile time. Should this also cover arbitrary SwiftPM packages, or is that better left as future work?

Thanks for reading. I'd like to hear whether others have run into this and how you've dealt with it.

6 Likes

Hi @kyle,

Let me start by saying that this is a problem worth solving.

Personally, I'm considering whether we could get more value out of the platforms property that we have in Package.swift. Maybe these can transitively check in dependencies whether a specific feature is available? Note, I expect that this requires features that are not available on all platforms, to be added to sub packages.

In the mean time, maybe a reasonable workaround is to use the upcoming Test Issue Severity feature to allow these tests to compile on a platform where Exit Tests are not supported. They would then instead of failing silently, issue a warning.

2 Likes

This reminded me of the Custom Availability Domain pitch, so one way this could roughly look using that feature would be:

// Somewhere in Swift Testing
@availabilityDomain(ExitTests)
@const
#if os(macOS) || os(Linux) || os(FreeBSD) || os(OpenBSD) || os(Windows) // note that this list is now maintained in one place, the original package
var exitTestsAvailable = true
#else
var exitTestsAvailable = false
#endif

// ...

@freestanding(expression)
@available(ExitTests)
public macro expect(
  processExitsWith: ExitTest.Condition,
  performing: @escaping @Sendable () async throws -> Void
) -> ExitTest.Result?

then in the downstream consumers the usage simply becomes like this

// In a downstream package's test file
#if compiler(>=6.2)
@Test
@available(ExitTests)
func testExitBehavior() async {
    await #expect(processExitsWith: .failure) {
        fatalError("expected failure")
    }
}
#endif

you can also use if #available(ExitTests) { ... } anywhere you like and the compiler should be able to constant fold the related branch at compile-time.


I'm not sure what happened to the original pitch, but this additional use case might be able to revive the disscusion around that feature

1 Like

Yeah. @availabilityDomain seems to be another way to solve this issue. But that pitch is also still not landed yet.