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:
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
)
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 newerswift-foundation. Most APIs match; a few Apple-only ones (NSXPCConnection, manyUIKit/AppKittypes) don't exist. - Use
FoundationEssentialsfor a smaller dependency where available, and#if canImport(Darwin)/Glibc/Musl/WinSDKfor platform C APIs. - CI: GitHub Actions has
swift-actions/setup-swiftand the officialswiftDocker images.
Publishing a library¶
Tag a commit with a semantic version (git tag 1.0.0 && git push --tags). Users add:
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
publicon types and initializers that other modules need. - Naming the file
main.swiftand also using@main— conflict. Pick one. - Importing a product name vs a target name: you
importmodules (target names), but declare dependencies on products. - Branch dependencies — unstable and unpublishable. Use versions.
- Not committing
Package.resolvedin 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¶
- Create
swift package init --type libraryand add aslugify(_:)function with at least three unit tests (next lesson). - Split an executable into
MyToolCore(library) andmytool(executable) targets. - Add
swift-argument-parserand give your tool--verboseand--countoptions. - Add a text file as a resource and load it through
Bundle.module. - Use
#if os(...)to print the correct line separator for each platform. - 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; commitPackage.resolvedfor apps.swift-argument-parseris the standard way to build CLIs.
Next: Testing →