This is a practical, copy-paste guide to building the Swift compiler from source on NixOS
and contributing a change upstream to swiftlang/swift.
It assumes the NixOS build recipe in this repo (flake.nix + dobuild.sh) is already working.
See HACKING.md for how that recipe was built and why each piece exists.
If you are new to open source: the thing you actually "contribute" is a small change to the
swift/ source tree plus a test, sent as a GitHub pull request. Everything NixOS-specific in
this guide is about building and testing locally; it never ends up in your PR. A reviewer
cannot tell you built on NixOS.
Conventions in this doc
$ROOT= this workspace (theswift-workspacedirectory you cloned in README §1).$B= the build directory,build/Ninja-RelWithDebInfoAssert+swift-DebugAssert.- Every command runs inside the Nix dev shell. Either prefix with
nix develop --command, or runnix developonce and stay in the subshell (or letdirenventer it automatically).swift/is a full checkout ofswiftlang/swift(itsorigin).llvm-project/,swift-corelibs-foundation/, etc. are the sibling repos the build needs.
cd swift-workspace
nvim swift/lib/Sema/... # 1. edit compiler/stdlib source
nix develop -c ninja -C build/Ninja-RelWithDebInfoAssert+swift-DebugAssert/swift-linux-x86_64 bin/swift-frontend # 2. incremental rebuild
nix develop -c build/Ninja-RelWithDebInfoAssert+swift-DebugAssert/llvm-linux-x86_64/bin/llvm-lit -s build/Ninja-RelWithDebInfoAssert+swift-DebugAssert/swift-linux-x86_64/test-linux-x86_64/<YourTest>.swift # 3. run the test
git -C swift commit ... && git -C swift push fork my-branch # 4. open a PRThe rest of this document explains each step.
Everything happens inside the Nix dev shell defined by flake.nix:
cd swift-workspace
nix develop # drops you in a shell with the whole NixOS toolchain wired upYou can also prefix any single command with nix develop --command <cmd> instead of entering
the subshell. If you use direnv, the shell is entered automatically when you cd in.
ls swift llvm-project swift-corelibs-foundation swift-corelibs-libdispatch
git -C swift remote -v # origin -> git@github.com:swiftlang/swift.git
git -C swift branch --show-current # mainFork swiftlang/swift on GitHub (button top-right → creates github.com/<you>/swift), then:
git -C swift remote add fork git@github.com:<you>/swift.git
git -C swift fetch originYou now have two remotes in swift/: origin (the real Swift repo, read-only for you) and
fork (yours, where you push branches).
cd swift-workspace
nix develop --command bash dobuild.sh foundation 2>&1 | tee /tmp/swift-build.logdobuild.sh wraps swift/utils/build-script with the NixOS-specific flags the flake can't
deliver any other way (glibc sysroot, gcc-toolchain for C++ interop, corelibs link flags). A
clean run exits 0. It builds: the compiler, the standard library, C++ interop
(CxxStdlib), libdispatch, and Foundation.
If you're only changing the compiler or standard library (not Foundation), ./dobuild.sh compiler skips libdispatch/Foundation for a faster loop; see the build-target table in
README.md (§2) for which shell + command to use.
Run only ONE build at a time. Two
build-script/ninjaruns in the same build dir clobber each other. Also watch free space on/(the Nix store);nix-collect-garbage -dreclaims it.
Output lands in $B. The compiler you just built:
build/Ninja-RelWithDebInfoAssert+swift-DebugAssert/swift-linux-x86_64/bin/swiftc # and swift-frontend, etc.By default dobuild.sh sets SWIFT_INCLUDE_TESTS=FALSE (faster). To run the lit tests you need
the generated test tree, so build the tests-on variant once:
- Edit
dobuild.sh: change-DSWIFT_INCLUDE_TESTS:BOOL=FALSE→TRUE. - Force a reconfigure:
rm build/Ninja-RelWithDebInfoAssert+swift-DebugAssert/swift-linux-x86_64/CMakeCache.txt(note: this cascades a ~40 min Foundation rebuild, unavoidable and one-time). - Re-run
nix develop --command bash dobuild.sh foundation.
This generates $B/swift-linux-x86_64/test-linux-x86_64/, which mirrors swift/test/.
This is your day-to-day cycle. You do not rebuild from scratch: ninja recompiles only
what changed (and sccache caches object files across rebuilds, so a one-file change is often
under a minute).
Improve a diagnostic's wording. We changed the "assignment in a condition" error from
a boolean context to a Boolean context (matching Swift's prose convention):
1. Edit the compiler in swift/include/swift/AST/DiagnosticsSema.def:
ERROR(use_of_equal_instead_of_equality,none,
- "use of '=' in a boolean context, did you mean '=='?", ())
+ "use of '=' in a Boolean context, did you mean '=='?", ())2. Update every test that checks that message (grep finds them):
grep -rn "boolean context, did you mean" swift/test/
# update the // expected-error {{...}} text in each match(Tests: Constraints/assignment.swift, Constraints/diagnostics.swift,
Constraints/result_builder_diags.swift, decl/func/operator.swift.)
3. Rebuild just the compiler (~40 s here, thanks to sccache):
nix develop -c ninja -C build/Ninja-RelWithDebInfoAssert+swift-DebugAssert/swift-linux-x86_64 bin/swift-frontend4. Run the affected tests:
B=build/Ninja-RelWithDebInfoAssert+swift-DebugAssert
nix develop -c $B/llvm-linux-x86_64/bin/llvm-lit -s \
$B/swift-linux-x86_64/test-linux-x86_64/Constraints/assignment.swift \
$B/swift-linux-x86_64/test-linux-x86_64/Constraints/diagnostics.swift
# -> Passed: 2 (100.00%)If you mismatch the message and the test, the test fails (expected-error not found). That
is the test doing its job. That round-trip is the contributor loop.
| You changed… | Rebuild target |
|---|---|
Compiler (swift/lib/**, *.def) |
bin/swift-frontend |
Standard library (stdlib/public/core/**) |
swiftCore-linux-x86_64 (or libswiftCore.so) |
| Everything / unsure | re-run dobuild.sh foundation (full, slow but always correct) |
All targets: nix develop -c ninja -C $B/swift-linux-x86_64 -t targets.
llvm-lit is the test runner. Point it at a single file, a directory, or many.
B=build/Ninja-RelWithDebInfoAssert+swift-DebugAssert
LIT="$B/llvm-linux-x86_64/bin/llvm-lit"
T="$B/swift-linux-x86_64/test-linux-x86_64"
nix develop -c $LIT -s $T/Constraints/assignment.swift # one file (-s = short summary)
nix develop -c $LIT -s $T/Parse # a whole suite
nix develop -c $LIT -a $T/Constraints/assignment.swift # -a = show full output (debugging)
nix develop -c $LIT -j8 $T/Parse $T/Sema # parallel, multiple suitesTest categories:
- Non-executable (typecheck / parse / SIL /
-verify): the bulk of compiler & stdlib work. These all pass on NixOS out of the box. - Executable (build + run a binary, often
import StdlibUnittest): these pass on NixOS too, thanks toSWIFT_DRIVER_TEST_OPTIONSexported by the flake (seeHACKING.md§5). You don't set anything; the dev shell already has it.
Current local results: Parse 252/252, Interpreter 257/260 (the 3 known exceptions are
documented in HACKING.md §5 and don't affect normal work).
Every bug fix / feature needs a test, placed under swift/test/ next to similar ones. A test is a
.swift file with RUN: lines (what to run) and checks. Two common shapes:
A diagnostic / typecheck test (-verify): the compiler's emitted diagnostics must match inline
// expected-error/// expected-warning annotations.
// RUN: %target-typecheck-verify-swift
func f() {
var i = 0
if i = 6 {} // expected-error {{use of '=' in a Boolean context, did you mean '=='?}}
}An executable test (compiles, runs, and matches stdout with FileCheck):
// RUN: %target-run-simple-swift | %FileCheck %s
// REQUIRES: executable_test
print("hello") // CHECK: helloRun it the same way as any other test (§4). Useful substitutions: %target-typecheck-verify-swift,
%target-run-simple-swift, %target-build-swift, %FileCheck, %s (this file), %t (temp dir).
B=build/Ninja-RelWithDebInfoAssert+swift-DebugAssert
cat > /tmp/hi.swift <<'EOF'
print("built from source on NixOS:", (1...5).map { $0 * $0 })
EOF
nix develop -c env LD_LIBRARY_PATH=$B/swift-linux-x86_64/lib/swift/linux \
$B/swift-linux-x86_64/bin/swiftc /tmp/hi.swift -o /tmp/hi && /tmp/hi(The LD_LIBRARY_PATH points the runtime at your freshly-built stdlib. The harmless
libc not found warning is expected on NixOS.) For import Foundation / C++ interop usage, see
the README/HACKING.md notes on the corelibs flags.
The code change lives in the swift/ subdirectory (that's the swiftlang/swift repo). Your PR
only ever contains changes to that repo, never flake.nix, dobuild.sh, or anything NixOS.
git -C swift checkout main && git -C swift pull origin main # start fresh
git -C swift checkout -b improve-equal-diagnostic # a topic branch
# ...make your edits + test...
git -C swift add -A
git -C swift commit # see message conventions below
git -C swift push fork improve-equal-diagnostic # push to YOUR fork- Title: concise, blank line, then body. Prefix with a component tag, e.g.
[Sema] Capitalize "Boolean" in the assignment-in-condition diagnostic. - Body: the full reasoning. Link the issue you're fixing (
Fixes #NNNNN). - New source files need the Swift.org Apache-2.0 copyright header (copy it from a neighboring file).
- Note for this repo: commits here are authored by your own git identity, with no Co-Authored-By trailers.
GitHub will show a "Compare & pull request" banner after you push. Open it against
swiftlang/swift main. Fill in what changed and why; link the issue.
-
Swift's CI runs on Apple's infrastructure (Ubuntu + macOS), not your machine. Your local NixOS quirks are invisible to it.
-
CI is triggered by a comment from someone with commit access:
Comment What it does @swift-ci Please smoke testIncremental build + core tests (fast, the usual first pass) @swift-ci Please testFull validation build across platforms/arches @swift-ci Please smoke test Linux platformLinux only @swift-ci Please smoke test macOS platformmacOS only @swift-ci Please clean testFull, non-incremental (workspace wiped) -
As a newcomer you usually can't self-trigger
@swift-ci(it needs commit access). Your reviewer/maintainer comments it for you. That's normal; just ping politely if your PR sits. -
Commit access itself is granted after ~5 non-trivial merged PRs (email code-owners@forums.swift.org).
Iterate on feedback; don't assume silent approval. Wait for an explicit ✅. Ping non-urgent PRs about weekly. Reviewing others' PRs builds goodwill.
- Good first issues: https://github.com/swiftlang/swift/contribute
- Discuss anything language/stdlib-shaped on the Swift Forums before coding; large language/stdlib changes go through Swift Evolution.
- Read
swift/docs/FirstPullRequest.mdand swift.org/getting-started.
- One build at a time in
$B: concurrentninja/build-scriptruns corrupt the build dir. - Watch
/(the Nix store), not just/home. Eachnix developand the build consume store space;/filling to 100% looks like random errors.nix-collect-garbage -dfrees it. - Editing
flake.nixrequires re-entering the shell (direnv reload, or a freshnix develop). - The bare build-clang (
$B/llvm-linux-x86_64/bin/clang) has no NixOS toolchain knowledge; the flake feeds it crt/gcc/glibc viaCCC_OVERRIDE_OPTIONSand the test harness viaSWIFT_DRIVER_TEST_OPTIONS. You normally don't touch these, but if you build/link by hand and getcannot find Scrt1.oor a startupSIGSEGV, that's the missing toolchain/dynamic-linker wiring (HACKING.md §5 explains it in full). import Foundation/ C++ interop at the command line need the corelibs flags (-sdk,-Xcc --gcc-toolchain, rpath-link); seeREADME/HACKING.md. Plain Swift needs none of this.
Identical (the part that matters for contributing):
- The
swift/source, the git history, the branch/commit/PR mechanics, the test files and how you runllvm-lit, the diagnostics, the SIL, the behavior of the compiler you build. - The PR you open and the CI that judges it. CI never runs on your machine (yours or an Ubuntu user's); it runs on Apple's Ubuntu+macOS fleet. Your reviewer can't tell which distro you used.
- The "local green is necessary, not sufficient; trust CI" rule applies to everyone.
Different (all of it about local build/test plumbing, none of it in your PR):
| Topic | Ubuntu / macOS (supported) | NixOS (this repo) |
|---|---|---|
| Build command | ./swift/utils/build-script directly |
nix develop -c bash dobuild.sh (wraps build-script with NixOS flags) |
| C/C++ toolchain | system gcc/glibc in /usr, clang finds them |
nothing in /usr; the flake injects glibc/gcc/sysroot |
| Dynamic linker | /lib64/ld-linux is real glibc |
/lib64/ld-linux is nix-ld; we force the nix glibc loader |
| Bootstrap compiler | downloaded Swift snapshot toolchain | nixpkgs swift 5.10.1 |
| Dependencies | apt/Homebrew packages |
pinned in flake.nix (reproducible) |
| Test suite | executable tests pass as-is | pass via SWIFT_DRIVER_TEST_OPTIONS; 3 niche tests still fail (LTO + 2 C-interop, HACKING.md §5) |
| Support status | officially supported, CI-covered | not an official platform; local-only failures can be environment, not your code |
The one practical asymmetry: because NixOS isn't a CI platform, occasionally CI may flag something you can't reproduce locally (e.g. one of the 3 unsupported-test categories). When that happens, reason from the CI logs rather than local repro. It's a minor tax, not a blocker, and for the vast majority of compiler/stdlib work (diagnostics, parser/sema, stdlib additions) you have a faithful, fast local loop that's at parity with an Ubuntu contributor.
Happy hacking. Start with a good first issue, run the loop in §3 once on it, and open that PR.