[Pitch] Allow Deprecating Package Products

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.
17 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.

2 Likes

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.

2 Likes

I like the direction and would personally use this, thank you @bkhouri for working on this!

A few questions/comments:

  • What is the logical difference between .deprecated(message:renamed:) and .renamed(to:message:)?
  • Is the only use-case for .supersededByMajorVersion within the same package to avoid having a deprecated package when creating a new major? Can it be combined with just .supersededBy?
  • A naming nit: .renamed(to:), .deprecated(renamed:) and .supersededBy(product:) are inconsistent. For example .superseded(by:) would be more consistent. Maybe even leave just retired(message: String) and superseded(by product: String, in package: Package?) (which would cover renaming and move to another package, or another major of the same package).

I was thinking a product may be deprecated but does not have a replacement. In this case, there would be no difference between .deprecated and .retired. From these two options, .retired is less ambiguous. So I'm considering removing .deprecated altogether.

I believe there is a use case for .supercededByMajorVersion where there is a critical defect that was released in a newer major version, or if the package author no longer wants to support a specific version of a product. In this way, the product at version X may be deprecated but version "X+1" is not. This use case is not covered by any other options.

1 Like

(Nit: "superseded" has no 'c'.)


I think this pitch tackles an important concept. I wonder if anchoring on the existing availability concept of "deprecation," though, is actually inapt. One reason I'm wondering is that you're trying to adapt existing terminology but you're also pulled to invent new ones such as "superseded" and "retired."

I think either the existing ideas around availability are straightforwardly adaptable to package products, in which case this proposal should be able to adopt existing terminology without much fuss—I'd point out as a nit, for example, that for availability it's just "renamed X" and not "renamed to X," and my suggestion would be that we should not introduce gratuitous discrepancies—or we should choose entirely new terminology for what this feature is about.

The example you give where "the package author no longer wants to support a specific version of a product" is one example that's suggesting to me the latter, that we should use altogether new terms. When it comes to API availability, it is vacuous to say that one version of an API has been superseded by another: indeed, APIs that have successors in later versions are the ones that don't require annotation because they have ongoing support—it's precisely those APIs that won't have successors that need to be marked deprecated.

And if we want an API to be immediately pulled out of service because "there is a critical defect," then we mark it as "obsoleted." There isn't a counterpart for the pitch here, presumably because making an entire package product immediately unavailable is exactly what you wouldn't want to edit a package manifest for—I'd assume you'd simply withdraw the package product itself?

So (naively), it doesn't seem to me that availability terminology maps well to this problem domain. Perhaps we can re-center based your point that these annotations are about what the package author wants to support: the feature would then be to mark a package product "unsupported" with either a parameter to indicate that it's been superseded by something (the absence of which would then naturally mean it's not been superseded by anything—ergo, it is retired altogether).

Thanks for this feedback @xwu . I reworked the Product.Deprecation API to provide a single factory function unsupported(message:reason:) and moved the supersededByMajor into the Future Direction section, providing only renamed and inPackage as valid replacements for the first iteration.

extension Product {
    /// Describes the deprecation state of a product.
    ///
    /// Values of this type are constructed exclusively through the
    /// static factory below. The type intentionally exposes no public
    /// properties so that new deprecation metadata can be added in
    /// future evolution without a source or ABI break.
    public struct Deprecation: Sendable, Equatable {
        /// Marks a product as unsupported.
        ///
        /// - Parameters:
        ///   - message: A human-readable explanation of the deprecation.
        ///   - replacement: An optional locator identifying the product
        ///     consumers should adopt instead. When `nil`, the product is
        ///     retired with no advertised replacement.
        public static func unsupported(
            message: String? = nil,
            replacement: Replacement? = nil
        ) -> Deprecation

        /// Identifies the product a consumer should adopt in place of an
        /// unsupported product.
        ///
        /// Values of this type are constructed exclusively through the
        /// static factories below.
        public struct Replacement: Sendable, Equatable {
            /// The replacement is another product in the same package.
            ///
            /// - Parameter newName: The name of the replacement product
            ///   in the same package.
            public static func renamed(
                to newName: String
            ) -> Replacement

            /// The replacement is a product in another package.
            ///
            /// - Parameters:
            ///   - package: The identity of the replacement package.
            ///   - product: The name of the replacement product in that package.
            public static func inPackage(
                _ package: String,
                product: String
            ) -> Replacement
        }
    }
}

I also kept deprecated as the parameter used in the Product factories. This name may be "incompatible" with the single .unsupported(...) factory. So I'm open to alternate names.

Does this better align to your terminology concern?

The pitch delta can be found here: SE-NNNN: Allow Deprecating Package Products by bkhouri · Pull Request #3401 · swiftlang/swift-evolution · GitHub

I think this is much clearer conceptually, yes!

One nit might be that the companion to renamed (which, again, doesn't need the parameter to: and needlessly differs from our other usage with it included) could be movedTo(package:product:) or something like that.

How about calling both kinds renamed? .renamed("NewProduct") / .renamed("NewProduct", package: "AdditionalPackage")

1 Like

If coalesced into one, it wouldn't need a static factory method or a type at all, right? Couldn't this be just something like unsupported(renamedReplacement: "NewProduct", package: "AdditionalPackage") instead of unsupported(replacement: .renamed("NewProduct", package: "AdditionalPackage"))?

1 Like

The only reason to use a static factory would be to allow for potentially future expansion, where the current "replacement" may be convenient for the "new" case.

I'd like to hear what folks think about the "property name" that will be added to the various product factories. I'm specifying a single factory here, but my comment applies to all factories that introduce the new "property".

    @available(_PackageDescription, introduced: 6.5)
    public static func executable(
        name: String,
        targets: [String],
        deprecated: Deprecation? = nil  // new
    ) -> Product

The name deprecated may not align that well with unsupported(...). What is the communities thoughts here?

I also update the Pitch title to Allow Deprecating Package Products

In addition, I made some changes to the swift package audit command.

  1. --include-transitive flag
  1. The JSON structure changed to include: breadcrumb, transitive properties for the deprecated products. The breadcrumb only exists for direct and transitiveReachable products.