June 21, 2017·5 min read·engineeringswiftpackagesapi

SwiftPM makes targets and exported products explicit

The Swift 4 manifest redesign replaces confusing target inference with explicit declarations. Products describe what a package exports, while custom paths let existing source layouts remain usable.

A package's directory layout should not have to explain both its internal modules and the libraries it exports. Swift Package Manager's redesigned manifest gives those relationships explicit declarations. Targets describe the components being built. Products describe the libraries and executables the package makes available to other packages. That distinction becomes useful as soon as a repository contains support code that clients should not depend on.

The June 21 explanation of the redesign identifies confusing Swift 3 target inference as a reason for the change. The Swift 4 manifest removes most of that inference and requires targets and their dependencies to be declared. It also supports custom source layouts, so a package author can describe an existing directory structure instead of rearranging source merely to satisfy complex conventions.

I find this consistent with the parts of Swift I like. Its explicit handling of absence and its value collections make important program behavior visible in the code. A package manifest benefits from the same approach when it tells a reader which modules exist and which ones a consumer can obtain.

A target and a product answer different questions

A target defines a module or a test suite. In the new manifest, a target can depend on other targets in the same package or on products from dependency packages. The package's dependency list identifies those other packages, while the target's dependency list describes which components it actually needs.

A product exposes selected targets as a library or executable. This lets a repository contain targets that are useful for its own tests or examples without exporting all of them as public libraries. The announcement specifically advises against exporting sample targets and test-support libraries unintentionally. A target's presence in the build does not automatically make it part of the package's public offering.

This minimal manifest follows the new format. It declares a library product and the target providing its code, along with a test target that depends on that module.

// swift-tools-version:4.0
import PackageDescription

let package = Package(
    name: "Ledger",
    products: [
        .library(name: "Ledger", targets: ["Ledger"])
    ],
    targets: [
        .target(name: "Ledger"),
        .testTarget(name: "LedgerTests", dependencies: ["Ledger"])
    ]
)

The product and target share a name here, but they describe different relationships. A product identifies the exported library, while its target identifies the module built from source. Keeping both declarations becomes more informative when a library product includes several targets or a package contains internal targets that are not exported.

Explicit test-target declarations are part of the same change. The old inference rules no longer supply the entire target graph. A migration needs to describe the test suite and its dependency on the module under test, even when the repository's filenames make that relationship seem obvious to a person.

Source paths can describe an existing repository

When a target path is absent, the new rules search Sources, Source, src, srcs and Tests, in that order, to find the target. The recommended layout has a directory for each target beneath Sources. A package using that layout can still rely on path discovery while declaring its target graph explicitly.

A custom path handles a target whose source lives elsewhere. The announcement shows a target rooted at the package directory and another rooted at Sources. Its C library examples also select source directories or individual source files. This is particularly useful for packaging existing C libraries whose directory structure was established for another build system.

The consequence is a smaller role for directory conventions. They remain convenient defaults for locating source, but they no longer have to encode every dependency and exported library. A manifest reader can inspect the target graph even when the source layout is unfamiliar. A C library maintainer can preserve a layout used by other tools while describing the inputs SwiftPM needs.

This does add declarations to small manifests. The tradeoff is that package structure becomes reviewable as data in the manifest. Renaming or adding a directory is less likely to carry an implicit meaning that only someone familiar with the previous inference rules understands. The stated problem is confusion in those rules, so additional explicit structure directly addresses the reason for the redesign.

Tools version selects the manifest API

The first-line tools-version comment chooses the manifest format. Packages without that comment default to tools version 3.1.0. The Swift 4 package manager remains backward compatible with Swift 3 packages, so adopting the new tool does not require rewriting every existing manifest immediately.

The Swift 3.1 release announcement introduced the underlying tools-version mechanism. It determines the language used to interpret Package.swift and the available PackageDescription API. It also participates in dependency resolution by excluding package versions that require newer tools than the client has.

The June explanation applies this to the new manifest. A Swift 3 package manager ignores Git tags containing a Swift 4 manifest and can select the last tag with a Swift 3 manifest. The Swift 4 package manager can select the latest available version across both manifest formats. Publishing a new-format version therefore does not necessarily make the package unusable to every older client.

This behavior depends on retaining a compatible version that the older resolver can select. It does not teach an older package manager to interpret the new API. The distinction explains why a package's tools requirement belongs in version selection instead of being treated solely as a diagnostic after downloading the latest tag.

Manifest migration can precede source migration

The tools version also supplies the default Swift language version for the package's sources. That default can be overridden with swiftLanguageVersions, available in both manifest formats. The announcement explicitly supports updating the manifest while continuing to build source in Swift 3 compatibility mode.

I value the ability to separate those changes. A package can make its build graph explicit before changing source that uses the language or standard library. That creates a smaller reviewable change and avoids making manifest improvements wait for every source migration decision.

The documented migration updates the tools version, changes dependency syntax, declares ordinary and test targets, supplies custom paths where needed and exports the intended products. Dependencies also move before targets in the manifest's initializer arguments. These are specific changes to the description of the package. The resulting manifest can continue to request Swift 3 source rules with swiftLanguageVersions: [3], so the new package description does not by itself assert that the implementation has migrated to Swift 4.