Skip to content

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:), not insertObject(_: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: internal by default; package for sharing between your own targets; public only on purpose.
  • Make public structs Sendable where truthful; add public inits explicitly (synthesized initialisers are internal).
  • Use final on public classes unless subclassing is a designed extension point (open means: 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:

swiftc -enable-library-evolution -emit-module-interface-path MyLib.swiftinterface ...

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

swift build -c release --arch arm64 --arch x86_64      # universal binary

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:

swift build -c release --static-swift-stdlib

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

swift build -c release

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

  1. All public API documented; docs build without warnings.
  2. swift package diagnose-api-breaking-changes <last-tag> is clean for minor/patch releases.
  3. CHANGELOG entry; semantic version chosen deliberately.
  4. Tests green on every supported platform and Swift version.
  5. Tag, push the tag, create the GitHub release.
  6. Submit to the Swift Package Index (or confirm it picked the package up).
  7. 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 switch break (source) — document or use @frozen/@unknown default guidance from day one.
  • @inlinable on 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

  1. Take the slugify library from the SwiftPM lesson: document it with DocC, build the docs, and preview them.
  2. Add a swift-docc-plugin dependency and host the docs on GitHub Pages with static hosting.
  3. Intentionally make a breaking change (rename a public function) and run swift package diagnose-api-breaking-changes against the previous tag.
  4. Build the capstone indexer with --static-swift-stdlib on Linux (or in the swift container) and check the dependencies with ldd.
  5. Write a GitHub Actions matrix that builds and tests on Linux, macOS, and Windows.
  6. Explain, for three of your own types, whether @frozen would 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.

Next: Capstone: parallel file indexer →