Ensuring Swift Compatibility on Linux
Table of Contents
Ensuring Swift Compatibility on Linux
Swift has long had a reputation for being that iOS language, but the truth is that Swift has had cross-platform Linux support for nearly a decade. It’s robust and battle-tested in production, powering web backends, microservices, and command-line tools. That being said, like any platform, Linux has its own quirks and best practices that differ slightly from development on Apple platforms. This guide covers essential tips and strategies to ensure your Swift projects run smoothly on Linux.
This guide will focus on best practices for Swift on Linux projects using Swift 6 and later. If you’re using Swift 5.x, some details may differ, especially around Foundation modularization. But the core principles still apply.
Why Swift on Linux?
- Swift on the Server – Frameworks like Vapor and Hummingbird run natively on Linux and deliver high-performance server applications.
- Swift in Docker – Official Swift Docker images allow you to build and run Swift apps in small, portable containers.
- Swift in CI/CD Pipelines – Linux-based GitHub Actions runners are significantly cheaper than macOS runners and are ideal for automated testing.
Cross-Platform Logging
❌ Avoid OSLog for Cross-Platform Projects
OSLog and the macOS unified logging system are Apple Platform-only.
✅ Use swift-log
Swift’s open-source logging API works across macOS, Linux, and Windows.
- Import as a SwiftPM dependency.
- Supports log levels, metadata, and custom back-end providers.
- Ideal default for any cross-platform Swift service or tool.
Foundation on Linux: What Changed?
As Swift developers, we’re spoiled. Swift ships with a powerful Foundation framework which provides essential data types, collections, and utilities. In fact, they are so useful and ubiquitous that they feel like they are a core part of the language itself. But Foundation is a massive framework. On Apple platforms, this doesn’t matter because Foundation ships as part of the OS, but on Linux, your application must bundle Foundation. In the past, this was an all-or-nothing choice: you either imported the entire Foundation framework, or you didn’t use it at all, and lose so much of what makes Swift great.
But Apple has greatly improved this story with two major developments:
- Modularized Foundation packages – Foundation is now split into smaller modules that can be imported individually, reducing unnecessary bloat.
- Swift-native Foundation rewrite – Foundation was originally created over 30 years ago, long before Swift existed, and was written in Objective-C. But the Linux version of Swift has no Objective-C runtime. This meant that many Foundation APIs had the same interface but different behavior on Linux, and some APIs were missing entirely.
Swift 6 modularized Foundation and reduced reliance on the Objective-C runtime. Apple is actively rewriting Foundation in pure Swift, which:
- Improves Linux compatibility (no ObjC runtime required).
- Improves performance.
- Reduces platform divergence between macOS and Linux.
What does this mean for you? Instead of importing the entire Foundation framework, you should now import only the specific Foundation modules you need. While on Apple platforms this doesn’t matter (since Foundation is part of the OS), on Linux this drastically reduces your app’s size and startup time.
Foundation Modules at a Glance (Swift 6+)
| Module | What It Contains / When to Use It | Notes for Linux Compatibility |
|---|---|---|
| FoundationEssentials | Core value types: Data, Date, URL, UUID, predicates, formatting protocols, time handling. | Great default for most cross-platform apps. Lightweight; no ObjC bridging. |
| FoundationInternationalization | Localization: date/number formatting, calendars, time zones, measuring units (ICU-backed). | Needed for user-facing locale formatting. Bigger dependency footprint. |
| FoundationNetworking | URLSession, URLRequest, HTTPURLResponse, cookies. | Required for all networking on Linux (not included in Essentials). |
| FoundationXML | XML parsing (XMLDocument, XMLParser). | Uses libxml2 on Linux. Import only if you need XML support. |
| FoundationObjCCompatibility | ObjC-bridged APIs: NSObject, KVC/KVO, some legacy classes. | Avoid in cross-platform code. Linux has no ObjC runtime. |
#if canImport(Darwin)
// Darwin means Apple platforms (macOS, iOS, etc.)
import Foundation
#else
// non-Apple platforms
import FoundationEssentials // Only import parts you need, leave out parts you don't
#endif
Linux-Specific Differences You Should Know
No Apple Frameworks – Avoid UIKit, AppKit, SwiftUI, CoreData, etc. Detect macOS with
#if os(macOS)or prefer the more flexible#if canImport(Darwin)when checking for Apple platforms broadly. For example:#if os(macOS) // macOS-specific code #endif #if canImport(Darwin) // Any Darwin platform: macOS, iOS, watchOS, tvOS #endifCase-Sensitive Filesystems – macOS often uses case-insensitive filesystems; Linux does not. This means
MyFile.swiftandmyfile.swiftare treated as different files on Linux but may conflict on macOS. Always use consistent casing in your imports and file references.File Permissions – Linux enforces POSIX file permissions strictly. Usew FileManager APIs or POSIX functions to set executable bits and ownership explicitly, as defaults may differ from macOS.
No Objective-C Runtime – Linux builds cannot use KVC, KVO,
NSObject, or Cocoa frameworks.Line Endings – macOS and Linux both use
\n. (Differences only arise when dealing with Windows files.)Process & Shell Differences – Environment variables and path resolution may differ across systems.
Use Docker for Local Linux Testing
Docker makes Linux testing extremely reliable without requiring a Linux machine or VM.
Quick One-Off Test
Test whether your project builds on Linux instantly, without installing Swift locally:
docker run --rm -v "$PWD":/host -w /host swift:6.2-jammy swift build
What each flag means:
docker run— Start a temporary container.--rm— Delete the container when finished (no cleanup required).-v "$PWD":/host— Mount your current directory into the container at/hostso Docker can access your project.-w /host— Set the working directory inside the container to your mounted folder.swift:6.2-jammy— Use the official Swift 6 Linux image based on Ubuntu Jammy.swift build— Run the Swift compiler inside the Linux environment, ensuring true cross-platform compatibility.
This is perfect for a quick sanity check before pushing to CI.
Production Dockerfile Setup
For consistent builds in CI/CD or when deploying to servers, create a Dockerfile in your project root with something like this:
FROM swift:6.0 as build
WORKDIR /app
COPY . .
RUN swift build -c release
Build the Docker image:
docker build -t my-swift-app .
Run the container:
docker run my-swift-app
This compiles your Swift package inside a Linux container and executes the resulting binary. Use this workflow to validate Linux compatibility even if your host machine is macOS—your source is built and executed by actual Linux Swift toolchains.
For development: Open a shell inside the container to run commands manually:
docker run -it --rm -v $(pwd):/app swift:6.0 bash
This allows your project to build consistently regardless of the host environment, ensuring reproducibility and simplifying setup for contributors.
Use Linux GitHub Actions for CI
Linux runners are fast, available, and 10× cheaper than macOS runners.
Minimal Swift CI Setup:
name: Linux Swift CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: swift-actions/setup-swift@v2
with:
swift-version: "6.0"
- name: Build
run: swift build --enable-testing
- name: Test
run: swift test
Quick Checklist
Now that you understand the key differences and best practices for Swift on Linux, here’s a handy checklist to ensure your project is fully compatible:
- Use modern Swift 6 strict concurrency – Audit your code for
@MainActor,Sendable, and data races to ensure thread safety across platforms. - Remove Objective-C runtime dependencies – Avoid
@objc,dynamic, andNSObjectinheritance unless wrapped in#if canImport(ObjectiveC). Linux has no ObjC runtime. - Prefer modular Foundation packages – Import only what you need (
FoundationEssentials,FoundationNetworking, etc.) rather than the monolithic Foundation. - Check platform boundaries – Use
#if os(Linux)and#if canImport(Darwin)to isolate platform-specific code. - Use
swift-loginstead of OSLog – Ensures consistent logging across macOS, Linux, and Windows. - Avoid macOS-only APIs – AppKit, CoreGraphics specifics, and FileManager extensions unavailable on Linux will cause build failures.
- Ensure paths are correct – Linux uses a POSIX filesystem with strictly case-sensitive paths. Test file references on Linux to catch casing issues early.
- Confirm file permissions manually – Linux enforces POSIX permissions strictly. Use
FileManageror POSIX APIs to set executable bits and ownership if your app depends on specific permissions. - Test on Linux via Docker and CI – Use
docker runfor quick local validation and GitHub Actions Linux runners for automated testing. Both are faster and cheaper than macOS alternatives. - Validate system library dependencies – Ensure any C libraries your code depends on exist on Linux, or vendor them into your project.
Acknowledgments
Thanks to Swift Package Index for sharing their Docker command for running swift build on Linux!
This was written by Daniel Lyons.
If you'd like to support him, please consider buying him a coffee so he can create more content like this.