I've been spending the past couple of weeks working to bring Swift to FreeBSD on arm64 hardware. swift.org ships no FreeBSD arm64 binaries, and the FreeBSD ports tree only carries Swift 5.10.1 for x86. The goal is to close that gap with a fully native toolchain and a working swiftly port. Here's what got done.
---
Hardware
Target: NXP DPAA2 board running FreeBSD 15.1 aarch64. This is an embedded-class machine with limited RAM, so build jobs are serialized (-DLLVM_PARALLEL_LINK_JOBS=1 -DSWIFT_PARALLEL_LINK_JOBS=1) and lld is required throughout.
---
Phase A: Swift 6.4 native build
Starting from a Swift 6.3.2 release toolchain built in a prior phase, I bootstrapped Swift 6.4-dev natively on the board using --bootstrapping=hosttools. This mode is required since Swift 6.3+ uses macros in the stdlib, so the host and target compiler revisions must match exactly — any mismatch produces AccessorDeclSyntax.modifiers missing or @_spi inaccessible errors deep in the build.
All smoke tests now pass on the board:
swiftc 6.4-dev — Target: aarch64-unknown-freebsd15.1 ✓
Hello world compile + run ✓
Async TaskGroup concurrency ✓
Synchronization.Mutex stress (50 tasks × 10,000 ops) ✓
swift-build --build-system native ✓
---
Critical bug: Synchronization.Mutex deadlock
The most interesting find was a deadlock in Synchronization.Mutex on FreeBSD. Under contention, threads would park and never wake — the whole runtime would hang.
Root cause: The FreeBSD _Concurrency implementation was calling UMTX_OP_MUTEX_LOCK, which operates on struct umutex — a kernel mutex structure. But the Mutex storage is a raw uint32_t. The kernel put waiting threads on the wrong wait queue, so UMTX_OP_MUTEX_UNLOCK never woke them.
Fix: Replace the mutex-lock/unlock pair with an exchange-and-park pattern using UMTX_OP_WAIT_UINT / UMTX_OP_WAKE. This matches what FreeBSD's userspace threading actually expects for a raw integer futex-style wait.
One wrinkle: Mutex.lock() is marked @_alwaysEmitIntoClient, meaning the implementation is compiled into every caller's object file — not into libswift_Concurrency.so. Deploying just a fixed .swiftmodule is not sufficient. You must delete stale .o files and rebuild Foundation (and anything that depends on it) from scratch to pull in the fix. I posted the analysis and resolution at swiftlang/swift#90057 (https://github.com/swiftlang/swift/issues/90057).
---
SwiftPM on FreeBSD: two workarounds needed
Getting swift build to work required two patches to my workflow:
1. ProcessInfo.executableURL returns nil. SwiftPM uses this to locate the compiler. On Linux it reads /proc/self/exe; FreeBSD has no /proc by default. SwiftPM falls back to $PWD/swiftc. Fix: create a symlink before building and remove it after.
ln -sf $TC/bin/swiftc swiftc
"$TC/bin/swift-build" --build-system native
rm -f swiftc
2. SWBBuildService computes a wrong path. The new build system generates a double usr/usr prefix for plugin paths on FreeBSD, causing Driver threw invalid absolute path ''. The old LLBUILD backend works correctly: --build-system native.
---
Phase B: swiftly FreeBSD port (PR #565)
swiftly (https://github.com/swiftlang/swiftly) is the Swift toolchain manager. PR #565 adds FreeBSDPlatform — platform detection, toolchain install/uninstall, CA certificate verification, and correct executable naming (swiftly-aarch64-unknown-freebsd).
Getting the test suite green on FreeBSD required 28 build rounds of patches to upstream packages in .build/checkouts/. All patches are candidates for upstream PRs.
swift-nio: _NIOFileSystem module
┌───────────────────────────┬─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Issue │ Fix │
├───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ O_TMPFILE (Linux-only) │ Excluded FreeBSD from FileDescriptor.OpenOptions.temporaryFile; split block to keep currentWorkingDirectory │
│ │ (uses AT_FDCWD, available on FreeBSD) │
├───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ xattr functions │ FreeBSD uses extattr_* API; added #elseif os(FreeBSD) stubs returning -1 │
│ (flistxattr etc.) │ │
├───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ sendfile signature │ Linux: sendfile(outFD, inFD, &offset, count). FreeBSD: sendfile(inFD, outFD, offset, nbytes, nil, &sbytes, │
│ mismatch │ 0) — file/socket swapped, value offset not pointer, bytes returned via out-param │
├───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ RENAME_NOREPLACE, │ Inside #ifdef __linux__ in CNIOLinux.h; added as literal constants for FreeBSD (FreeBSD 15 supports these │
│ AT_EMPTY_PATH │ natively) │
├───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ CNIOLinux_renameat2 │ Wrapped FreeBSD 15's native renameat2 from <stdio.h> as a static inline in CNIOLinux.h │
├───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ Errno.noData (ENODATA) │ Not defined on FreeBSD; guarded case .noData: with #if !os(FreeBSD) │
├───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ fts_info type │ u_short on Linux, int on FreeBSD; added UInt16(clamping:) cast │
├───────────────────────────┼─────────────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ DT_* dirent constants │ Added || os(FreeBSD) to the Darwin/Musl Int32 branch │
└───────────────────────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
swift-nio: NIOCore / NIOPosix networking
- Moved FreeBSD from os(Linux) || os(FreeBSD) to os(OpenBSD) || os(FreeBSD) blocks across Interfaces.swift, SocketAddresses.swift, System.swift, and others — required because FreeBSD's Glibc overlay doesn't expose the same symbols as Linux's.
- FreeBSD-specific UIO_MAXIOV=1024 and SHUT_* literals (Glibc exposes these as an enum type, not CInt).
- Kqueue selector: added EVFILT_EXCEPT stub in CNIOOpenBSD.h (FreeBSD kqueue has it but the header guard differs); set isEarlyEOFDeliveryWorkingOnThisOS = false.
- CMSG shims (CMSG_SPACE, CMSG_DATA, etc.) routed through CNIOOpenBSD_CMSG_* for FreeBSD.
- BSD-style sendfile via CNIOOpenBSD.sendfile().
swift-nio-ssl: TLS CA certificate verification
This one was subtle. NIOSSL's platformDefaultConfiguration() has platform branches for Linux and Android. FreeBSD had no branch — so TLSConfiguration.makeClientConfiguration() loaded zero trust roots, and every HTTPS connection to swift.org failed with CERTIFICATE_VERIFY_FAILED.
The fix is two lines in NIOSSL:
// SSLContext.swift — platformDefaultConfiguration()
- #if os(Linux)
+ #if os(Linux) || os(FreeBSD)
And FreeBSD CA paths added to LinuxCABundle.swift (which already had #if os(Linux) || os(FreeBSD) at the file level, but no FreeBSD search paths):
"/usr/local/etc/ssl/cert.pem", // openssl port
"/etc/ssl/cert.pem", // base system (FreeBSD 14+)
"/usr/local/share/certs/ca-root-nss.crt", // ca_root_nss port
---
Test results
With internet access restored via USB ethernet (the board's DPAA2 NIC had a routing conflict), all 21 swiftly test suites pass on FreeBSD aarch64:
✔ Test run with 191 tests in 21 suites passed
PlatformTests, InstallTests, UninstallTests, LinkTests,
ListTests, UpdateTests, UseTests, HTTPClientTests … all green
---
Open PRs and upstream work
- swiftly PR #565 — FreeBSDPlatform implementation
- The swift-nio and swift-nio-ssl patches described above are ready to be submitted as upstream PRs
---
What's next
- Phase C: Rebuild Swift 6.3.2 with the Locked atomics patch and publish v0.4.2 of the FreeBSD port.
- Phase D: lang/swift64 port in FreeBSD ports tree targeting 6.4, dropping patches that landed upstream.
Happy to discuss any of the above — especially the Mutex fix and the NIOSSL trust root gap, both of which seem like they should be addressed upstream.
@networkextension
I bootstrapped Swift 6.4-dev natively on the board using --bootstrapping=hosttools. This mode is required since Swift 6.3+ uses macros in the stdlib,
The hosttools bootstrap is pretty much the only one supported at this point, as it's all anybody uses. I still maintained the cross-compile option with external patches up to 6.2, but have not updated it lately.
so the host and target compiler revisions must match exactly — any mismatch produces AccessorDeclSyntax.modifiers missing or @_spi inaccessible errors deep in the build.
Not sure what you're referring to here, the hosttools bringup natively on your AArch64 hardware should only build the stdlib and its macros with the freshly-built Swift 6.4.x compiler that matches the stdlib, not the host compiler used to build Swift libraries internal to the freshly-built Swift compiler.
Great to see you get so far, looking forward to seeing Swift listed on this page soon, once you get it into their packages.
Glad to see interest in the project.
Starting from a Swift 6.3.2 release toolchain built in a prior phase, I bootstrapped Swift 6.4-dev natively on the board using --bootstrapping=hosttools. This mode is required since Swift 6.3+ uses macros in the stdlib, so the host and target compiler revisions must match exactly — any mismatch produces AccessorDeclSyntax.modifiers missing or @_spi inaccessible errors deep in the build.
A lot of the --bootstrapping=* flags are a bit misleading and don't really work anymore.
This was the script I used to do a four-stage Swift 6.2 bootstrap to bring up the x86_64 support:
#!/usr/bin/env bash
set -ex
SOURCE_ROOT="$HOME/Swift-Project"
STAGE_ROOT="$SOURCE_ROOT/Swift-Package/Stages"
INSTALL_ROOT="$SOURCE_ROOT/Swift-Package/toolchain"
BUILD_SCRIPT="$SOURCE_ROOT/swift/utils/build-script"
mkdir -p "$STAGE_ROOT" "$INSTALL_ROOT"
"$BUILD_SCRIPT" \
--preset=bootstrap_stage0 \
build_subdir=bootstrap_stage0 \
install_destdir="$STAGE_ROOT/stage0"
export LLVM_SYMBOLIZER_PATH="$SOURCE_ROOT/build/bootstrap_stage0/llvm-linux-x86_64/bin/llvm-symbolizer"
PATH="$STAGE_ROOT/stage0/usr/bin/:$PATH" \
"$BUILD_SCRIPT" \
--preset=bootstrap_stage1 \
build_subdir=bootstrap_stage1 \
install_destdir="$STAGE_ROOT/stage1"
PATH="$STAGE_ROOT/stage1/usr/bin/:$PATH" \
"$BUILD_SCRIPT" \
--preset=bootstrap_stage2 \
build_subdir=bootstrap_stage2 \
install_destdir="$STAGE_ROOT/stage2"
PATH="$STAGE_ROOT/stage2/usr/bin:$PATH" \
"$BUILD_SCRIPT" \
--preset=freebsd_package,no_test \
install_destdir="$INSTALL_ROOT" \
installable_package="$INSTALL_ROOT/swift-main-freebsd.tar.gz"
6.2 was the last toolchain version that we ensured could bootstrap from a pure C++ environment to a fully-working, tests-passing, toolchain (did the work on Ubuntu 24.04, which we already supported to separate bootstrap failures from missing platform support. I don't believe 6.0 or 6.1 can successfully, bootstrap, and I don't know about the 6.3 release either, though we left the CI job running long enough that it might work.
The fully bootstrapped 6.2 compiler should be able to compile the main compiler and at least looking at your repo, it looks like you're cross-compiling.
Getting swift build to work required two patches to my workflow:
- ProcessInfo.executableURL returns nil. SwiftPM uses this to locate the compiler. On Linux it reads /proc/self/exe; FreeBSD has no /proc by default. SwiftPM falls back to $PWD/swiftc. Fix: create a symlink before building and remove it after.
I'm a little surprised that this workaround is still necessary. It should have been fixed by:
- SWBBuildService computes a wrong path. The new build system generates a double usr/usr prefix for plugin paths on FreeBSD, causing the Driver to throw and invalid absolute path ''. The old LLBUILD backend works correctly: --build-system native.
Do you have info on how to reproduce this? @jakepetroules, did we see anything like this thus far?
I haven't dug into the Swiftly/Swift-NIO side of things yet, so cool to see someone looking at that. I added your optimizer miscompile issue to the FreeBSD project board Swift on FreeBSD · GitHub so that we can track it.
Also surprised about ProcessInfo.executableURL; the fix is definitely in the 6.4 branch. Were you able to dig in and learn anything about why this returns nil?
As far as SWBBuildService computing a usr/usr prefix, I think I'd need more context to understand exactly where and how you're seeing this.
Thanks both — haven't had a chance to dig into either of these properly yet. The last couple of days went into getting a FreeBSD aarch64 CI runner up so I can reproduce them reliably (the board's NIC has been flaky, which slowed things down). I'll verify both on CI and follow up once I've got clean logs.
You're not gonna believe this, but…
Progress update — cross-building 6.4 for FreeBSD on macOS, and the Mutex fix verified on hardware
A few concrete steps forward since the last post.
Swift 6.4 stdlib now cross-builds for FreeBSD/aarch64 from macOS (Apple Silicon). Previously the toolchain bring-up needed an x86 FreeBSD host. I now build the full 6.4 standard library + runtime overlays (22 .so, 21 .swiftmodule, FreeBSD-15 aarch64 ELF) directly on an M-series Mac: a 6.4-dev swift-frontend as the host compiler, swift-6.3.1-RELEASE's clang 21 / ld.lld for the C/C++ + link, and a sysroot rsynced from the board. The compiler itself still runs natively on the board — this is the stdlib/runtime only.
One sharp edge worth flagging: the host frontend has to be matched to the release/6.4.x branch point (main-snapshot-2026-05-04 for me) — a newer snapshot has already dropped Builtin.cancelAsyncTask, an older one doesn't know the @diagnose attribute the 6.4 stdlib uses.
Synchronization.Mutex deadlock fix (swiftlang/swift#90143) — verified on hardware. I cross-built the stdlib with the fix and ran a contended stress test (16 threads × 100k withLock increments) on real boards: PASS, no deadlock, on both FreeBSD 14.4 and 15.1 / aarch64. The isolated cross-compile also surfaced two reasons the PR didn't actually build on FreeBSD — now folded into #90143:
_SynchronizationShims.husedNULLwithout#include <stddef.h>FreeBSDImpl.swiftcalls_spinLoopHint(), butSpinLoopHint.swiftwas registered only in the Linux source set, not FreeBSD
swiftly (swiftlang/swiftly#565). Addressed @jakepetroules's review: FreeBSD runtime deps (libuuid, python311, sqlite3) from the makePackage reference, package detection/install via pkg info -e / pkg install, and a /bin/sh shell default.
Prebuilt artifacts. The 6.4 FreeBSD/aarch64 stdlib + a ready sysroot are up as a release: networkextension/swift-freebsd → v0.6.0-6.4-macos-cross. Full recipe + patches in MACOS-CROSS-BUILD.md / patches-6.4/.
Feedback welcome — happy to open the two #90143 build fixes as a separate small PR if that's easier to review.
Amazing, I wonder whether other direction also possible