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
- 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 regularswiftSettings
. This prevents misuse but introduces API divergence. - 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: Aredev
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 adev
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).
- May cause build failures for the consumer (e.g.,
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:
- No New Top-Level Setting Arrays: Continue using the existing
swiftSettings
,cSettings
,cxxSettings
. - Introduce
BuildPurpose
inBuildSettingCondition
:public enum BuildPurpose: String { // Or similar case development case distribution } public static func when( // ... existing parameters ... purpose: BuildPurpose? = nil // New parameter ) -> BuildSettingCondition
- 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)) ] )
- 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 includesBuildPurpose
. - 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.
- SwiftPM determines the current
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
.