How do swiftSettings and their .when conditions work in a package, and which targets do they reach?

16.8.1 · L3 · Interview

Predict first

Which conditions are active in debug and release, and does App see Core’s define?

// === Package.swift ===
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
    name: "App",
    platforms: [.macOS(.v13)],
    targets: [
        .target(name: "Core", swiftSettings: [
            .define("STAGING"),
            .define("VERBOSE", .when(configuration: .debug)),
            .define("APPLE", .when(platforms: [.macOS, .iOS])),
        ]),
        .executableTarget(name: "App", dependencies: ["Core"]),
    ]
)
// === Sources/Core/Core.swift ===
public func flags() -> [String] {
    var f: [String] = []
    #if STAGING
    f.append("STAGING")
    #endif
    #if VERBOSE
    f.append("VERBOSE")
    #endif
    #if DEBUG
    f.append("DEBUG")
    #endif
    #if APPLE
    f.append("APPLE")
    #endif
    return f
}
// === Sources/App/main.swift ===
import Core
print(flags())
#if STAGING
print("App also sees STAGING")
#else
print("App does not see STAGING")
#endif
// === run.sh ===
echo "debug:   $(swift run 2>&1 | tail -2 | tr '\n' ' ')"
echo "release: $(swift run -c release 2>&1 | tail -2 | tr '\n' ' ')"
  • debug: STAGING, VERBOSE, DEBUG, APPLE; release: STAGING, APPLE; App does not see STAGING
  • both configurations print the same list
  • App sees STAGING because it depends on Core

Mental model. Settings are labels stuck on one box of parts, not a notice on the whole warehouse: the box next door never reads them.

Each target has its own swiftSettings (and cSettings, linkerSettings), and a setting can carry a condition: .when(configuration: .debug) or .release, .when(platforms: [.macOS, .iOS]), and (tools 6.1) .when(traits:). In the check, the Core target defines STAGING always, VERBOSE only in debug and APPLE only on macOS or iOS. A debug run prints ["STAGING", "VERBOSE", "DEBUG", "APPLE"] and a release run ["STAGING", "APPLE"]: SwiftPM itself defines DEBUG for debug builds (Xcode does the same through SWIFT_ACTIVE_COMPILATION_CONDITIONS, Q369) and nothing for release. Crucially the App target does not see STAGING: a define applies to the module it is declared on, not to its dependents or dependencies, which is why a flag that should reach several modules has to be repeated or moved into a shared helper such as a function in one module.

Consequences for design. Compile-time conditions create different binaries, so keep them few and well tested in both configurations (a CI job that only builds debug will not see a release-only compile error). Do not use a condition for something you can decide at run time with a flag (Q372); use it for code that cannot compile everywhere (#if os(macOS) APIs) or for debug-only tooling. Conditions are also how one package targets several platforms: platforms: sets the minimum deployment version (next question), while .when(platforms:) turns individual settings or dependencies on and off. Remember that an .unsafeFlags escape is not allowed for dependencies (Q620) and that a manifest is evaluated once per package, so it cannot branch on the running machine except through #if on the host.

Conditional defines follow configuration and platform, and apply only to the target that declares them.

Common traps

  • Defining a flag on the app target and expecting a library target to honour it. Each module is compiled with its own settings.
  • Only ever building debug. A #if !DEBUG branch can hold a compile error nobody sees.

They might follow up with

  • Where does DEBUG come from? SwiftPM adds it for debug builds; Xcode sets it in the build settings.
  • How do I share a flag across modules? Repeat the define in each target, or read one value from a module they all import.

Related questions

  • 16.8.2 What does the platforms: array in Package.swift mean, and why can a dependency stop your build?
  • 16.7.4 Why does SwiftPM refuse a dependency that uses unsafeFlags, and what should you do instead?
  • 16.8.4 What are package traits, and how do they differ from build configurations or separate packages for optional…
  • 16.4.1 What are build configurations and build settings, how do xcconfig files layer, and how does #if DEBUG work?
  • 16.5.2 How do you use feature flags: compile-time versus runtime, ownership, and modularisation by team?

What does the platforms: array in Package.swift mean, and why can a… →