[Pitch] Deprecating Package Products in the Package Manifest

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:

  1. At most once per consumer target that references the deprecated product, so consumers are not flooded with duplicate messages.
  2. 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.
  3. 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 deprecation is non-nil.
  • Groups output by producing package, then product, and includes the author-supplied message and renamed values.
  • Exits with status 0 when 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 optional deprecation object on each product with message and renamed fields when present. Tools that already consume dump-package output 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 declare swift-tools-version:6.5 or later. Older tools versions parse the manifest without recognizing the parameter, matching existing behavior for other version-gated API.
12 Likes

Very happy to see this, thanks @bkhouri for this pitch! Overall I think this is the right direction, a few comments below:

(1) The deprecated: .deprecated( spelling in Package.swift is a little awkward, but I can't say I have a better one at the moment. Maybe deprecated: .renamed(...) would be nicer, not sure.

(2) While not blocking this initial pitch, and could be extended later, I'd like to see more capability in what the package author communicates: a) a replacement product in another package, b) a different major version in the same package, same product name, c) attaching soft/hard deprecation dates, d) explicitly marking the product as going away without replacement. This extra metadata would allow SwiftPM to adjust UI based on the urgency of the deprecation.

(3)

  • Exits with status 0 when 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.

I don't love this, as it means we can't simply put swift package audit into a nightly GitHub action, relying on the fact that it'll start failing once a deprecation appears in our dependency graph - but that's ultimately how I'd want to use this as a consumer of this feature. We'd have to parse the JSON output instead, which is a lot more work, especially in an ad-hoc script/YAML.

(4) Slightly related, but potentially something to break out into a separate discussion - being informed that a new version of a package is available that SwiftPM didn't resolve (probably because someone set an upper bound on a version constraint, or because the current major isn't the newest one anymore). While that's not an explicit deprecation, since most maintainers only ever actively maintain the latest version, being stuck on a not-latest version could be considered to be an implicit deprecation, if we squint.

1 Like

I completely agree this is awkward. I purposely left it this way to gather feedback. I suppose the API could look like deprecated: .renamed(to: "...", message: "") or deprecated: .message("..."). This API would allow us to augment the "deprecation notice" with obsoleted

Sounds good, we can return a non-zero if, and only if, swift package audit has no violations. we can support a --allow-deprecations option for developers that always want to have a 0 return code.

I agree this is useful to have, put it is outside the scope of this pitch. I'll be sure to include this in the "Future directions" sections

updated pitch

I posted an updated pitch here:
https://github.com/bkhouri/swift-evolution/blob/t/main/swiftpm_support_product_deprecation/proposals/NNNN-swiftpm-allow-deprecating-package-products.md

The pitch also changes, among other things, the swift package audit JSON output with a proposed JSON schema.

1 Like