Ensuring Swift Compatibility on Linux

Topics: Swift , Linux , Cross-Platform , Docker
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.

Designed for Swift 6 and Later

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:

  1. Modularized Foundation packages – Foundation is now split into smaller modules that can be imported individually, reducing unnecessary bloat.
  2. 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.
Key Takeaway: Modularize

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+)

ModuleWhat It Contains / When to Use ItNotes for Linux Compatibility
FoundationEssentialsCore value types: Data, Date, URL, UUID, predicates, formatting protocols, time handling.Great default for most cross-platform apps. Lightweight; no ObjC bridging.
FoundationInternationalizationLocalization: date/number formatting, calendars, time zones, measuring units (ICU-backed).Needed for user-facing locale formatting. Bigger dependency footprint.
FoundationNetworkingURLSession, URLRequest, HTTPURLResponse, cookies.Required for all networking on Linux (not included in Essentials).
FoundationXMLXML parsing (XMLDocument, XMLParser).Uses libxml2 on Linux. Import only if you need XML support.
FoundationObjCCompatibilityObjC-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
    #endif
    
  • Case-Sensitive Filesystems – macOS often uses case-insensitive filesystems; Linux does not. This means MyFile.swift and myfile.swift are 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 /host so 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, and NSObject inheritance 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-log instead 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 FileManager or POSIX APIs to set executable bits and ownership if your app depends on specific permissions.
  • Test on Linux via Docker and CI – Use docker run for 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.