Files
obsidian-vault/personal/docs/Swift cross-platform testing.md
T
2026-05-29 14:03:13 +00:00

5.9 KiB
Raw Blame History

# Conversation Summary

## Main Topic
Exploration of Swift development across Windows, macOS, and Linux/Armbian — including:
- Managing multiple Swift toolchains & PATH on Windows
- Swift timers (Foundation.Timer, GCD timers, asyncAfter loops)
- Swift Package Manager configuration
- Cross-compiling Swift applications and tests to Linux (incl. Static Linux SDK)
- Swift Concurrency examples (tasks, MainActor hops, async URL fetches)

---

## Key Decisions & Findings

### Swift on Windows
- Changing default toolchain requires editing **PATH** manually.
- To view PATH:
  - PowerShell: `$env:Path -split ';'`
  - CMD: `echo %PATH%`
- System PATH: `[Environment]::GetEnvironmentVariable("Path","Machine")`
- Uninstall Swift:
  - Remove toolchain folders.
  - Clean PATH entries.

### Timer & GCD Behavior on Windows
- `DispatchSource.makeTimerSource()` **crashes with illegal instruction** on Windows 10 → avoid.
- Use **`DispatchQueue.main.asyncAfter` loop** or **Foundation.Timer** instead.

### Swift Package Manager
- `platforms:` in `Package.swift` **only supports Apple platforms**.
- Linux/Windows builds **must omit** the `platforms` constraint.
- Can still use `.when(platforms: [.linux, .windows])` for conditional build flags.

### Swift Cross-Compilation
- Swift supports **cross-compiling from macOS to Linux** via the Static Linux SDK.
- Static builds produce **large binaries** (40150MB), can be reduced with:
  - `-Osize`
  - `--gc-sections`
  - `strip` / `llvm-strip`
  - avoiding Foundation where possible
- Dynamic builds (glibc) are much smaller but require Swift runtime on target.

### Swift Testing Framework
- Yes — **Swift Testing** tests can be cross-compiled and shipped to run on Linux **without Swift installed** using the Static SDK:

swift build --build-tests --swift-sdk aarch64-swift-linux-musl -c release

- Run the produced test binary directly on the target machine.

---

## Important Code Created

### 1. GCD asyncAfter repeating loop (Windows-safe)
```swift
import Foundation
import Dispatch

func startRepeating(on queue: DispatchQueue, interval: TimeInterval) {
  queue.asyncAfter(deadline: .now() + interval) {
      print("[tick] \(Date())")
      startRepeating(on: queue, interval: interval)
  }
}

print("Start…")
startRepeating(on: .main, interval: 1.0)
RunLoop.main.run()


2. Full Swift Concurrency test app (tasks, hops, sleep, throwing)

enum DemoError: Error, CustomStringConvertible {
    case boom(id: String)
    var description: String { "DemoError.boom(\(id))" }
}

func sleepSeconds(_ seconds: Double) async throws {
    #if compiler(>=6.0)
    try await Task.sleep(for: .seconds(seconds))
    #else
    try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
    #endif
}

func runScenario(id: String, shouldThrow: Bool) async -> Result<Int,Error> {
    await MainActor.run { print("[\(id)] start on MainActor") }

    let worker = Task.detached(priority: .background) { () throws -> Int in
        print("[\(id)] background: begin")
        try await sleepSeconds(1)
        print("[\(id)] background: after 1s")

        if shouldThrow { throw DemoError.boom(id: id) }

        try await sleepSeconds(1)
        print("[\(id)] background: after 2s")
        return id.unicodeScalars.reduce(100) { $0 + Int($1.value) }
    }

    let result = await worker.result
    await MainActor.run { print("[\(id)] result: \(result)") }
    return result
}

@main
struct App {
    static func main() async {
        let t1 = Task { await runScenario(id:"A", shouldThrow:false) }
        let t2 = Task { await runScenario(id:"B", shouldThrow:true) }
        print(await t1.value, await t2.value)
    }
}


3. Enhanced concurrency test with real URLSession async fetches

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif

enum DemoError: Error, CustomStringConvertible {
    case boom(id: String)
    var description: String { "DemoError.boom(\(id))" }
}

func runScenario(id: String, shouldThrow: Bool) async -> Result<Int,Error> {
    await MainActor.run { print("[\(id)] start") }

    let worker = Task.detached(priority: .background) { () throws -> Int in
        try await Task.sleep(nanoseconds: 1_000_000_000)
        let session = URLSession(configuration: .ephemeral)

        let (data1, _) = try await session.data(from: URL(string:"https://example.com")!)
        if shouldThrow {
            _ = try await session.data(from: URL(string:"https://invalid.invalid")!)
        }

        try await Task.sleep(nanoseconds: 1_000_000_000)
        let (data2, _) = try await session.data(from: URL(string:"https://worldtimeapi.org/api/timezone/Etc/UTC")!)

        return data1.count + data2.count
    }

    let result = await worker.result
    await MainActor.run { print("[\(id)] -> \(result)") }
    return result
}

@main
struct App {
    static func main() async {
        let r1 = await Task { await runScenario(id:"A", shouldThrow:false) }.value
        let r2 = await Task { await runScenario(id:"B", shouldThrow:true) }.value
        print(r1, r2)
    }
}


4. Minimal cross-platform Package.swift

// swift-tools-version: 5.10
import PackageDescription

let package = Package(
    name: "MyApp",
    products: [ .executable(name: "MyApp", targets: ["MyApp"]) ],
    targets: [
        .executableTarget(
            name: "MyApp",
            swiftSettings: [ .unsafeFlags(["-Osize"], .when(configuration: .release)) ],
            linkerSettings: [ .unsafeFlags(["--gc-sections"], .when(platforms: [.linux])) ]
        )
    ]
)


Unresolved / Open Items

  • Detailed example of dynamic-linking cross-compile workflow (glibc) if reducing binary size further is needed.

  • Automatic helper for selecting Swift toolchains and PATH switching on Windows.

  • Setting up CI to run cross-compiled Swift Testing binaries on remote Linux/ARM targets.