What are package traits, and how do they differ from build configurations or separate packages for optional features?
Predict first
When is the Logging trait active?
// === run.sh ===
W=$(mktemp -d); cd $W; mkdir -p Lib/Sources/Lib Lib/Sources/App Consumer/Sources/Consumer
cat > Lib/Package.swift <<'E'
// swift-tools-version: 6.1
import PackageDescription
let package = Package(
name: "Lib",
products: [.library(name: "Lib", targets: ["Lib"]), .executable(name: "app", targets: ["App"])],
traits: [.trait(name: "Logging", description: "Adds verbose logging"), .default(enabledTraits: [])],
targets: [
.target(name: "Lib", swiftSettings: [.define("LOGGING", .when(traits: ["Logging"]))]),
.executableTarget(name: "App", dependencies: ["Lib"]),
]
)
E
cat > Lib/Sources/Lib/L.swift <<'E'
public func features() -> String {
#if LOGGING
"logging on"
#else
"logging off"
#endif
}
E
printf 'import Lib\nprint(features())\n' > Lib/Sources/App/main.swift
(cd Lib; echo "default: $(swift run app 2>&1 | tail -1)"
echo "--traits Logging: $(swift run --traits Logging app 2>&1 | tail -1)")
for t in '' ', traits: ["Logging"]'; do
cat > Consumer/Package.swift <<E
// swift-tools-version: 6.1
import PackageDescription
let package = Package(name: "Consumer", dependencies: [.package(path: "../Lib"$t)],
targets: [.executableTarget(name: "Consumer", dependencies: [.product(name: "Lib", package: "Lib")])])
E
printf 'import Lib\nprint(features())\n' > Consumer/Sources/Consumer/main.swift
label="asks for nothing"; [ -n "$t" ] && label="asks for Logging"
echo "consumer $label: $(cd Consumer; swift run 2>&1 | tail -1)"
done
- off by default; on with --traits Logging; on for a consumer only when it names the trait
- always on once declared
- on for a consumer even when it does not name the trait
Mental model. Traits are the optional extras on a car order form: the dealer builds one car, and the box you tick adds a feature, but nobody can tick “remove the brakes”.
Traits (SE-0450, tools 6.1) let a package declare optional features that a consumer turns on: traits: [.trait(name: "Logging"), .default(enabledTraits: [])], with targets reacting through .define("LOGGING", .when(traits: ["Logging"])) and optional dependencies through .when(traits:) on the dependency. In the check a library prints “logging off” by default, “logging on” with swift run --traits Logging app, and a consuming package gets “logging on” only when it writes .package(path: "../Lib", traits: ["Logging"]). Traits are additive and resolved across the whole graph: if any dependent enables a trait, it is on for everyone, so a trait must never remove or change behaviour, only add it (the same rule as Cargo features).
Compared with the alternatives. A build configuration (Debug or Release) is chosen by the person building and cannot be requested per dependency. A separate package (MyLibLogging) works with every tools version but multiplies repositories, versions and releases. Compile-time flags set by the consumer do not reach a dependency’s modules (Q623). Traits fill the gap: a dependency can offer a heavy optional integration (a networking backend, a logging sink, an experimental API) without making every client pay for it in build time and binary size. Limits: they need tools 6.1 or newer on the consumer, support in Xcode’s package UI was still evolving when this was written, a default trait set is part of your API (removing a default trait is a source break), and every combination is more to test. Consider a plain protocol plus a separate package first, and reach for a trait when the integration needs compile-time access to a heavy dependency.
A trait is off unless a default list, the command line or a dependent turns it on.
Common traps
- Using a trait to change existing behaviour. If another dependency enables it, your assumption flips under you.
- Forgetting that default traits are public API. Removing one from the default list breaks consumers who relied on it.
They might follow up with
- How do I test every trait? Build and test with
--traitsfor each combination you support, or--enable-all-traits. - Can a trait pull in a dependency? Yes: a dependency declaration can be conditioned on a trait.
Related questions
- 16.8.1 How do swiftSettings and their .when conditions work in a package, and which targets do they reach?
- 16.7.4 Why does SwiftPM refuse a dependency that uses unsafeFlags, and what should you do instead?
- 16.1.1 What are targets, products and modules in a Swift package, and what does a first Package.swift look like?
- 16.5.2 How do you use feature flags: compile-time versus runtime, ownership, and modularisation by team?
← How do per-target Swift language modes let a package migrate to Swift… What is a build tool plugin, how does it generate code, and what does… →