Hello folks,
I'd like to pitch the idea to improve SwiftPM such that package authors can declare deprecated products. The pitch refers to 6.5 simply as a placeholder - the actual version is dependent on acceptance of the SE proposal (whenever it occurs) and delivery of the implementation.
Introduction
Package authors currently have no first-class way to signal that a product vended from a Swift package is on a path to removal. Swift Package Manager (SwiftPM) supports rich deprecation semantics for Swift source APIs through @available(*, deprecated, ...), but a Package.swift manifest cannot express that same intent for a .library, .executable, or .plugin product.
This proposal introduces a way to mark products as deprecated directly in the Package.swift\ manifest. When a consumer package depends on a deprecated product, SwiftPM emits a warning at build time that surfaces the deprecation message (and optional replacement) declared by the producing package. A companion `swift package audit` command lists every deprecated product in a package graph on demand.
Motivation
Deprecation is a common part of the software lifecycle: package authors need to evolve their APIs, rename products, split libraries into smaller pieces, or retire executables that have been superseded. Today, the manifest offers no mechanism to communicate any of that to package consumers.
The current workarounds are all lossy:
- Source-level
@available(*, deprecated, ...)annotations apply to Swift declarations, not to products. If a package vends a library product whose entire purpose is superseded, source-level deprecations must be sprinkled across every public symbol in the module, which is noisy and easy to miss. They also don't apply to executable or plugin products at all. - README notes, CHANGELOG entries, or GitHub release notes rely on consumers reading external documentation. They provide no build-time signal, so downstream packages continue using the product silently until a future major version removes it.
- Renaming or deleting the product outright is a hard break. It forces every consumer to react immediately, even when the producer intends to provide a grace period.
- Emitting warnings via a custom build tool plugin is possible only for packages that already ship a plugin and does not integrate with SwiftPM's diagnostic pipeline.
Package authors need a lightweight, declarative way to communicate that a product should no longer be used and, when applicable, to point consumers at a replacement. SwiftPM is already the natural place for this signal because it is what resolves and links the product, and it is where the consumer's Package.swift declares the dependency.
Proposed solution
Add a deprecated: parameter to the .library(...), .executable(...), and .plugin(...) factory methods on Product. The parameter accepts a Product.Deprecation value describing the deprecation, including an optional human-readable message and an optional replacement product name.
// swift-tools-version: 6.5
import PackageDescription
let package = Package(
name: "Paper",
products: [
.library(name: "Paper", targets: ["Paper"]),
.library(
name: "PaperLegacy",
targets: ["PaperLegacy"],
deprecated: .deprecated(
message: "PaperLegacy is superseded by Paper. It will be removed in Paper 3.0.",
renamed: "Paper"
)
),
.executable(
name: "paper-tool-old",
targets: ["paper-tool-old"],
deprecated: .deprecated(message: "Use ``paper-tool`` instead.")
),
],
targets: [
.target(name: "Paper"),
.target(name: "PaperLegacy", dependencies: ["Paper"]),
.executableTarget(name: "paper-tool-old"),
]
)
When any consumer package (or another product within the same package) depends on a deprecated product, SwiftPM emits a warning that includes the product name, the producing package's identity, the author-supplied message, and the suggested replacement, if any. In addition, this proposal introduces a swift package audit subcommand that lists every deprecated product reachable from the current package graph so authors and consumers can survey their migration surface without having to trigger a full build.
Existing manifests are unaffected as the deprecated: parameter is optional and defaults to nil.
Detailed design
PackageDescription API
A new nested type, Product.Deprecation, models a product deprecation:
extension Product {
/// Describes the deprecation state of a product.
public struct Deprecation: Sendable, Equatable {
/// A human-readable message explaining why the product is deprecated
/// and, ideally, what a consumer should do instead.
public let message: String?
/// The name of a product that supersedes this one, if any.
///
/// The named product should exist in the same package. SwiftPM uses
/// this value in diagnostics but does not automatically rewrite
/// consumer dependencies.
public let renamed: String?
/// Marks a product as deprecated.
///
/// - Parameters:
/// - message: A human-readable explanation of the deprecation.
/// - renamed: The name of the replacement product, if any.
public static func deprecated(
message: String? = nil,
renamed: String? = nil
) -> Deprecation
}
}
The existing factory methods gain a new trailing parameter:
extension Product {
@available(_PackageDescription, introduced: 6.5)
public static func library(
name: String,
type: Library.LibraryType? = nil,
targets: [String],
deprecated: Deprecation? = nil
) -> Product
@available(_PackageDescription, introduced: 6.5)
public static func executable(
name: String,
targets: [String],
deprecated: Deprecation? = nil
) -> Product
@available(_PackageDescription, introduced: 6.5)
public static func plugin(
name: String,
targets: [String],
deprecated: Deprecation? = nil
) -> Product
}
The existing overloads without deprecated: remain available so that manifests targeting older tools versions continue to compile unchanged.
Manifest and model changes
ProductDescription in PackageModel gains a new field:
public struct ProductDescription: Hashable, Codable, Sendable {
public let name: String
public let targets: [String]
public let type: ProductType
public let settings: [ProductSetting]
/// The deprecation information declared for the product, if any.
public let deprecation: ProductDeprecation?
}
public struct ProductDeprecation: Hashable, Codable, Sendable {
public let message: String?
public let renamed: String?
}
The manifest loader serializes and deserializes the new field. Because ProductDescription is Codable, the field is added as an optional to keep existing serialized manifests compatible.
Diagnostic behavior at build time
When SwiftPM resolves a package graph, for every product dependency it inspects the resolved ProductDescription for a non-nil deprecation value. For each such use, SwiftPM emits a warning through its existing ObservabilityScope:
warning: product 'PaperLegacy' from package 'paper' is deprecated: PaperLegacy is superseded by Paper. It will be removed in Paper 3.0. Use 'Paper' instead.
The warning is emitted:
- At most once per consumer target that references the deprecated product, so consumers are not flooded with duplicate messages.
- Only when the *consumer* package's manifest sees the dependency. A package that vends a deprecated product but does not consume it does not receive a warning about its own product.
- Independently of whether the product is a library, executable, or plugin.
When `renamed:` is provided, SwiftPM appends a `Use '' instead.` sentence to the diagnostic. SwiftPM does not verify that `renamed` refers to an existing product in the producing package; unresolved names are surfaced as-is in the diagnostic, mirroring the behavior of `@available(..., renamed:)` in Swift.
The warning is emitted by SwiftPM itself rather than by the Swift compiler, so it applies uniformly to executable and plugin products for which the compiler has no notion of "product."
swift package audit subcommand
Warnings surface deprecations only for products a package currently consumes. Package authors often want a broader view — for example, "which of my direct or transitive dependencies vend a deprecated product?" — without waiting for each warning to appear during a build. This proposal adds a new subcommand:
swift package audit [--json] [--include-transitive]
Behavior:
- Loads the current package graph without triggering a build.
- Walks every resolved product (direct or transitive) and reports each one whose
deprecationis non-nil. - Groups output by producing package, then product, and includes the author-supplied
messageandrenamedvalues. - Exits with status
0when the audit succeeds, regardless of whether any deprecations were found. A non-zero exit reserved for genuine load failures. This keeps the command usable in CI as a reporting step without automatically failing builds.
Flags:
--include-transitive(default:false) — also lists deprecated products in transitive dependencies that the current package does not directly consume. Useful for producers auditing their own reach.--json— emits machine-readable output with the following schema:
{
"deprecatedProducts": [
{
"package": "paper",
"product": "PaperLegacy",
"type": "library",
"message": "PaperLegacy is superseded by Paper. It will be removed in Paper 3.0.",
"renamed": "Paper",
"usedBy": ["MyApp"]
}
]
}
The subcommand is purely a reporting tool. It does not modify any files, does not fail the build, and does not require network access beyond what a normal package graph load already performs.
Interaction with existing features
swift package dump-package: The dumped JSON gains an optionaldeprecationobject on each product withmessageandrenamedfields when present. Tools that already consumedump-packageoutput are unaffected because the field is additive and optional.- Package traits (SE-0450): Deprecation applies to the product itself, independent of which traits enable it. If a product is only available under a particular trait, its deprecation is reported when that trait is enabled and the product is consumed.
- Registry and source-control dependencies: The deprecation information travels with the manifest, so the mechanism works uniformly for packages fetched from a registry (SE-0292) or from source control.
- Tools-version gating: Manifests that use the new
deprecated:parameter must declareswift-tools-version:6.5or later. Older tools versions parse the manifest without recognizing the parameter, matching existing behavior for other version-gated API.