Skip to content

10. Swift Package Manager

What & why

Swift Package Manager (SwiftPM) is Swift's official build system and dependency manager. It builds your code, runs your tests, fetches libraries, and works the same on macOS, Linux, and Windows. Xcode uses it too. A project is a package, described by a Package.swift manifest written in Swift.

Anatomy of a package

Greeter/
├── Package.swift
├── Sources/
│   ├── GreeterKit/            ← library target
│   │   └── Greeting.swift
│   └── greet/                 ← executable target
│       └── main.swift
└── Tests/
    └── GreeterKitTests/
        └── GreetingTests.swift

Create these with:

swift package init --type library      # or: executable, tool, ...

The manifest

// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "Greeter",
    platforms: [.macOS(.v13)],                       // only matters for Apple platforms
    products: [
        .library(name: "GreeterKit", targets: ["GreeterKit"]),
        .executable(name: "greet", targets: ["greet"]),
    ],
    dependencies: [
        .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"),
    ],
    targets: [
        .target(name: "GreeterKit"),
        .executableTarget(
            name: "greet",
            dependencies: [
                "GreeterKit",
                .product(name: "ArgumentParser", package: "swift-argument-parser"),
            ]
        ),
        .testTarget(name: "GreeterKitTests", dependencies: ["GreeterKit"]),
    ]
)

Key ideas:

  • swift-tools-version (first line, a comment that is parsed) declares which manifest API and default language mode you use. 6.0 → Swift 6 language mode with strict concurrency checking.
  • Target — a unit of compilation: a module. Types: .target (library code), .executableTarget, .testTarget, plus plugin, macro, system-library and binary targets.
  • Product — what you offer to others: a library or executable. Targets you don't list as products are internal.
  • Dependency — another package, by URL and version rule.

Common commands

swift build                 # debug build
swift build -c release      # optimized
swift run greet --name Ada  # build + run an executable product
swift test                  # run tests
swift test --filter GreetingTests/testBasic
swift package resolve       # fetch dependencies
swift package update        # update to newest versions allowed by the manifest
swift package clean         # remove build artifacts
swift package describe      # print the package structure
swift package show-dependencies

Dependency versions are recorded in Package.resolved. Commit it for applications (reproducible builds); for libraries it's conventionally not committed because consumers resolve their own graph.

Version requirements

.package(url: "…", from: "1.2.3")                   // 1.2.3 up to (not including) 2.0.0
.package(url: "…", .upToNextMinor(from: "1.2.3"))   // 1.2.3 up to 1.3.0
.package(url: "…", exact: "1.2.3")
.package(url: "…", "1.2.0"..<"1.5.0")
.package(url: "…", branch: "main")                   // not allowed for released dependencies
.package(path: "../LocalPackage")                    // local development

SwiftPM follows semantic versioning: from: trusts that minor and patch releases don't break you.

Splitting code into modules

A library target next to an executable is the standard shape: put the logic in a library, keep main.swift thin. That makes the logic testable (you can't @testable import an executable's main.swift conveniently, though modern toolchains do allow testing executable targets).

// Sources/GreeterKit/Greeting.swift
public struct Greeter: Sendable {
    public let prefix: String
    public init(prefix: String = "Hello") { self.prefix = prefix }
    public func greet(_ name: String) -> String { "\(prefix), \(name)!" }
}
// Sources/greet/main.swift
import GreeterKit

let name = CommandLine.arguments.dropFirst().first ?? "world"
print(Greeter().greet(name))

Across module boundaries, declarations must be public (or package) to be visible, and public structs need a public init. This forces you to design the API surface deliberately.

Access to the whole package: package

package access shares symbols between targets within one package without exposing them to clients — perfect for internal glue between your own modules.

Using the ArgumentParser dependency

swift-argument-parser is the de facto standard for command-line tools:

import ArgumentParser
import GreeterKit

@main
struct Greet: ParsableCommand {
    @Option(name: .shortAndLong, help: "Who to greet.")
    var name: String = "world"

    @Flag(help: "SHOUT the greeting.")
    var loud = false

    func run() throws {
        let text = Greeter().greet(name)
        print(loud ? text.uppercased() : text)
    }
}

@main marks the entry point, so the file must not be named main.swift. Run swift run greet --name Ada --loud, and you get --help for free.

Resources and plugins

.target(
    name: "GreeterKit",
    resources: [.process("Resources")]       // images, JSON, etc. → Bundle.module
)
let url = Bundle.module.url(forResource: "words", withExtension: "txt")

Build tool plugins (e.g. code generation, linters like SwiftLint) are declared as dependencies and attached to targets with plugins:.

Language settings per target

.target(
    name: "GreeterKit",
    swiftSettings: [
        .enableUpcomingFeature("ExistentialAny"),
        .define("DEBUG_LOGGING", .when(configuration: .debug)),
    ]
)

Conditional compilation in code:

#if DEBUG_LOGGING
print("verbose")
#endif

#if os(Windows)
let separator = "\\"
#elseif os(Linux) || os(macOS)
let separator = "/"
#endif

#if canImport(Foundation)
import Foundation
#endif

Cross-platform notes

  • On Linux and Windows, Foundation is swift-corelibs-foundation / the newer swift-foundation. Most APIs match; a few Apple-only ones (NSXPCConnection, many UIKit/AppKit types) don't exist.
  • Use FoundationEssentials for a smaller dependency where available, and #if canImport(Darwin) / Glibc / Musl / WinSDK for platform C APIs.
  • CI: GitHub Actions has swift-actions/setup-swift and the official swift Docker images.

Publishing a library

Tag a commit with a semantic version (git tag 1.0.0 && git push --tags). Users add:

.package(url: "https://github.com/you/GreeterKit", from: "1.0.0")

Document with DocC (/// comments + swift package generate-documentation via the DocC plugin) and list the package on the Swift Package Index.

Common mistakes

  • Putting all logic in the executable target. Move it into a library target.
  • Forgetting public on types and initializers that other modules need.
  • Naming the file main.swift and also using @main — conflict. Pick one.
  • Importing a product name vs a target name: you import modules (target names), but declare dependencies on products.
  • Branch dependencies — unstable and unpublishable. Use versions.
  • Not committing Package.resolved in apps, then getting different dependency versions on different machines.
  • Mixing Apple-only frameworks into code meant to run on Linux/Windows without #if canImport.

Exercises

  1. Create swift package init --type library and add a slugify(_:) function with at least three unit tests (next lesson).
  2. Split an executable into MyToolCore (library) and mytool (executable) targets.
  3. Add swift-argument-parser and give your tool --verbose and --count options.
  4. Add a text file as a resource and load it through Bundle.module.
  5. Use #if os(...) to print the correct line separator for each platform.
  6. Create two local packages where one depends on the other with .package(path:).

Recap

  • A package = Package.swift + Sources/ + Tests/; the manifest is Swift code.
  • Targets are modules; products are what you export; dependencies come in by URL and version.
  • Keep logic in a library target, executables thin; control visibility with public/package.
  • swift build/run/test, swift package resolve/update; commit Package.resolved for apps.
  • swift-argument-parser is the standard way to build CLIs.

Next: Testing →