11. Libraries, ABI & distribution¶
What & why¶
Shipping Swift code means choosing how it reaches users: as source (SwiftPM), as a binary framework, as a static executable, or as a container image. This lesson covers API design for libraries, what ABI stability and library evolution mean, how to publish and document, and how to ship binaries on macOS, Linux, Windows, and the web.
Source vs. binary distribution¶
| Option | You ship | Client builds | Evolution constraints |
|---|---|---|---|
| SwiftPM source package | Git repo + tag | the whole package | semantic versioning; none on binary layout |
Binary framework (.xcframework, .artifactbundle) |
prebuilt binary + .swiftinterface |
links only | library evolution (ABI) rules apply |
| Executable | the program | nothing | none (but static vs dynamic runtime) |
Most Swift libraries ship as source. A package is "just a git repo with a tag"; SwiftPM compiles it with the client's compiler and flags, so generics are specialised and inlined with no extra effort.
API design for libraries¶
Follow the Swift API Design Guidelines:
- Clarity at the point of use:
insert(_:at:), notinsertObject(_:atIndex:). - Name by role, not type. Methods that read like English:
x.isEmpty,array.sorted(by:). - Mutating vs non-mutating pairs:
sort()/sorted(),append(_:)/appending(_:). - Prefer labels over comments; document every public declaration with
///. - Use the smallest public surface:
internalby default;packagefor sharing between your own targets;publiconly on purpose. - Make
publicstructsSendablewhere truthful; addpublic inits explicitly (synthesized initialisers are internal). - Use
finalon public classes unless subclassing is a designed extension point (openmeans: designed for subclassing).
Documenting with DocC¶
/// Parses a semantic version string such as `"1.4.2-beta.1"`.
///
/// ```swift
/// let v = try Version("1.4.2")
/// print(v.minor) // 4
/// ```
///
/// - Parameter string: The text to parse.
/// - Throws: ``VersionError/invalid(_:)`` when the text isn't a valid version.
/// - Returns: The parsed version.
public init(_ string: String) throws { /* ... */ }
Add the DocC plugin (.package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.4.0")), then:
swift package generate-documentation
swift package --disable-sandbox preview-documentation --target MyLibrary
Hosting static DocC on GitHub Pages is a one-liner with --transform-for-static-hosting. The Swift Package Index can build and host docs for you.
Versioning¶
SwiftPM resolves with semantic versioning:
MAJOR— breaking API change.MINOR— new, backwards-compatible API.PATCH— bug fixes.
Adding a case to a public enum, adding a requirement to a public protocol, removing or renaming anything, changing a parameter type, or tightening a generic constraint are all breaking for source. Adding a defaulted parameter is source-compatible but changes the function's type (a problem if clients stored Foo.bar as a function value) — add an overload instead if that matters.
Release by tagging: git tag 1.2.0 && git push origin 1.2.0. Pre-1.0 tags (0.x) are allowed to break between minors by convention.
ABI stability and library evolution¶
The ABI (application binary interface) is how compiled code lays out types and calls functions. Apple platforms have a stable Swift ABI (since Swift 5), so the Swift runtime ships in the OS and apps built with different compiler versions interoperate. On Linux and Windows you generally ship the runtime with your app (or link statically).
ABI stability is about the runtime. Module stability (.swiftinterface text files) lets different compilers read a binary module. Library evolution is the mode that lets a library change without breaking already-compiled clients:
or in SwiftPM: swiftSettings: [.unsafeFlags(["-enable-library-evolution"])] (for binary products built outside a published package).
With evolution enabled, the compiler treats public types as resizable and reorderable: clients access stored properties through indirection, can't assume layout, and can't exhaustively switch over public enums without @unknown default. That costs performance, so you opt back into speed with:
| Attribute | Promise | Effect |
|---|---|---|
@frozen (struct/enum) |
layout will never change | clients can inline, stack-allocate, and switch exhaustively |
@inlinable |
function body is public API | clients may inline it, so the body can never change in ways clients depend on |
@usableFromInline |
an internal symbol may be referenced by inlinable code | stays in the ABI |
@available(...) |
when API became available | deployment-target safety |
@_spi(Name) |
System Programming Interface: public but hidden unless imported with @_spi(Name) import |
internal-only APIs between your frameworks |
@frozen public struct Point { public var x: Double; public var y: Double } // can never gain fields
public func lerp(_ a: Double, _ b: Double, _ t: Double) -> Double { ... } // body hidden: calls cross the module boundary
@inlinable public func square(_ x: Double) -> Double { x * x } // body visible: must keep forever
For source packages you can ignore all of this. Reach for these tools when shipping a binary framework that must stay compatible across versions.
Binary distribution¶
Apple platforms: XCFramework¶
xcodebuild archive -scheme MyLib -destination "generic/platform=iOS" -archivePath build/ios \
SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
xcodebuild archive -scheme MyLib -destination "generic/platform=macOS" -archivePath build/mac \
SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
xcodebuild -create-xcframework \
-framework build/ios.xcarchive/Products/Library/Frameworks/MyLib.framework \
-framework build/mac.xcarchive/Products/Library/Frameworks/MyLib.framework \
-output MyLib.xcframework
BUILD_LIBRARY_FOR_DISTRIBUTION=YES turns on library evolution and emits .swiftinterface. Consumers add it as a binary target:
.binaryTarget(
name: "MyLib",
url: "https://example.com/MyLib-1.2.0.xcframework.zip",
checksum: "<output of: swift package compute-checksum MyLib.xcframework.zip>"
)
Cross-platform: artifact bundles¶
SwiftPM also supports .artifactbundle binary targets (used for prebuilt command-line tools and build plugins).
Shipping executables¶
macOS¶
Distribute via a signed, notarised .pkg/.dmg, or Homebrew. Command-line tools only need the code signature and (for downloads) notarisation.
Linux: static executables¶
Dynamic builds depend on the Swift runtime libraries on the target machine. Avoid that with:
For a fully static, portable binary — no glibc dependency — use the Static Linux SDK (musl):
swift sdk install https://download.swift.org/.../swift-6.0-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz --checksum <...>
swift build -c release --swift-sdk x86_64-swift-linux-musl
The result runs on any x86-64 Linux, including FROM scratch containers — a tiny Docker image:
FROM swift:6.0 AS build
WORKDIR /src
COPY . .
RUN swift build -c release --static-swift-stdlib
FROM ubuntu:24.04
COPY --from=build /src/.build/release/mytool /usr/local/bin/mytool
ENTRYPOINT ["mytool"]
Windows¶
Copy the executable plus the Swift runtime DLLs (from the toolchain's Runtimes\ folder), or build with -static-stdlib. Installers can use WiX/MSI; winget manifests publish to the Windows package manager.
WebAssembly¶
Swift compiles to WASI via the Wasm SDK (swift sdk install + --swift-sdk wasm32-unknown-wasi) — handy for browser-side tools; use JavaScriptKit for DOM interop.
Cross-compilation¶
Swift SDKs (the successor to toolchain hacks) let one machine target others: swift build --swift-sdk <id>. List installed SDKs with swift sdk list.
CI/CD¶
A minimal GitHub Actions matrix across platforms:
name: CI
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: "6.0"
- run: swift build -c release
- run: swift test
Add swift build -Xswiftc -warnings-as-errors for libraries, API-diff checks (swift package diagnose-api-breaking-changes 1.0.0) before releases, sanitizer jobs for tools that touch unsafe code, and DocC builds.
Release checklist¶
- All public API documented; docs build without warnings.
swift package diagnose-api-breaking-changes <last-tag>is clean for minor/patch releases.- CHANGELOG entry; semantic version chosen deliberately.
- Tests green on every supported platform and Swift version.
- Tag, push the tag, create the GitHub release.
- Submit to the Swift Package Index (or confirm it picked the package up).
- For binary products: update URL + checksum, test the binary target in a fresh project.
Common mistakes¶
- Public type without public initialiser — clients can't construct it.
- Adding enum cases in a minor release to a non-frozen public enum; clients with exhaustive
switchbreak (source) — document or use@frozen/@unknown defaultguidance from day one. @inlinableon code you'll want to change.- Publishing a package depending on a branch or using
unsafeFlags(SwiftPM won't allow it for dependencies). - Forgetting the runtime when shipping a Linux/Windows binary built dynamically.
- Leaking dependency types in your public API, so upgrading a dependency is a breaking change for you.
- Shipping binary frameworks without
BUILD_LIBRARY_FOR_DISTRIBUTION.
Exercises¶
- Take the
slugifylibrary from the SwiftPM lesson: document it with DocC, build the docs, and preview them. - Add a
swift-docc-plugindependency and host the docs on GitHub Pages with static hosting. - Intentionally make a breaking change (rename a public function) and run
swift package diagnose-api-breaking-changesagainst the previous tag. - Build the capstone indexer with
--static-swift-stdlibon Linux (or in theswiftcontainer) and check the dependencies withldd. - Write a GitHub Actions matrix that builds and tests on Linux, macOS, and Windows.
- Explain, for three of your own types, whether
@frozenwould be appropriate and what it would commit you to.
Recap¶
- Ship source packages by default; use binary targets only when you must.
- Follow the API guidelines, minimise public surface, document with DocC, and version semantically.
- ABI stability is the runtime contract; library evolution (
@frozen,@inlinable,.swiftinterface) is for binary frameworks that must evolve. - Ship executables statically on Linux (
--static-swift-stdlib, Static Linux SDK), with the runtime on Windows, and universal on macOS. - Automate CI, API-diff checks, and releases.