Contents

kingpin-apps/swift-package-compat-check

Run the Swift Package Index build matrix against your Swift package locally, so you don't have to push a tag and wait for SPI's CI queue to find out a platform broke.

Requirements

| Requirement | Version | Why | |-------------|---------|-----| | macOS | 15+ | spcc is a macOS-only tool. | | Xcode | 26.4+ | For the Apple-platform cells (macos-spm, macos-xcodebuild, ios, tvos, watchos, visionos). Multiple Xcodes can be selected per Swift version via --xcode-6.X. | | Container runtime | Docker (any modern release), apple/container 0.12+, or Podman | For linux, android, wasm cells. Apple cells don't need any. Default is docker; select another with --container-runtime container\|podman, or omit to auto-detect (see Container runtime below). | | Swift | 6.2+ | Only needed to build spcc from source. |


Installation

brew install kingpin-apps/tap/spcc

Or build from source with Swift Package Manager:

git clone https://github.com/Kingpin-Apps/swift-package-compat-check.git
cd swift-package-compat-check
swift build -c release
cp .build/release/spcc ~/.local/bin/spcc

Make sure ~/.local/bin is on your $PATH (or copy somewhere else that is).


Quick start

# Show the matrix that would run without actually building anything
spcc run --dry-run

# Run the full matrix against the package in the current directory
spcc run

# Run a subset for fast iteration
spcc run -p macos-spm,linux -s 6.3

# Run against a package elsewhere
spcc run --path ~/Projects/swift-nacl

# Run `swift test` per cell instead of `swift build`
spcc run --test -p macos-spm,linux -s 6.3

# Install extra system packages the tests need (only applied with --test).
# Host packages go on the Mac via brew (Apple cells); container packages go
# inside each Linux/Android/Wasm container via apt.
spcc run --test --install-host gnupg --install-container "gnupg,libgcrypt20-dev" \
  -p macos-spm,linux -s 6.3

# Run each cell's tests serially for suites that share global state (keyrings,
# ports, temp files). Distinct from --max-parallel, which bounds cell concurrency.
spcc run --test --test-no-parallel -p macos-spm,linux -s 6.3

# Load default flags from a config file (--config wins over $SPCC_CONFIG)
spcc run --config ./.spi-compat.toml
SPCC_CONFIG=~/.config/spcc.toml spcc run

When a cell fails, its log path is printed below the matrix so you can drill in:

Failed cells (1):
  ✗ wasm × Swift 6.3   /Users/me/.cache/spi-compat-check/logs/swift-nacl/.../wasm-6.3.log

Subcommands

| Subcommand | Purpose | |------------|---------| | spcc run [path] | Run the matrix (default subcommand — bare spcc is equivalent). | | spcc clean [path] | Remove the package's caches (Docker volumes + log/derived-data/cloned-packages dirs). | | spcc clean-all [--remove-images] | Wipe every spi-compat cache globally. --remove-images also drops cached SPI builder images (typically 50+ GB). | | spcc list-caches | du-style report of all caches + Docker volumes + builder images. | | spcc images [--remove] | List or remove cached SPI builder images. |

Run spcc <subcommand> --help for the full flag list.


What it actually runs

Each cell of the matrix reproduces SPI's own Build Command panel, except the container cells (Linux, Android, Wasm), which by default build natively (see below):

| Platform | Command | |----------|---------| | linux | docker run … swift:X.Y-jammy swift build (with --linux-mode spi: docker run --platform linux/amd64 … spi-images:basic-X.Y-latest swift build --triple x86_64-unknown-linux-gnu) | | macos-spm | xcrun swift build --arch arm64 | | macos-xcodebuild | xcrun xcodebuild build -scheme <s> -destination platform=macOS,arch=arm64 | | ios / tvos / watchos / visionos | xcrun xcodebuild build -scheme <s> -destination generic/platform=<SDK> | | android | docker run … swift:X.Y.Z-jammy swift build --swift-sdk aarch64-unknown-linux-android28 with the official Android Swift SDK (with --linux-mode spi: … spi-images:android-X.Y-latest …) | | wasm | docker run … swift:X.Y.Z-jammy swift build --swift-sdk swift-X.Y.Z-RELEASE_wasm with the official Wasm Swift SDK (with --linux-mode spi: … spi-images:wasm-X.Y-latest …) |

Apple cells use whichever Xcode xcode-select points at by default.

Container cells: native by default

SPI's Linux builders are amd64. Running SPI's amd64 image on an Apple Silicon Mac means x86_64 emulation, which is slow and can hang outright: a SwiftPM dependency checkout has been seen sitting at 0% CPU for hours. So container cells default to --linux-mode native: the official swift image at your machine's own architecture. It's the same Swift release on the same Ubuntu, without emulation.

Android and Wasm cells run the official image pinned to the exact patch release their Swift SDK was built for (a Swift SDK only works with that compiler), and install the SDK on first use:

| Swift | Android SDK | Wasm SDK | |-------|-------------|----------| | 6.1 | finagolfin/swift-android-sdk 6.1.3 | swiftwasm 6.1 | | 6.2 | finagolfin/swift-android-sdk 6.2 | swift.org 6.2.4 | | 6.3 | swift.org 6.3.3 | swift.org 6.3.3 | | 6.4 | swift.org 6.4.0 | swift.org 6.4.0 |

Every download is checked against a pinned SHA-256. Android SDKs also need the Android NDK (r27d); spcc extracts only the parts the SDK uses, so Google's x86_64-only Linux NDK works on arm64 too. SDKs, the NDK and downloads live in one shared Docker volume, spi-compat-sdk-cache, so only the first cell pays for them. Installed, each Swift version takes about 1.5 GB (Android ~1.2 GB, Wasm ~0.4 GB), plus 750 MB for the NDK: about 6.6 GB for 6.1–6.4. spcc clean-all removes it.

Use --linux-mode spi (or linux_mode = "spi" in the config) when you need SPI's exact result: its amd64 basic, android and wasm images, --platform linux/amd64 and (for Linux) --triple x86_64-unknown-linux-gnu, as in earlier spcc versions. The run header shows which mode each run used.

Note (October 2026): registry.gitlab.com/swiftpackageindex/spi-images no longer allows anonymous pulls, so --linux-mode spi only works with images you already have, or with --linux-image-X.Y / --android-image-X.Y / --wasm-image-X.Y pointing at images you can pull. A refused pull fails the cell with a message saying so.

What native mode doesn't reproduce:

  • SPI's preinstalled C libraries. SPI's basic image ships libsodium-dev, libsqlite3-dev, libjemalloc-dev and libcurl4-openssl-dev; the official image doesn't. A package that links one of those builds on SPI but fails natively with Could not find <library>. Check it with --linux-mode spi, or point --linux-image-X.Y at an image that adds them.
  • Architecture-specific code. Native builds on Apple Silicon are arm64. Code with an x86_64-only path (e.g. an assembly fast path in a C dependency) is only exercised by --linux-mode spi.

Native and SPI builds keep separate build volumes (…-X.Y-native vs …-X.Y), so arm64 and amd64 build output never mix.

For full documentation including all flags, caching behaviour, and troubleshooting, see the SwiftPackageCompatCheck DocC catalog.


Container runtime

Linux / Android / Wasm cells dispatch through a host-side container runtime — one of docker, container (apple/container, Apple Silicon, Virtualization.framework), or podman. Select one explicitly, or omit the flag to auto-detect whichever is running (priority: container → docker → podman):

# CLI flag
spcc run --container-runtime container   # or docker, or podman

# Or persist in .spi-compat.toml
container_runtime = "container"

apple/container support is experimental. It reuses the same SPI builder images and produces identical pass/fail results in spcc's smoke tests, but it hasn't yet been validated against as wide a range of real-world packages as Docker — which is why Docker stays the default.

Things to know when using apple/container:

  • Image pulls happen as an explicit pre-step (apple/container has no --pull on run); concurrent cells share a single pull.
  • apple/container caps per-container memory at 1 GB by default, which OOM-kills any non-toy Swift build. spcc raises this to 8 GB per cell so builds get a comparable allotment to Docker Desktop's VM.
  • Container cells have a 60-minute limit and fail after 15 minutes without output by default (see Limits below), so a stuck cell fails rather than sitting indefinitely. Tune with --timeout / --stall-timeout.

Podman is a Docker-compatible alternative and shares Docker's code path (inline --pull, ps --filter label-based timeout kill, volume/images verbs). It's a good fit for light packages and smoke checks, but not for heavy real-world matrices — see the caveats below. Things to know:

  • Memory / concurrency. Podman runs all cells in a single podman machine VM, and spcc runs the matrix cells concurrently, so they share that one VM's RAM (unlike apple/container, which gives each cell its own VM). spcc sets no per-cell cap for Podman, so a matrix of N concurrent heavy cells will OOM unless the machine has roughly N × 6 GiB. Size it with podman machine init/set -m <MiB> (the 2 GiB default OOMs even one real build), or serialize with --max-parallel 1.
  • No Rosetta in containers → slow amd64. This applies to Android / Wasm cells and to --linux-mode spi; native Linux cells run arm64 and aren't affected. Even on the applehv provider with rosetta=true, Podman does not mount Rosetta into podman run --platform linux/amd64 containers, so amd64 Swift compilation runs under qemu — measured ~5–6× slower than apple/container. Heavy packages (e.g. a full Cardano-stack build) time out at 1800s under Podman where apple/container finishes in ~5 min. Lower --timeout for Podman so a slow cell fails fast instead of grinding.
  • Public-registry pulls. If Podman inherits a credsStore/credHelpers from ~/.docker/config.json, pulls of the public SPI registry can fail with a credential-helper error — bypass with an empty REGISTRY_AUTH_FILE (see the DocC Troubleshooting guide).

Caches

spcc keeps a stable per-package cache so repeat runs are fast:

~/.cache/spi-compat-check/
├── derived-data/<pkg>/<platform>-<sv>/    ← xcodebuild incremental state
├── cloned-packages/<pkg>/                 ← SourcePackages cache shared across xcodebuild cells
└── logs/<pkg>/<RUN_TS>/<platform>-<sv>.log

Plus Docker volumes named spi-compat-build-<pkg>-<sv> (Linux) and spi-compat-build-<pkg>-<platform>-<sv> (Android/Wasm) holding the cross-SDK scratch path.

Logs auto-trim to the latest 5 runs per package. Use spcc list-caches to see total disk usage, spcc clean <pkg> to drop one package's caches, spcc clean-all to nuke everything.

Override the cache root with SPI_COMPAT_CACHE=/custom/path spcc run.


Features

  • Live updating matrix under a TTY — cells transition ? → ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏ (braille spinner) → ✓/✗ in place, no scrollback.
  • Scheme auto-detection via swift package dump-package — skips system C targets like Clibsodium that alphabetically win against the real Swift library product.
  • Per-Swift-version overrides — --xcode-6.X, --toolchain-6.X, --linux-image-6.X, --android-image-6.X, --wasm-image-6.X, --wasm-sdk-url-6.X.
  • Bounded concurrent fan-out — --max-parallel N runs cells in parallel within each Swift version. Defaults to activeProcessorCount / 2.
  • Test-dependency installs — --install-host (brew, on the Mac for Apple cells) and --install-container (apt, inside each Linux/Android/Wasm container) pull in system packages a package's tests need — e.g. gpg for swift-gnupg. Applied only with --test. Host installs run once and persist on your machine; container installs are ephemeral. Both accept a comma-separated list and are validated against shell injection.
  • Serial test execution — --test-no-parallel runs each cell's tests serially (swift test --no-parallel / xcodebuild test -parallel-testing-enabled NO) for suites that share global state. Orthogonal to --max-parallel, which bounds how many cells run at once.
  • Limits for container cells — each Linux / Android / Wasm cell is killed after 60 minutes (--timeout SECONDS) or after 15 minutes without any output (--stall-timeout SECONDS); 0 turns either off. A hung cell fails with "timed out" or "stalled" instead of blocking the run. Apple cells have no limit.
  • Disk-full diagnosis — a cell that fails because the container runtime ran out of disk says so, with the cleanup commands, instead of a bare exit code.
  • Native Android / Wasm — official Swift SDKs on the official image, at your machine's architecture, cached in one shared volume (see Container cells: native by default).
  • Qemu IPC retry (--linux-mode spi) — the SPI-image resolver detects transient "failed parsing the Swift compiler output" errors under qemu emulation and retries the build before falling back.
  • Single-triple builds — when an Android SDK bundle ships multiple triples, spcc builds the one triple matching SPI's intent rather than every architecture in the bundle.

Development

just test           # Unit tests
just hello          # Smoke-test spcc against the bundled HelloWorld fixture
just hello-full     # Full 34-cell matrix against HelloWorld (slow on first run)
just build          # Debug build
just release        # Release build for current host arch
just bump           # Cut a release: `cz bump` then push --follow-tags

The DocC catalog doubles as the full user guide and the API documentation for the SwiftPackageCompatCheck library target.


License

MIT. See LICENSE.

Package Metadata

Repository: kingpin-apps/swift-package-compat-check

Default branch: main

README: README.md