
Swift Package Manager behaves the same on a rented Mac as it does on your laptop. Every swift subcommand — build, test, package resolve, package describe — is a plain command-line tool with no window-server dependency, so a Linux-first library author only needs the Mac for the Apple-platform test passes. The official reference is the Swift Package Manager documentation and Apple's integration guidance lives at Adding package dependencies to your app.
Access method matters before you wire anything up. Figment's $29/month virtual Intel macOS environment (2 vCPU, 8 GiB RAM, 80 GiB disk) is reached through a browser desktop console; inbound SSH is not provided on that VM, so you run swift inside the console terminal or push jobs from an outbound CI agent. A physical M-series Mac from $95/month is request-based — confirm SSH, chip, RAM and disk in the quote. See Figment mac hosting. The Makefile below assumes SSH is already configured on whichever host you use.
The SPM CLI has no dependency on a window server, so these commands behave the same whether you're at the keyboard or on an SSH session:
| Command | What it does |
|---|---|
swift package resolve | Fetches and pins all dependencies to Package.resolved |
swift build | Compiles all targets in debug configuration |
swift build -c release | Compiles with optimizations into .build/release/ |
swift test | Builds and runs all test targets |
swift test --parallel | Runs test cases in parallel across available cores |
swift test --filter TestSuiteName | Runs only the named suite or test method |
swift package describe --type json | Prints the package graph in JSON |
swift package show-dependencies | Prints the resolved dependency tree |
swift package generate-xcodeproj was removed from recent Swift toolchains; modern Xcode opens Package.swift directly, so a pure SPM package does not need a generated project.
Figment plan choice. The $29 virtual Intel macOS runs swift build and swift test for packages that compile with an Intel-compatible toolchain — pure Swift libraries and most macOS-framework code built against Xcode 26 or earlier (see Apple's porting guidance). Pick an M-series Mac when you need arm64 execution, Metal, or Xcode 27. The exact chip, RAM and disk at $95 are stated in the quote; ask before you buy.
On first use, swift package resolve fetches each dependency from its Git URL and writes Package.resolved. It's a one-time cost per toolchain; subsequent builds reuse the resolved graph.
ssh user@YOUR_MAC_HOST "cd ~/projects/MyLibrary && swift package resolve"
Resolved sources and compiled artifacts both live in .build/. On a persistent Mac the directory survives between sessions, so incremental builds recompile only changed sources. If you need to move the cache to a fresh host, archive it after a clean build:
# Save on the current Mac
ssh user@OLD_HOST "cd ~/projects/MyLibrary && tar czf ~/build-cache.tar.gz .build"
scp user@OLD_HOST:~/build-cache.tar.gz ~/build-caches/MyLibrary-$(date +%Y%m%d).tar.gz
# Restore on a new Mac before the next build
scp ~/build-caches/MyLibrary-20261110.tar.gz user@NEW_HOST:~/build-cache.tar.gz
ssh user@NEW_HOST "cd ~/projects/MyLibrary && tar xzf ~/build-cache.tar.gz"
A restored .build is tied to a specific Swift toolchain and target triple. If you upgrade Xcode or switch between Intel and arm64 Macs, run swift package clean first; otherwise expect spurious link errors.
swift test streams human-readable results to stdout. Recent toolchains also accept --xunit-output for CI systems that consume JUnit XML. Confirm the flag on your delivered Mac with swift test --help, since it varies by Swift release:
ssh user@YOUR_MAC_HOST \
"cd ~/projects/MyLibrary && swift test --parallel --xunit-output /tmp/results.xml"
scp user@YOUR_MAC_HOST:/tmp/results.xml ~/ci-results/macos-$(date +%Y%m%d).xml
If your toolchain does not expose --xunit-output, pipe the verbose output through xcpretty:
ssh user@YOUR_MAC_HOST \
"cd ~/projects/MyLibrary && set -o pipefail; swift test --verbose 2>&1" \
| xcpretty --report junit --output ~/ci-results/macos.xml
The remote set -o pipefail matters: without it a failing swift test can be masked by a successful xcpretty exit, and a red build would arrive dressed as green.
This Makefile is the practical artifact for the workflow. Override REMOTE_HOST and REMOTE_DIR on the command line; the exit code of swift test propagates back through the pipe so make fails when tests fail.
# Makefile — remote Swift testing on a rented Mac
# Usage:
# make test-macos REMOTE_HOST=user@mac.example.com REMOTE_DIR=/Users/user/projects/MyLibrary
REMOTE_HOST ?= user@YOUR_MAC_HOST
REMOTE_DIR ?= ~/projects/MyLibrary
LOCAL_SRC := $(shell git rev-parse --show-toplevel)
SSH := ssh -o StrictHostKeyChecking=accept-new
.PHONY: sync test-macos build-macos clean-macos sync-build-cache
# Push local source; exclude .build so the remote cache stays intact
sync:
rsync -avz --delete-excluded \
--exclude '.build' --exclude '.git' \
"$(LOCAL_SRC)/" "$(REMOTE_HOST):$(REMOTE_DIR)/"
# Resolve, build, and test on the remote Mac; stream output live
test-macos: sync
$(SSH) $(REMOTE_HOST) \
"bash -c 'set -euo pipefail; cd $(REMOTE_DIR) && \
swift package resolve && \
swift test --parallel --verbose 2>&1 | tee /tmp/swift-test-output.txt'"
# Release build without running tests
build-macos: sync
$(SSH) $(REMOTE_HOST) \
"bash -c 'set -euo pipefail; cd $(REMOTE_DIR) && swift build -c release'"
# Archive the .build directory for reuse on another host
sync-build-cache:
$(SSH) $(REMOTE_HOST) \
"cd $(REMOTE_DIR) && tar czf ~/build-cache.tar.gz .build"
mkdir -p "$(LOCAL_SRC)/build-caches"
scp "$(REMOTE_HOST):~/build-cache.tar.gz" \
"$(LOCAL_SRC)/build-caches/macos-$(shell date +%Y%m%d).tar.gz"
clean-macos:
$(SSH) $(REMOTE_HOST) \
"cd $(REMOTE_DIR) && swift package clean"
The bash -c 'set -euo pipefail; ...' wrapper invokes bash explicitly so pipefail behaves the same regardless of the remote account's login shell. zsh has been the macOS default since Catalina and its handling of pipefail inside one-shot SSH commands varies; calling bash keeps the test exit code deterministic. This example is unexecuted reference code — run it against your own project before using it in CI.
Three habits make the cache earn its keep:
rsync transfers only changed files, so a one-line commit does not re-upload the whole package..swift-version file or xcode-select -s so the same compiler produces every artifact. A mismatch silently invalidates the cache.swift build writes to .build/debug/ and swift build -c release writes to .build/release/, so you can hold both without clobbering either.Some older repos keep both a Package.swift and an .xcodeproj or .xcworkspace. xcodebuild resolves SPM dependencies at build time and caches them in DerivedData, but you can still drive dependency management from the SPM CLI:
swift package show-dependencies
swift package update SomeDependency
git add Package.resolved && git commit -m "chore: bump SomeDependency"
xcodebuild then picks up the new Package.resolved on its next invocation — no project-file edit required. For library authors chasing Linux/macOS parity, pair this SSH workflow with a Linux runner on the same commit; any test that passes both without #if canImport(Darwin) guards is a credible cross-platform signal.
Ready to point a Makefile at a real Mac? Start at /mac-hosting/start or review plans at /mac-hosting.
Related guides: Build Your First iOS App on a Rented Mac Mini · iOS SDK Upgrade on a Remote Mac · GitHub Actions Self-Hosted Runner on Mac