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:
- A way for libraries to declare named features along with the conditions under which they are available (platforms, configurations, etc.).
- A way for downstream code to query those features at compile time through
#if. - Backward compatibility: on older compilers that don't understand the new condition, it should degrade gracefully — ideally evaluating to
falsein unevaluated branches, following the precedent ofhasFeature()(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.@availableis 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
- SE-0362: Piecemeal adoption of upcoming language improvements —
hasFeature()for language-level feature flags. - Pitch: Conditional compilation for attributes and modifiers —
hasAttribute(), addressing a similar gap for compiler attributes. - Pitch: Introduce
#if stdlibor clarify#if swift— The broader theme of compiler vs. library version mismatches. - ST-0008: Exit Tests — Where the platform limitation was discussed, with reviewers noting the lack of ergonomic conditional compilation for this.
Questions for the community
I'd appreciate thoughts on:
- Is this a problem worth solving at the compilation-condition level? Or is there a simpler approach I've overlooked?
- Which direction (if any) feels right? Extending
canImport, a new standalone condition, or something different? - 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.