March 31, 2025·6 min read·engineeringswiftpackagesconditional-compilation

Swift 6.1 package traits compose across the dependency graph

Package traits make optional APIs and dependencies explicit in Swift 6.1. Their graph-wide union determines how package authors must design defaults and additive behavior.

A package's optional dependency can become an organizational problem. The Swift OpenAPI Generator ecosystem uses separate transport repositories so an application does not have to include every possible transport. That works, but users must discover the neighboring repositories and declare the right dependencies. SE-0450 uses this example to motivate package traits, which make configurable functionality part of a package's declared interface.

Swift 6.1, released on March 31, includes that mechanism. Authors declare traits in Package.swift, associate them with conditional source or dependencies, and choose defaults. Consumers can request additional traits or omit the defaults. The important composition rule is that SwiftPM calculates the union of requested traits for each package across the dependency graph.

I like Swift's tendency to put relevant distinctions in declarations. Package configuration benefits from the same treatment. A manifest that names an optional capability gives consumers information they cannot reliably infer from environment variables or an undocumented compiler define.

A platform does not determine every dependency choice

Platform checks answer whether code targets a particular operating system. They cannot express every choice a package's users need to make. The proposal gives logging as an example. Apple platforms offer OSLog, while server packages commonly use swift-log. A user can still want swift-log on an Apple platform, so an operating-system condition cannot determine the desired behavior.

Package traits provide named choices independent of that platform decision. They can control imports and source declarations through conditional compilation. They can also condition a target's dependency on a product. Those two uses need to agree. Source that imports an optional module must be guarded consistently with the dependency that supplies it.

The mechanism also replaces some reasons packages inspect environment variables in their manifests. SE-0450 describes that practice as unsupported and potentially vulnerable to stricter sandboxing. Moving configuration into the manifest's declared trait interface gives the dependency graph an explicit representation of the request.

This does not make every configuration choice appropriate for a trait. The proposal's union rule favors capabilities that coexist. A trait selecting an additional codec can compose with another codec. Two traits that each demand exclusive control over the same implementation create a different problem, because separate dependencies can request both.

A trait can enable other traits

The Swift 6.1 implementation of Trait exposes a name, an optional description, and a set of other traits enabled by that trait. It also provides a default-trait declaration. A package can use those relationships to offer a convenient group of capabilities without requiring consumers to enumerate every component.

For example, the following is a small manifest for a library target called Example. It declares JSON as a default capability and offers a Formats trait that also enables JSON:

// swift-tools-version: 6.1
import PackageDescription

let package = Package(
    name: "Example",
    products: [
        .library(name: "Example", targets: ["Example"])
    ],
    traits: [
        "JSON",
        .trait(name: "Formats", enabledTraits: ["JSON"]),
        .default(enabledTraits: ["JSON"])
    ],
    targets: [
        .target(name: "Example")
    ]
)

The source can use #if JSON around declarations associated with that capability. The initial mechanism uses compilation defines, as SE-0450 explains. It does not introduce a separate source-language expression for querying a package trait. Names are scoped to their package, so unrelated packages can use the same trait name without establishing a global option.

This resembles a package-level capability declaration, but it remains conditional compilation. Disabling the condition removes the guarded source from the compilation. A consumer therefore needs to know which traits supply an API, especially when that API is absent from the package's minimal configuration.

The union rule prevents private configuration assumptions

Suppose two dependencies use the same library. One requests its JSON capability and the other requests another format. SwiftPM combines their requests for that library. The resulting build enables both traits. It does not build an isolated variant for each parent solely because they requested different traits.

SE-0450 consequently says traits should be additive, and the release implementation's documentation states that enabling a trait must not remove API. Adding a requested capability must leave other consumers' requirements intact. Otherwise one dependency could invalidate code that another dependency was entitled to compile.

Mutually exclusive traits require coordination across the graph. The proposal discourages them and suggests explicit compile-time errors when such a design is unavoidable. Separating implementations into packages or using platform checks where the distinction truly is platform-specific can avoid some of these conflicts. The right representation depends on whether the choices can coexist.

This is the part of the feature I find most useful to understand before designing a package interface. A trait request is not a private instruction to construct a personal version of a dependency. It contributes to a shared configuration. Treating it as an exclusive selector gives an individual manifest more authority than the composition model provides.

Defaults affect compatibility

Default traits provide a convenient initial configuration. They also create compatibility obligations. Removing a default can remove APIs that consumers previously received without an explicit request. The proposal identifies that as a semantic-versioning break.

Existing APIs present another constraint. An author cannot safely move an established declaration behind a newly introduced trait merely because that trait is enabled by default. A consumer may already have disabled defaults. Adding a new default does not establish that every consumer will receive it, so the formerly unconditional declaration can disappear from an otherwise valid configuration.

That makes defaults more than a convenience setting. They are part of the expectations a package publishes. A newly added optional API is easier to place behind a trait because existing consumers have no prior entitlement to that declaration. Reclassifying an existing API changes the circumstances under which it is available.

The same reasoning applies to documentation. SE-0450 explains that symbol extraction sees only the code actually compiled. A documentation build without a trait cannot describe declarations removed by that trait's condition. The proposal recommends enabling all traits for documentation systems so optional APIs remain discoverable. Their conditional availability still needs explanation for readers choosing a smaller configuration.

Optional compilation does not promise optional resolution

The proposal separates dependency resolution from construction of the module graph. Its initial implementation considers traits after resolution. A trait can keep an optional dependency out of the constructed build without guaranteeing that the resolver never fetches it.

That distinction matters when evaluating the effect of adopting traits. Reducing the code linked into a product and avoiding a repository fetch are different outcomes. The mechanism addresses conditional source and dependencies, while making trait-aware resolution a possible future implementation improvement. A package author should not promise network behavior that this release does not establish.

Graph-wide unification also has a subtler consequence. One package can accidentally compile because another package enabled a dependency's optional API. The first package then appears to work despite not having declared the trait it needs. SE-0450 describes this loss of per-parent information and discusses deeper compiler integration as a future direction.

An explicit declaration remains necessary even when a larger application currently enables the capability through another path. Otherwise removing an unrelated dependency can remove the trait that made the accidental use compile. This follows from the shared union, even though no version of the underlying library changed.

The distinction also affects reusable package examples. An example that uses an optional API should declare the corresponding trait in its own dependency request. Depending on a larger application to enable that API makes the example less portable. A minimal consumer can reveal this mistake because its graph contains fewer unrelated trait requests. This is a direct consequence of configuration unification, not a separate isolation mechanism supplied by the compiler.

Root-package commands expose the configuration

The proposal adds command-line controls for the root package. Named traits can be enabled as a comma-separated list, while separate options enable all traits or disable defaults. For the illustrative package above, a build selecting the declared group is:

swift build --traits Formats

These controls make a minimal configuration and an expanded configuration reproducible without editing the manifest. They also provide package authors a way to exercise code excluded by ordinary defaults. SE-0450 discusses automatic exploration of all combinations as future work, so the release should not be described as supplying that broader testing facility.

Swift 6.1 places this package work alongside ongoing concurrency usability and compile-time improvements. For package design, the concrete advance is a declared way to associate optional source with optional dependencies. The graph-wide union determines which configurations are valid together, and the compatibility rules determine which existing APIs must remain available as packages adopt it.