What are package traits, and how do they differ from build configurations or separate packages for optional features?

16.8.4 · L5 · Interview

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 --traits for 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… →