- Proposal: SE-NNNN
- Authors: Clive Liu
- Review Manager: TBD
- Status: Awaiting review
Introduction
This proposal extends the plugins parameter of SwiftPM target declarations to support platform and trait conditions, using the same .when(platforms:) and .when(traits:) syntax already available for target dependencies. This allows package authors to apply build tool plugins only on platforms where they are supported.
Motivation
SE-0303 introduced build tool plugins, and SE-0325 extended them with command plugins. Plugins are widely used for linting (SwiftLint), formatting (SwiftFormat), code generation (SwiftGen, SwiftProtobuf), and documentation (DocC). However, the plugins parameter on target declarations does not support any form of conditional application.
This is an important gap because build tool plugins are part of the build environment, not the built product. A package may be portable across platforms while some of its plugins are only relevant, available, or desirable on certain build hosts. SwiftPM already lets package authors conditionalize target dependencies and build settings, but not plugin application.
This creates a few practical problems:
- Host-specific tooling cannot be expressed declaratively. Build tool plugins run on the machine performing the build. A linter, formatter, code generator, or documentation tool may only be supported on a subset of host platforms, or may depend on host-specific toolchains and SDKs. Today there is no manifest-level way to say "apply this plugin only on macOS" or "only when building on a host that opts into linting".
- Development-only workflow tools are forced into every build. Many plugins are valuable for maintainers but are not actually required to build the package's product. Linters are the clearest example: they enforce policy and improve developer ergonomics, but they do not change the package's runtime behavior. Without conditional plugin application, package authors must either run such tools everywhere or fall back to manifest workarounds.
- Plugins can impose substantial build cost even when they are not always desired. Build tool plugins participate in build planning and execution. In some cases they also have noticeable impact on incremental builds. This makes traits a natural fit for plugin application: package authors should be able to attach tools like linting or optional generation to the manifest while letting users opt in only when they want them.
- There is no first-class manifest feature for this. Package authors who need host- or trait-specific plugin application must fall back to manifest compilation conditionals and helper variables instead of expressing the condition inline where the plugin is declared.
Consider a package developed on macOS that uses SwiftLint as a build tool plugin:
.executableTarget(
name: "MyTool",
plugins: [
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins"),
]
)
SwiftLint distributes a pre-built binary artifact bundle. That binary is compiled against a newer glibc than what ships on some Linux distributions (e.g., Amazon Linux 2). When building this package on such a system, the build fails immediately - not because of any issue with the package's own code, but because the plugin binary cannot execute:
swiftlint: /lib64/libc.so.6: version `GLIBC_2.34' not found
error: failed: PrebuildCommand(...)
The build never reaches compilation. The plugin is a development tool that is only meaningful on the developer's workstation - it has no effect on the compiled output. Yet there is no way to express "apply this plugin only on macOS" in the package manifest.
Current workarounds
The only workaround today is to use #if conditions in Package.swift to conditionally define the plugins array:
#if os(Linux)
let lintPlugins: [Target.PluginUsage] = []
#else
let lintPlugins: [Target.PluginUsage] = [
.plugin(name: "SwiftLintBuildToolPlugin", package: "SwiftLintPlugins"),
]
#endif
This works, but it has several drawbacks:
- Inconsistency with the rest of the manifest API. Target dependencies support
.when(platforms:)(SE-0273) and.when(traits:)(SE-0450). Plugin usage is the only target-level configuration that lacks conditional support. - Verbose and error-prone. Every target that uses the plugin must reference the computed variable instead of declaring the plugin inline. For packages with many targets, this scatters the conditional logic away from where it is used.
- Scales poorly. If a package needs different plugins on different hosts or behind different traits, the
#ifblocks multiply. - Breaks the declarative model.
Package.swiftis designed to be a declarative manifest. Manifest compilation conditionals are an escape hatch, not a first-class feature - they are evaluated when the manifest is compiled, not when SwiftPM plans the build.
Traits are a particularly good fit for plugins
Traits are especially useful for plugin application because many plugins represent workflow policy rather than product semantics. A package may reasonably want to define a Lint trait and apply a linter plugin only when that trait is enabled, for example via swift build --traits Lint. The same applies to optional code generation or documentation workflows.
Proposed solution
Extend Target.PluginUsage to accept an optional condition parameter using a new PluginUsageCondition type. The API follows the same .when(platforms:) and .when(traits:) style already used elsewhere in PackageDescription, while keeping plugin conditions as a distinct type.
.executableTarget(
name: "MyTool",
plugins: [
.plugin(
name: "SwiftLintBuildToolPlugin",
package: "SwiftLintPlugins",
condition: .when(platforms: [.macOS])
),
]
)
When the condition is not met, the plugin is not applied to the target. The plugin's package dependency is still resolved, consistent with SE-0273, but the plugin is not invoked and its prebuild/build commands are not added to the build graph.
With SE-0450 trait support, plugins could also be conditioned on traits:
.plugin(
name: "SwiftLintBuildToolPlugin",
package: "SwiftLintPlugins",
condition: .when(traits: ["Lint"])
)
This would let users opt into linting via swift build --traits Lint without requiring the plugin to run on every build or on every platform.
Detailed design
New PackageDescription API
The existing PluginUsage type gains a new factory function with a condition parameter:
extension Target.PluginUsage {
/// Creates a reference to a plugin with an optional condition.
///
/// When the condition is not met for the current build environment,
/// the plugin is not applied to the target.
///
/// - Parameters:
/// - name: The name of the plugin target.
/// - package: The name of the package that provides the plugin, or nil
/// if the plugin is defined in the same package.
/// - condition: The condition under which the plugin is applied.
@available(_PackageDescription, introduced: 6.x)
public static func plugin(
name: String,
package: String? = nil,
condition: PluginUsageCondition? = nil
) -> PluginUsage
}
This proposal introduces a new PluginUsageCondition type with the same overall shape as TargetDependencyCondition:
/// A condition that limits the application of a plugin to a target.
public struct PluginUsageCondition: Sendable {
/// Creates a condition that limits plugin application to specified platforms.
///
/// - Parameter platforms: The platforms on which the plugin should be applied.
public static func when(
platforms: [Platform]
) -> PluginUsageCondition
/// Creates a condition that limits plugin application to when specified traits are enabled.
///
/// - Parameter traits: The traits that must be enabled for the plugin to be applied.
public static func when(
traits: Set<String>
) -> PluginUsageCondition
/// Creates a condition that limits plugin application based on both platforms and traits.
///
/// - Parameters:
/// - platforms: The platforms on which the plugin should be applied.
/// - traits: The traits that must be enabled for the plugin to be applied.
public static func when(
platforms: [Platform],
traits: Set<String>
) -> PluginUsageCondition
}
Build planning behavior
When SwiftPM plans a build and encounters a plugin usage with a condition, it should do the following:
- Condition evaluation. The condition is evaluated against the host platform and enabled traits, using the same logic as
TargetDependencyCondition. - Plugin skipped. If the condition is not met, the plugin is not invoked. No prebuild or build commands from that plugin are added to the build graph.
- Dependency resolution unchanged. The plugin's package dependency is still resolved and fetched, consistent with SE-0273. This avoids adding host-specific logic to dependency resolution.
- Binary artifacts. If the plugin uses a binary artifact that is unavailable for the current platform, and the condition excludes that platform, SwiftPM does not raise an error. Without this proposal, the unavailable binary can cause a build failure even though the plugin would not be used.
Cross-compilation
For cross-compilation scenarios, the condition should be evaluated against the host platform, not the compilation target. Build tool plugins run on the host machine, so their platform requirements are host requirements.
Security
This proposal has no impact on security, safety, or privacy. It restricts when plugins are applied but does not change what plugins can do when they are applied.
Impact on existing packages
This proposal is additive. Existing plugin usage declarations without a condition parameter continue to work as before. The new API is gated on a new tools version.
Packages that currently use #if os(...) workarounds in their manifests can migrate to the new API for cleaner, more declarative manifests.
Alternatives considered
Reuse TargetDependencyCondition directly
Instead of introducing PluginUsageCondition, we could reuse TargetDependencyCondition. This would reduce API surface, but it conflates two different concepts: a dependency that is linked into the build product, and a plugin that runs during the build process. Separate types also leave room for the APIs to evolve independently - for example, a future configuration condition might make sense for plugins (skip linting in release builds) but not for dependencies.
Conditional package-level dependencies
An alternative approach would be to make the package-level dependency on the plugin package conditional, so it is not even fetched on unsupported platforms. This was considered but rejected because:
- It would require changes to dependency resolution, which is significantly more complex.
- SE-0273 explicitly chose not to affect dependency resolution for conditional target dependencies, and this proposal follows that precedent.
- Fetching a package that is not used has minimal cost compared to the build failure caused by invoking an incompatible plugin.
Do nothing - rely on #if os(...) in Package.swift
This is the status quo. It works, but it is inconsistent with the rest of the manifest API, verbose, and does not participate in cross-compilation planning. As more packages adopt plugins and support more platforms, this workaround will become more common and less acceptable.
Future directions
Configuration conditionals for plugins
SE-0273 proposed but has not yet implemented configuration conditionals (.when(configuration: .debug)). If configuration conditionals are added to TargetDependencyCondition, they should also be added to PluginUsageCondition. A common use case would be applying a linter plugin only in debug builds.