"Dev-Only" Settings for SwiftPM

Hi everyone,

During the review of the Warning Control Settings for SwiftPM, @FranzBusch raised an excellent point about the concept of "dev-only" settings – settings that apply to a target when it's being built as a local package (i.e., the root package of the build) but not when it's built as a dependency of another package. This is particularly relevant for warning control flags.

I've been exploring this idea further, and it's become clear that a simple solution like adding a new top-level array (e.g., devSwiftSettings) to the target description has several complex implications that need careful consideration.

Initial Challenges with a Simple devSwiftSettings Approach

  1. Type Safety and Scope: If devSwiftSettings are to be truly distinct, they might warrant a new type (e.g., DevSwiftSetting) to prevent "dev-only" APIs (like the new warning controls) from being used in regular swiftSettings. This prevents misuse but introduces API divergence.
  2. The "Distribution" Counterpart: If we have settings specific to "development" (local builds), there's a natural need for settings specific to "distribution" (when built as a remote dependency, or when an author prepares a binary for distribution). For instance, one might want -DLOG_LEVEL=VERBOSE for development but -DLOG_LEVEL=ERROR for distribution. This could lead to another set of top-level arrays (e.g., distributionSwiftSettings).

Exploring the first challenge (distinct devSwiftSettings) raised several design questions:

  • Relationship between dev and regular settings: Are dev settings a distinct set, or can they include regular settings (like .swiftLanguageVersion or .interoperabilityMode)? It seems reasonable to consider allowing .unsafeFlags in "dev-only" settings. If .unsafeFlags are permitted, it might seem arbitrary to forbid other structured settings. However, this flexibility also introduces the risk of users misplacing vital settings in a dev scope, leading to unexpected behavior for consumers.
  • Universality of "dev-only": Is a binary "dev-only" vs. "all others" distinction sufficient for the future? If other orthogonal build contexts emerge that warrants their own top-level settings array, this could lead to a combinatorial explosion of setting types: dev-only, xxx-only, dev-and-xxx, all others.
  • Criteria for "dev-only" settings: How do we decide which settings are "dev-only"? For warnings, it's clear: prevent breaking consumer builds. For others (like sanitizers), it's less direct. My thinking evolved towards defining criteria for when a setting is unsuitable for regular or distribution settings (and thus belongs in a dev-scoped context), such as if it:
    • May cause build failures for the consumer (e.g., -Werror).
    • Produces irrelevant or excessive output for the consumer (e.g., verbose logs).
    • Generates additional artifacts not required by the consuming build (e.g., those from -save-temps).
    • Reduces the portability of the target (e.g., unconditionally enabling CPU-specific instructions).

The second challenge (needing distributionSettings) would further bloat the .target() API, potentially adding six new parameters (devSwiftSettings, distributionSwiftSettings, devCSettings, etc.), which is undesirable.

Building for Binary Distribution and the "Build Purpose" Concept

An important use case is when an author builds their local package with the intent of creating a distributable binary. In this scenario, they'd want "distribution" settings applied, not their local "development" settings (like verbose logs).

This led to the idea of a "build purpose" – distinct from the Debug/Release build configuration – to capture the intent behind a build:

  • development purpose: For active local development.
  • distribution purpose: For building a package for consumption by others (either as a remote dependency or a pre-compiled binary).

We could introduce a --purpose <purpose> flag for swift build. When building a local package with swift build we can pass --purpose distribution to force SwiftPM build the package as if it was a remote dependency.

Default Inference for Local Builds

While development/distribution is a distinct concept from debug/release, I feel like it's safe to say that most release builds are builds for distribution. We can leverage this in order to not break the existing practice of using swift build -c release to build for distribution with the following default purpose inference rules:

  • swift build -c debug would default to --purpose development.
  • swift build -c release would default to --purpose distribution.

The explicit --purpose flag would override these defaults, allowing any combinations of build configuration and build purpose, for example:

  • swift build -c release --purpose development (e.g., for profiling optimized code with some dev-time diagnostics active)
  • swift build -c debug --purpose distribution (e.g., to debug the exact "distribution" version of the code with symbols, or if the library is distributed in two versions: with and without debug info).

Remote Dependencies

When SwiftPM builds a remote dependency, it would always use the distribution purpose internally, independent of the build configuration (Debug/Release).

A More Integrated API: "Purpose" as a BuildSettingCondition

While having distinct DevSwiftSetting structs and devSwiftSettings arrays offers strong syntactic guarantees (making it impossible to put warning control flags in regular settings, for example), it creates a significant API split and doesn't integrate well with existing SwiftPM features that already have "local-only" behavior (@hborla brought examples of strict concurrency checking and strict memory safety).

Therefore, I'm now leaning towards a more integrated approach:

  1. No New Top-Level Setting Arrays: Continue using the existing swiftSettings, cSettings, cxxSettings.
  2. Introduce BuildPurpose in BuildSettingCondition:
    public enum BuildPurpose: String { // Or similar
        case development
        case distribution
    }
    
    public static func when(
        // ... existing parameters ...
        purpose: BuildPurpose? = nil // New parameter
    ) -> BuildSettingCondition
    
  3. Usage:
    .target(
        // ...
        swiftSettings: [
            // Common setting, always applies (unless further conditioned by config/platform)
            .define("COMMON_FEATURE"),
    
            // Dev-only warning setting
            .treatAllWarnings(as: .error, .when(purpose: .development)),
    
            // Purpose-specific macros
            .define("LOG_LEVEL_VERBOSE", .when(purpose: .development)),
            .define("LOG_LEVEL_ERROR", .when(purpose: .distribution)),
    
            // Existing local-only features could adopt this:
            .enableExperimentalFeature("StrictConcurrency", .when(purpose: .development))
        ]
    )
    
  4. SwiftPM Behavior & Validation:
    • SwiftPM determines the current BuildPurpose for each package being built (based on CLI flags for the root package, or hardcoded to .distribution for remote dependencies, as described above).
    • Settings are applied if their conditions (including purpose) match.
    • For settings known to be "dev-only" (like the new warning control APIs and potentially strict concurrency/memory safety), if they are used without an explicit .when(purpose: .development) (or are inappropriately used with .when(purpose: .distribution)), SwiftPM would issue a manifest validation warning to guide the user. Note: for existing settings like strict concurrency, SwiftPM should check the manifest's tools version and only produce these guidance warnings for manifests adopting a tools version that includes BuildPurpose.
    • A blanket warning suppression (e.g., -w) would still be applied by SwiftPM to all remote dependencies after their own settings are resolved, ensuring consumers are not impacted by dependency warnings.

Pros and cons of this Condition-Based Approach

Pros:

  • Keeps .target() API clean.
  • Leverages and extends the familiar BuildSettingCondition mechanism.
  • Offers a path to integrate existing "local-only" features into this explicit "purpose" model without breaking changes, improving manifest clarity over time.
  • Doesn't block SE-0480.

Cons:

  • swiftSettings: [.treatAllWarnings(as: .error)] remains a syntactically valid construction from the perspective of Swift's type system; the guidance would come from a SwiftPM warning during manifest evaluation.
  • The correct spelling, swiftSettings: [.treatAllWarnings(as: .error, .when(purpose: .development))] is verbose. Though I think explicit is often better, even if verbose.

I'd love to hear your thoughts on this refined direction, particularly the "build purpose" concept and its integration via BuildSettingCondition.

5 Likes

I appreciate the thinking that’s gone into this, but honestly the “purpose” reads exactly like a build configuration.

SwiftPM still doesn’t fully support configuration conditionals, or custom configurations. I’d much rather see SwiftPM’s support for custom configurations improve before adding in another analogous feature.

All I can see as an outcome here is that we would have two layers of classification (and the added complexity that entails).

10 Likes

I can see how purpose and configuration can be seen as related, but I can think of at least a couple of reasons why I think they're truly orthogonal (in no particular order):

  • Development release builds are required for profiling.
  • Service endpoints can be different per build purpose. You may need a development release build to use the company-private endpoints without sacrificing performance where you don't need to debug the code.
  • Debug distribution builds can be useful for user acceptance testing and end-to-end testing.
  • Debug distribution builds can be used to hot-swap libraries in existing builds for production debugging purposes.

EDIT

Having said that, I wonder if it'd make sense to explore the possibility of utilizing package traits for this. They seem like the kind of tool made for these situations.

1 Like

Is your thinking here guided by the limited implementation of build configurations in SwiftPM as it stands today, though? If they were fully implemented, you could configure them much like we've always done in Xcode to support things like Profiling configurations, UAT configs, etc.

What exactly do you mean by "Debug Distribution Builds"? Builds that include debugging symbols, but that still use a better than -O compiler optimisation flag? That sounds like a configuration to me, but perhaps my thinking is too inline with what Xcode is doing.

I mean that the Debug part of it describes how there are no optimizations done and the debut symbols are available, while the Distribution part of it means that it is compiled without warnings as errors, with the production endpoint for the back-end, and with library evolution mode.

Essentially, configuration answers the question "how to build the target", while purpose answers the question "why build the target".

When defining Xcode projects, I often combine the two into a single list of permutations like:

  • Development-Debug
  • Development-Release
  • Distribution-Debug
  • Distribution-Release

This is a great discussion. As we adopt Swift Build, I think the opportunity to better model build configurations in SwiftPM is a lot closer. Swift Build has a powerful build setting management system that brings settings defined at various levels and intelligently combines them to produce the command line fed to the build tools and to control various phases of the build process.

Xcode's xcconfig files feed into this, Adding a build configuration file to your project | Apple Developer Documentation. I'm wondering if we could adopt that as a mechanism, maybe make it more Swift-y like we've done with the package manifest. And maybe that's where you specify your developer only settings.

At any rate, I have to agree with @tonyarnold here. I think we really need to model build configurations explicitly. We need to get a deep understanding of how Swift Build works and how we can take advantage of it with SwiftPM. And we need a focus on developer ergonomics to make sure build settings are defined consistently whether you do it on targets in the package manifest, or in a custom build configuration description file.

I think we can do something great here with Swift Build in the world of Swift Packages. But it's going to take some time to learn it all and come up with a unifying vision.

5 Likes