# Linux-server Swift Operational rules for PureYAML code that runs on Linux in production: HTTP client, logging, database, crypto, signals, resources, POSIX interop, file I/O, and Docker CI. Load on demand. Triggers: Linux, server, Vapor, Hummingbird, AsyncHTTPClient, swift-log, swift-crypto, FluentSQLite, FluentPostgres, NIO, SIGTERM, graceful shutdown, Docker, `swift:6.0-jammy`, `swift:6.0-noble`, Bundle.module, FoundationNetworking on a server, isatty, BoringSSL. If your target runs on Linux in production (server, CLI binary, container workload), this file is the operational rule. The higher-level topology rule and the `#if canImport(...)` patterns live in `docs/rules/cross-platform.md`; this file is the server-specific operational layer. ## When this rule applies - Building a Vapor / Hummingbird / NIO server that ships to Linux (Docker, ECS, Kubernetes). - Building a Swift CLI that supports Linux installation (Homebrew on macOS, package manager or `swift build` on Linux). - Building a server that ships cross-platform. - Any repo with a Linux-buildable product in its Package.swift (Topology 2 or 3 from `cross-platform.md`). If the answer is "Apple-only iOS/macOS app, no Linux," skip this file; load `cross-platform.md` for the Apple-cross-platform patterns instead. Note: the PureYAML engine targets macOS and Linux. Both support subprocess and shell-out, so the engine may spawn subprocesses. As a design preference, keep server-only concerns (this file's HTTP/database/signal patterns) in dedicated CLI/server targets rather than the core, so the core stays focused; this is an organizational choice, not a platform limitation. ## 1. HTTP: AsyncHTTPClient, not URLSession The canonical HTTP client on Linux server code is `AsyncHTTPClient` (swift-server). Use it on every platform for cross-platform parity, not just Linux. ```swift import AsyncHTTPClient import NIOHTTP1 let httpClient = HTTPClient(eventLoopGroupProvider: .singleton) let request = HTTPClientRequest(url: "https://example.com/api") let response = try await httpClient.execute(request, timeout: .seconds(30)) let body = try await response.body.collect(upTo: 1024 * 1024) ``` **Why not URLSession on Linux:** `FoundationNetworking`'s URLSession on Linux lacks `URLSessionWebSocketTask`, `URLSessionStreamTask`, and PAC (proxy auto-configuration). For any feature beyond plain `data(from:)`, you will hit gaps. **Why use AsyncHTTPClient on Apple too:** a single HTTP client across all platforms eliminates the entire `#if canImport(FoundationNetworking)` class of bug. The cost is one dependency. You can tune one `HTTPClient` config (HTTP/1.1 only, short idle timeout, POSIX sockets) and run the same code on macOS and Linux. **TLS:** AsyncHTTPClient ships bundled BoringSSL on Linux; on Apple it uses Security.framework transparently. No source-level branching needed. If you must use URLSession (existing library code, an Apple-specific feature), see `cross-platform.md` Pattern 10 for the `canImport(FoundationNetworking)` form. **URLSession lifetime on Linux (FoundationNetworking):** `URLSession.deinit` asserts that the session was invalidated before release. On Apple platforms this is advisory; on Linux it crashes with "Object deallocated with non-zero retain count" at process exit. Always invalidate before the session goes out of scope: ```swift // Wrong: session deallocated without invalidation, Linux crash let session = URLSession(configuration: .ephemeral) // ... use session ... // session goes out of scope here → crash // Correct option 1: final class with deinit (preferred, automatic for all callers) public final class PureYAMLloader: @unchecked Sendable { private let session: URLSession init() { session = URLSession(configuration: .ephemeral) } deinit { session.invalidateAndCancel() } } // Correct option 2: explicit invalidation at call site defer { session.invalidateAndCancel() } ``` Use `invalidateAndCancel()` rather than `finishTasksAndInvalidate()` when all tasks are already done. `invalidateAndCancel` sets `invalidated = true` synchronously, whereas `finishTasksAndInvalidate` does so async on the work queue and may not run before process exit. Sessions created with a delegate (e.g., for TLS handling) are especially prone to this: the session retains the delegate, and the retain cycle is only broken on invalidation. ## 2. Logging: swift-log on the server, `canImport(os)` only for local dev Two-tier strategy: **Server tier (Vapor / Hummingbird / NIO target):** use `swift-log` (the `Logging` package) exclusively. Bootstrap once at startup: ```swift import Logging LoggingSystem.bootstrap { label in var handler = StreamLogHandler.standardError(label: label) handler.logLevel = .info return handler } let logger = Logger(label: "com.example.tiledown") logger.info("server starting") ``` In production, redirect stderr to journald / Docker logs / your hosting platform's log sink. **App tier (iOS / macOS UI):** wrap `os.log` with a `canImport(os)` conditional and a print fallback: ```swift #if canImport(os) import os.log public struct TileLogger { private let logger: Logger public init(subsystem: String, category: String) { self.logger = Logger(subsystem: subsystem, category: category) } public func info(_ message: String) { logger.info("\(message, privacy: .public)") } public func error(_ message: String) { logger.error("\(message, privacy: .public)") } } #else public struct TileLogger { public init(subsystem: String, category: String) {} public func info(_ message: String) { print("[INFO] \(message)") } public func error(_ message: String) { print("[ERROR] \(message)") } } #endif ``` The Apple side uses unified logging (Console, Instruments). The Linux side uses print. Same call site, different sink. **Do not mix the two in the same file.** `Logger` (swift-log) and `Logger` (os.log) are different types with the same name. Trying to use both in one source file requires fully qualified names (`Logging.Logger` vs `os.Logger`) and is brittle. Pick one per target. Reference: SSWG `swift-log` is the standard for the server tier. ## 3. Database: Fluent on Linux, avoid CoreData CoreData is Apple-only; do not reach for it on the server. Options on Linux: - **Vapor + Fluent + FluentSQLiteDriver** (local development) / **FluentPostgresDriver** (production), the dominant SSWG stack. - **GRDB**, a cross-platform SQLite wrapper, single binary, no actor abstractions. - **swift-sqlite** (the raw `sqlite3` C module imported via Swift), the lowest level, lowest overhead. A good fit for FTS5 search indexes. **CloudKit, iCloud Drive, NSPersistentCloudKitContainer:** Apple-only. For Linux backends that need sync, build it explicitly over your chosen database driver. ## 4. Cryptography: swift-crypto cross-platform, CryptoKit Apple-only The standard conditional: ```swift #if canImport(CryptoKit) import CryptoKit #else import Crypto #endif ``` This imports correctly on both platforms. But the APIs are NOT fully source-compatible. Real gotchas: - **`SHA256.hash` vs `SHA256.digest`**: CryptoKit exposes `Digest` types differently. The hash methods return `Digest` on CryptoKit and `Digest` on Crypto, but their `description`, `Sequence` conformance, and bytewise iteration can differ at the protocol level. Test both sides. - **`SecureEnclave`** does not exist in swift-crypto. Hardware-backed keys are Apple-only. - **`SymmetricKey(data:)`** is identical on both. `ChaChaPoly`, `AES.GCM`, `HKDF`, `Curve25519` are all source-compatible. - **`P256.Signing.PrivateKey.publicKey.derRepresentation`** is identical. When in doubt: write the code against `Crypto` first (smaller surface, cross-platform), only reach for `CryptoKit`-specific APIs (SecureEnclave, biometric-backed keys) at Apple-only call sites with explicit `#if canImport(CryptoKit)` guards. ## 5. Graceful shutdown: SIGTERM grace then SIGKILL Server containers receive SIGTERM on shutdown (Docker stop, Kubernetes pod termination, systemd stop). The correct response: stop accepting new work, drain in-flight requests, exit cleanly. Vapor and Hummingbird handle the basic SIGTERM hook automatically; your server's `application.run()` returns when SIGTERM arrives. For multi-process supervisors (a parent that forks server children), the reaper pattern: ```swift #if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #endif func reapChild(pid: pid_t, graceSeconds: Double = 2.0) { _ = kill(pid, SIGTERM) let deadline = Date().addingTimeInterval(graceSeconds) while Date() < deadline { var status: Int32 = 0 if waitpid(pid, &status, WNOHANG) == pid { return } // exited cleanly Thread.sleep(forTimeInterval: 0.1) } _ = kill(pid, SIGKILL) _ = waitpid(pid, nil, 0) } ``` POSIX signal semantics are identical on Linux and Darwin (sigaction, signal handlers, realtime signals). The wrapper is for the `Darwin` vs `Glibc` import only. **Gotcha:** `proc_pidpath()` is Darwin-only. To resolve a Linux child's binary path, read `/proc/$pid/exe` as a symlink. Do not assume `proc_pidpath` works cross-platform. **For Vapor servers specifically:** Vapor calls `application.shutdown()` on SIGTERM, which drains the event loop group. Do not reinvent. For Hummingbird, the equivalent is `application.shutdownGracefully()`. ## 6. Resources: compile to Swift, do not rely on `Bundle.module` `Bundle.module` is Apple-only as a fully reliable API. On Linux SPM, `Bundle.module` either does not exist or returns paths that do not match expectations. On macOS with Homebrew installs, the binary symlink trap breaks `Bundle.module` lookups even on Apple. Both failure modes have the same fix: compile resources as Swift code. The pattern (in a `Resources` target): ```swift // Embedded/Static.swift (generated) extension Embedded { static let initialSeedJSON: String = """ { "key": "value", ... } """ } ``` A build-time generator script reads the resource file, escapes it as a Swift string literal, and writes the `.swift` file. At runtime, no bundle lookup, no FS access, no symlink resolution. Works identically on every platform. When this pays off: - Resource is small (< 1 MB total per file works fine in a string literal). - Resource is read-only at runtime. - Resource is needed at startup (no lazy disk access). - You are shipping a binary to multiple package managers (Homebrew, apt, MSI) and do not want to debug each one's resource-path semantics. When to NOT use: - Large binary assets (images, sound, large CSV). Use SPM's `resources: [.process("...")]` and read at runtime; accept the platform-specific resource lookup. - Resources that change per-installation (user data, downloaded content). ## 7. POSIX C interop: Darwin / Glibc / Musl For raw POSIX calls (read, write, ioctl, signal, kill, waitpid, dup2, pipe), the canonical conditional: ```swift #if canImport(Darwin) import Darwin #elseif canImport(Glibc) import Glibc #elseif canImport(Musl) import Musl #endif ``` Musl is reserved for Alpine Linux containers; most production deployments use glibc (Ubuntu, Debian, Amazon Linux 2, RHEL). Test on Musl only if your container base is Alpine. **Modern alternative, `apple/swift-system`:** ```swift import SystemPackage let fd = try FileDescriptor.open("/path", .readOnly) defer { try? fd.close() } let bytesRead = try fd.read(into: buffer) ``` `swift-system` provides a unified Swift surface for POSIX file I/O, file paths, errno handling. Use it for new code instead of raw Darwin/Glibc imports. Reference: SYS-0008 (`CInterop.stat` back-deploy), SE-0063, SE-0208. **When to reach for raw imports:** legacy code, operations not covered by `swift-system` (signal handlers, exec / posix_spawn, terminal ioctls). ## 8. FileHandle write API diverges `FileHandle.write(_:)` has different signatures across platforms: - macOS / iOS: `func write(_ data: Data)`, non-throwing, never actually fails - Linux (swift-corelibs-foundation): `func write(contentsOf: Data) throws`, throws on error Code that compiles on one platform may not compile on the other. The wrapper: ```swift extension FileHandle { func write(contentsOf data: Data) throws { #if canImport(Darwin) write(data) #else try write(contentsOf: data) #endif } } ``` Use this wrapper anywhere FileHandle writes are needed in cross-platform code. Apple platforms swallow errors silently (consistent with their non-throwing API); Linux propagates. **Alternative:** switch to `swift-system`'s `FileDescriptor.write(_:)`, which has the same throwing signature on every platform. ## 9. Piped stdio detection: `isatty()` When a CLI binary is piped (shell pipeline, log collector, host process), interactive prompts and TUI escape sequences must be suppressed. Standard POSIX check, works identically on Linux and Darwin: ```swift import Foundation let isPiped = isatty(STDOUT_FILENO) == 0 if isPiped { // protocol mode: structured output only } else { // interactive mode: colours, prompts, redraws } ``` Use this to disable terminal logging when the binary is invoked by another process rather than an interactive shell. ## 10. Test framework on cross-platform server code XCTest works on Linux (auto-discovery since Swift 5.4; no `LinuxMain.swift` required). Swift Testing is platform-agnostic by design. For new tests in any Linux-buildable target, default to Swift Testing. ```swift import Testing @Test func serverHealthEndpointReturns200() async throws { let response = try await client.execute(.health) #expect(response.status == .ok) } ``` Mixed XCTest / Swift Testing in the same target is fine; both runners coexist. See `concurrency.md` for the broader rule. References: ST-0009, ST-0016, ST-0017, ST-0022 (Swift Testing parity across Apple, Linux, Windows). ## 11. CI for Linux-buildable server products Every Linux-buildable product runs a Linux build on every PR. Minimum GitHub Actions job: ```yaml linux-build: runs-on: ubuntu-latest container: swift:6.0-jammy steps: - uses: actions/checkout@v4 - run: swift build -c release --product ``` For testing, add a parallel job that runs `swift test --filter ` against the Linux-buildable test suites. **Docker image choice:** - `swift:6.0-jammy`, Ubuntu 22.04 LTS, the default for most production deploys. - `swift:6.0-noble`, Ubuntu 24.04 LTS, newer. - `swift:6.0-jammy-slim`, runtime-only base for the production stage. - `swift:6.0-bookworm`, Debian 12, for some legacy deploys. - `swift:6.0-alpine`, Alpine + Musl, smallest image. Test Musl conditionals if you use this. **Production deployment via Docker:** ```dockerfile FROM swift:6.0-jammy as build WORKDIR /src COPY . . RUN swift build -c release --product tiledownserver FROM swift:6.0-jammy-slim COPY --from=build /src/.build/release/tiledownserver /usr/local/bin/ EXPOSE 8080 CMD ["tiledownserver"] ``` Multi-stage builds keep the runtime image small. The Docker production path can catch catastrophic failures, but it is not a substitute for PR-time Linux CI; deploy-time discovery means broken builds reach main. ## 12. Manifest discipline (Topology 2: Apple clients + Linux server) If your repo is the Apple-clients-plus-Linux-server split (`cross-platform.md` Topology 2), the manifest pattern from `cross-platform.md` Layer A applies: ```swift let baseProducts: [Product] = [ .singleTargetLibrary("PureYAML"), .singleTargetLibrary("TileServer"), .executable(name: "tiledownserver", targets: ["TileServerApp"]), // ... unconditional, Linux-buildable ] #if os(iOS) || os(macOS) let appleOnlyProducts: [Product] = [ .singleTargetLibrary("PureYAMLUI"), .singleTargetLibrary("PureYAMLComponents"), // ... Apple-only UI ] #else let appleOnlyProducts: [Product] = [] #endif let products = baseProducts + appleOnlyProducts ``` Same shape for targets. Server-side products and targets stay unconditional. UI-side wraps in the conditional array. Document the Linux-buildable surface in the repo's `README.md`: name the product (`tiledownserver`), name the build command (`swift build -c release --product tiledownserver`), name the supported Docker image. Otherwise new developers running plain `swift build` on Linux hit Apple-target compile errors. ## 13. Cross-references - **`docs/rules/cross-platform.md`** is the higher-level topology rule and covers UI patterns + library patterns. This file is the server-specific operational layer; load both together for a server repo. - **`concurrency.md`** is platform-agnostic; Sendable + async/await behave identically on Apple and Linux. ## 14. Authoritative references - **SSWG packages:** `swift-server/async-http-client`, `apple/swift-log`, `apple/swift-crypto`, `apple/swift-system`, `apple/swift-nio`. - **Vapor / Hummingbird:** their respective package docs. - **System interop:** SE-0063, SE-0208 (System packages). SYS-0008 (`CInterop.stat` back-deploy). - **Swift Testing platforms:** ST-0009, ST-0016, ST-0017, ST-0022. - **swift-foundation architecture:** `swiftlang/swift-foundation` README for the three-tier model (Darwin / swift-foundation / swift-corelibs-foundation). ## Quick reference table | Concern | Apple-only choice | Cross-platform / Linux server choice | |---|---|---| | HTTP client | URLSession | AsyncHTTPClient | | Logging | `os.Logger` | swift-log (`Logging`) | | Crypto | CryptoKit | swift-crypto (`Crypto`) | | Database | CoreData | Fluent + SQLite/Postgres driver, GRDB, or raw `sqlite3` | | C interop | `import Darwin` | `swift-system` preferred; `canImport(Darwin/Glibc/Musl)` for legacy | | FileHandle write | `write(_:)` non-throwing | wrap with `canImport(Darwin)` (see § 8) | | Resources | `Bundle.module` | compile to Swift code | | Tests | XCTest or Swift Testing | Swift Testing (platform-agnostic) | | Shutdown | OS handles UI lifecycle | SIGTERM grace + SIGKILL | | CI | macOS only | macOS + Linux Docker on every PR | | Production runtime | App Store / Mac App | Docker container (jammy-slim base) |