The supported way to install Swift on Ubuntu is now swiftly, the toolchain manager listed first on swift.org’s Linux install page. The tarball recipe most guides still copy fails at the verify step, because every Swift 6 release is signed with a third key and importing the usual two ends in “Can’t check signature: No public key”. Ubuntu’s archive is no shortcut either: its swift package is the OpenStack object store, and swiftlang is too old to build what a current toolchain creates.
This guide installs Swift with swiftly, adds the build packages it needs, then builds and tests real code. The signed tarball and the archive package are covered too. Use swiftly unless you have a reason not to. We ran the swiftly and tarball steps on Ubuntu 26.04 and 24.04 in October 2026 with Swift 6.4.1 and swiftly 1.2.0.
Choose a Swift install method
Pick swiftly for a workstation, a dev VM or a CI runner you control. It installs per user, verifies every toolchain it downloads, and switches or updates toolchains with one command, so you never edit PATH by hand. The other two methods fit narrower jobs.
| Method | Swift you get on 26.04 | Installs to | Use it when |
|---|---|---|---|
| swiftly | Latest release, other 6.4.x on demand | ~/.local/share/swiftly (per user) | Dev machines and CI runners. The default. |
| Release tarball | The exact release you download | /opt/swift or any path you pick | You need one pinned build, system-wide |
Ubuntu archive (swiftlang) | 6.1.3 | /usr/libexec/swift | Ubuntu mirrors are all the host can reach |
We wouldn’t start new work on the archive package. An apt install is convenient, but its compiler is old, so a package made with the current toolchain fails to build on it (the exact error is in the troubleshooting section). The tarball is the right call when the toolchain must not move, such as in a Dockerfile, on a build host that installs from a file share, or as one copy under /opt shared by several users.
Prerequisites
swiftly needs very little from the box itself, but the toolchain it downloads is large:
- Ubuntu 26.04 LTS or Ubuntu 24.04 LTS, on x86_64 or aarch64
- A regular user with sudo rights, for apt and the optional
/opttarball - About 4 GB free under your home directory for each toolchain
- curl, tar and gpg, which the 26.04 cloud image already ships
- HTTPS access to www.swift.org and download.swift.org, plus github.com for package dependencies
One toolchain measured 3.6 GB installed on our test VM, a 2 vCPU, 4 GB RAM OpenStack instance with a 40 GB disk, and every timing in this guide comes from that box. We then replayed the same reader path on a fresh Ubuntu 24.04 VM of the same size, and the whole replay ran without an error. Its toolchain took 3.5 GB, the dependency list was the one step that differed, and the static binary’s glibc floor came out lower, so both are called out below.
Install Swift on Ubuntu with swiftly
swiftly is one self-contained binary. It installs itself under ~/.local/share/swiftly, downloads the latest Swift release, checks its signature and adds the tools to your PATH, so you run it as your normal user rather than root. If you have used rustup for Rust, it’s the same idea. Refresh the package index first, because a later step installs build packages from apt.
sudo apt-get update
The server and cloud images already have curl and git, but a fresh Ubuntu Desktop install may lack both, so install them first. On a desktop install, add them before you go on, since swiftly is fetched with curl and SwiftPM uses git to clone package dependencies:
sudo apt-get install -y curl git
Next, download swiftly for your CPU and unpack it in your home directory. The URL carries no version number, so it always fetches the current swiftly release, and $(uname -m) picks the x86_64 or aarch64 build.
cd ~
curl -O https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz
tar zxf swiftly-$(uname -m).tar.gz
The 29 MB archive unpacks to a single 75 MB swiftly binary plus LICENSE.txt. swift.org publishes a signature for the archive too, so you can check it before running anything. This imports swift.org’s keys into your GnuPG keyring and verifies the download against them:
curl -sSO https://download.swift.org/swiftly/linux/swiftly-$(uname -m).tar.gz.sig
curl -sSL https://www.swift.org/keys/all-keys.asc | gpg --import -
gpg --verify swiftly-$(uname -m).tar.gz.sig swiftly-$(uname -m).tar.gz
The swiftly archive is signed by the Swift Automatic Signing Key #4, so a good result reads Good signature from "Swift Automatic Signing Key #4 <[email protected]>" with a primary key fingerprint ending in CF9C E069. A WARNING that the key isn’t certified also appears, and the tarball section explains why that one is harmless. Now run the init step, which is the same command swift.org documents.
./swiftly init --quiet-shell-followup
init prints where it will put files and asks Proceed? (Y/n), so answer Y. On our VM it then downloaded the 1,069.7 MiB toolchain, verified and unpacked it in about 28 seconds, and finished like this (progress bar lines left out):
Installing swiftly in /home/josphat/.local/share/swiftly/bin/swiftly...
Creating shell environment file for the user...
Updating profile...
Fetching the latest stable Swift release...
Installing Swift 6.4.1
Downloading Swift 6.4.1
Downloaded 1069.7 MiB of 1069.7 MiB
Verifying toolchain signature...
Extracting toolchain...
The global default toolchain has been set to `Swift 6.4.1`
Swift 6.4.1 is installed successfully!
There are some dependencies that should be installed before using this toolchain.
You can run the following script as the system administrator (e.g. root) to prepare
your system:
apt-get -y install binutils-gold unzip zip gnupg2 libcurl4-openssl-dev libgcc-15-dev libpython3-dev libstdc++-15-dev libxml2-dev libncurses-dev libz3-dev pkg-config zlib1g-dev
Verifying toolchain signature... is swiftly checking the download against swift.org’s keys, which it imports into your own GnuPG keyring. The apt-get line at the end lists only packages that are missing, so a box that already has them gets no list at all. Keep that line for the next step. For unattended installs such as cloud-init or a CI image, add --assume-yes, which skips both the prompt and the welcome text.
Besides adding swift.org’s keys to your GnuPG keyring, init writes these files:
~/.local/share/swiftly/bin, the swiftly binary plus 60 symlinks to it (swift, swiftc, clang, lldb and the rest)~/.local/share/swiftly/toolchains, one directory per installed releaseconfig.jsonandenv.shin~/.local/share/swiftly, the settings and the PATH setup- Two lines at the end of
~/.profilethat loadenv.shat login
That profile file is ~/.profile on a stock Ubuntu account using bash. If ~/.bash_profile or ~/.bash_login exists, swiftly writes to that file instead. Under zsh it uses ~/.zprofile, so read the profile paths in this guide accordingly. The init step also moved the unpacked binary into its bin directory, which leaves only LICENSE.txt, the archive and, if you verified it, the .sig file in your home directory.
Notice that ~/.bashrc is not on that list. Your current shell read ~/.profile before swiftly existed, so load the environment file by hand this once and clear the shell’s command cache.
. "${SWIFTLY_HOME_DIR:-$HOME/.local/share/swiftly}/env.sh"
hash -r
New SSH logins and desktop sessions pick it up from ~/.profile from now on. Commands run as ssh host command, cron jobs and other non-login shells never read that file, so a fresh install can still answer swift: command not found there. The troubleshooting section has the fix.
Install the Swift build dependencies
Swift runs without these packages, but it cannot link anything. Before this step, swift hello.swift printed its greeting, yet swift build on a new package died with cannot find crtbeginS.o. The GCC runtime files Swift links against were missing from the stock 26.04 cloud image, and so were all 13 packages swiftly listed. On the 26.04 cloud image it printed the list below; swiftly prints whatever is missing on your machine, so install that. The line here also adds git, which swiftly’s full 26.04 list includes and SwiftPM needs for package dependencies, and apt skips anything already installed.
sudo apt-get -y install binutils-gold unzip zip gnupg2 libcurl4-openssl-dev libgcc-15-dev libpython3-dev libstdc++-15-dev libxml2-dev libncurses-dev libz3-dev pkg-config zlib1g-dev git
apt added 57 packages and upgraded 26 more, a 51.5 MB download that used 184 MB of disk and finished in 14 seconds. If you script this step, remember that sudo on 26.04 is sudo-rs, which ignores sudo -E, so pass variables after sudo instead, as in sudo DEBIAN_FRONTEND=noninteractive apt-get ....
On Ubuntu 24.04, install the list swiftly prints there rather than this one, because the GCC packages in it are versioned. On our 24.04 VM it asked for the 13 series instead of 15, plain binutils instead of binutils-gold, and no unzip:
sudo apt-get -y install binutils zip gnupg2 libcurl4-openssl-dev libgcc-13-dev libpython3-dev libstdc++-13-dev libxml2-dev libncurses-dev libz3-dev pkg-config zlib1g-dev
On 24.04 that added 37 packages and upgraded 35, a 71.5 MB download that used 199 MB of disk in 19 seconds. With these packages in place, everything up to the tarball section runs as your normal user, and the package builds later in this guide link cleanly on both releases.
Verify the Swift toolchain
Four commands tell you which compiler you are on, which swiftly manages it, which toolchain is active and where that toolchain lives on disk.
swift --version
swiftly --version
swiftly list
swiftly use --print-location
A healthy install shows the release with its target triple, the swiftly version, the toolchain marked as in use and default, and its directory:
Swift version 6.4.1 (swift-6.4.1-RELEASE)
Target: x86_64-unknown-linux-gnu
1.2.0
Installed release toolchains
----------------------------
Swift 6.4.1 (in use) (default)
Installed snapshot toolchains
-----------------------------
/home/josphat/.local/share/swiftly/toolchains/6.4.1
(in use) means the current directory resolves to that toolchain and (default) means it’s the global choice, so the two only split apart once you pin a project. If swift --version reports a release you did not expect, run which -a swift to see every copy on your PATH, because the first one listed wins. The toolchain directory holds the compiler, SwiftPM, LLDB and sourcekit-lsp, the language server editors use for Swift completion, so an editor such as VS Code on Ubuntu only needs to find this toolchain. Our test VM gave the same results, with the size of the toolchain directory added:

Each toolchain took 3.6 GB on disk, so two or three side by side are fine on a laptop but worth planning for on a small CI runner.
Build and run a Swift package
Swift Package Manager (SwiftPM) ships inside the toolchain, so a new project is one command. Create a directory and initialise an executable package in it.
mkdir -p ~/projects/hello && cd ~/projects/hello
swift package init --type executable
SwiftPM writes the manifest, a source file and, on current releases, a test target with a Swift Testing stub even for an executable:
Creating executable package: hello
Creating Package.swift
Creating .gitignore
Creating Sources
Creating Sources/hello/hello.swift
Creating Tests/
Creating Tests/helloTests/
Creating Tests/helloTests/helloTests.swift
The first line of the generated Package.swift is // swift-tools-version: 6.4. It sets the minimum toolchain the package builds with, so it’s the first thing an older compiler checks and rejects. Build the program, then run it.
swift build
swift run
The first debug build took just under 5 seconds on 2 vCPUs, and swift run only rebuilt what had changed before it started the program:
Building for debugging...
[Constructing description]
[Planning deferred tasks]
[18 / 22] hello-product
[19 / 22] hello-product
[23 / 23] hello-product
Build complete! (4.87 secs)
Building for debugging...
[Planning deferred tasks]
Build complete! (0.70 secs)
warning: safeExec: signal(32, SIG_DFL) failed: Invalid argument
warning: safeExec: signal(33, SIG_DFL) failed: Invalid argument
Hello, world!
Both safeExec warnings appeared on every swift run on this box and are harmless. They appear to come from SwiftPM resetting signal handlers before it starts your program, which fails on 32 and 33 because glibc reserves those signals. They go to stderr, the program exits 0, and the built binary never prints them when you run it directly. Now build the release version you would ship and check what it links against.
swift build -c release
ls -lh .build/release/hello
ldd .build/release/hello
The release build finished in about a second. Watch the file size and the library paths in the output:
Building for production...
[Planning deferred tasks]
[10 / 13] hello-product
[13 / 13] hello-product
Build complete! (1.09 secs)
-rwxrwxr-x 1 josphat josphat 19K Oct 15 06:55 .build/release/hello
linux-vdso.so.1 (0x00007ac21dc91000)
libswiftCore.so => /home/josphat/.local/share/swiftly/toolchains/6.4.1/usr/lib/swift/linux/libswiftCore.so (0x00007ac21d400000)
libswift_Concurrency.so => /home/josphat/.local/share/swiftly/toolchains/6.4.1/usr/lib/swift/linux/libswift_Concurrency.so (0x00007ac21dbdd000)
libswift_StringProcessing.so => /home/josphat/.local/share/swiftly/toolchains/6.4.1/usr/lib/swift/linux/libswift_StringProcessing.so (0x00007ac21d345000)
libswift_RegexParser.so => /home/josphat/.local/share/swiftly/toolchains/6.4.1/usr/lib/swift/linux/libswift_RegexParser.so (0x00007ac21d237000)
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007ac21d000000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007ac21cc00000)
libm.so.6 => /usr/lib/x86_64-linux-gnu/libm.so.6 (0x00007ac21ceda000)
libgcc_s.so.1 => /usr/lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007ac21dba6000)
/lib64/ld-linux-x86-64.so.2 (0x00007ac21dc93000)
libdispatch.so => /home/josphat/.local/share/swiftly/toolchains/6.4.1/usr/lib/swift/linux/libdispatch.so (0x00007ac21db47000)
libswift_Builtin_float.so => /home/josphat/.local/share/swiftly/toolchains/6.4.1/usr/lib/swift/linux/libswift_Builtin_float.so (0x00007ac21db43000)
libswiftGlibc.so => /home/josphat/.local/share/swiftly/toolchains/6.4.1/usr/lib/swift/linux/libswiftGlibc.so (0x00007ac21db31000)
libBlocksRuntime.so => /home/josphat/.local/share/swiftly/toolchains/6.4.1/usr/lib/swift/linux/libBlocksRuntime.so (0x00007ac21db2d000)
A 19K binary has no room for the Swift runtime. Its libswiftCore.so and the other Swift libraries resolve to paths under ~/.local/share/swiftly, so the same file copied to a server without that toolchain won’t start. Here is that release build on the test VM, run from the project directory:

To carry the Swift runtime inside the binary instead, link the Swift standard library statically. Rebuild and look at the libraries again.
swift build -c release --static-swift-stdlib
ls -lh .build/release/hello
ldd .build/release/hello
This time the runtime is compiled in, and only system libraries are left in the ldd list:
Building for production...
[Planning deferred tasks]
[2 / 5] hello-product
[6 / 6] hello-product
Build complete! (1.05 secs)
-rwxrwxr-x 1 josphat josphat 8.5M Oct 15 06:55 .build/release/hello
linux-vdso.so.1 (0x00007b89eda91000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007b89ed000000)
libm.so.6 => /usr/lib/x86_64-linux-gnu/libm.so.6 (0x00007b89ed2da000)
libgcc_s.so.1 => /usr/lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007b89eda54000)
libc.so.6 => /usr/lib/x86_64-linux-gnu/libc.so.6 (0x00007b89ecc00000)
/lib64/ld-linux-x86-64.so.2 (0x00007b89eda93000)
Static linking grew it to 8.5M and removed every path into ~/.local/share/swiftly, so it ran on this box even under an empty environment (env -i). It still links glibc and libstdc++ dynamically, though, so unlike a CGO-free Go build it is not a file you can drop anywhere. Check the newest glibc symbol version it asks for:
objdump -T .build/release/hello | grep -oE 'GLIBC_[0-9.]+' | sort -Vu | tail -3
The highest version is the floor for any host that runs it:
GLIBC_2.34
GLIBC_2.38
GLIBC_2.43
GLIBC_2.43 is the glibc Ubuntu 26.04 ships (ldd --version reports 2.43 on the test box), so a binary built on 26.04 runs on 26.04 and newer but not on Ubuntu 24.04, which ships glibc 2.39. The same build on our 24.04 VM came out at 8.4M with a highest symbol of GLIBC_2.38 (the list read GLIBC_2.33, GLIBC_2.34, GLIBC_2.38), which is below what both releases ship. So build on the oldest release you deploy to. For a fully static binary that runs on any Linux, swift.org’s Static Linux SDK links against musl instead of glibc; we did not test it for this guide.
Run tests with swift test
swift test builds the test target and runs it with Swift Testing, the framework current package templates use. A library with a couple of real checks shows what passing and failing look like, so create one.
mkdir -p ~/projects/MathKit && cd ~/projects/MathKit
swift package init --type library
Open the template’s source file, which only holds a hello() function. Any editor works if vim isn’t installed.
vim Sources/MathKit/MathKit.swift
Replace the whole file with two functions worth testing:
public func add(_ a: Int, _ b: Int) -> Int {
a + b
}
public func isPrime(_ n: Int) -> Bool {
guard n > 1 else { return false }
var i = 2
while i * i <= n {
if n % i == 0 { return false }
i += 1
}
return true
}
Then open the test file the template created.
vim Tests/MathKitTests/MathKitTests.swift
Replace its contents with three tests. The middle one is parameterized, so it runs once for each argument in the list.
import Testing
@testable import MathKit
@Test func addition() {
#expect(add(2, 3) == 5)
}
@Test(arguments: [2, 3, 5, 7, 11])
func primes(n: Int) {
#expect(isPrime(n))
}
@Test func notPrime() {
#expect(!isPrime(9))
}
Run the suite. The first run compiles the library, the tests and the test runner, which took 7.5 seconds here.
swift test
Two test runners report in the output, and only one of them matters:
Building for debugging...
[Constructing description]
[Planning deferred tasks]
[30 / 53] MathKit
[38 / 57] MathKitTests-product
[39 / 57] MathKitTests-product
[40 / 58] MathKitTests-product
[48 / 59] MathKitTests-product
[51 / 59] MathKitTests-product
[55 / 62] MathKitTests-test-runner
[59 / 62] MathKitTests-test-runner
[63 / 63] MathKitTests-test-runner
Build complete! (7.55 secs)
Test Suite 'All tests' started at 2026-10-15 06:56:08.533
Test Suite 'Debug-linux-x86_64.xctest' started at 2026-10-15 06:56:08.534
Test Suite 'Debug-linux-x86_64.xctest' passed at 2026-10-15 06:56:08.534
Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds
Test Suite 'All tests' passed at 2026-10-15 06:56:08.534
Executed 0 tests, with 0 failures (0 unexpected) in 0.0 (0.0) seconds
◇ Test run started.
↳ Testing Library Version: 6.4.1 (804dc4530ef3666)
↳ Target Platform: x86_64-unknown-linux-gnu
◇ Test primes(n:) started.
◇ Test addition() started.
◇ Test notPrime() started.
◇ Test case passing 1 argument n → 2 to primes(n:) started.
◇ Test case passing 1 argument n → 3 to primes(n:) started.
◇ Test case passing 1 argument n → 5 to primes(n:) started.
◇ Test case passing 1 argument n → 7 to primes(n:) started.
◇ Test case passing 1 argument n → 11 to primes(n:) started.
✔ Test primes(n:) with 5 test cases passed after 0.001 seconds.
✔ Test addition() passed after 0.001 seconds.
✔ Test notPrime() passed after 0.001 seconds.
✔ Test run with 3 tests in 0 suites passed after 0.001 seconds.
The Executed 0 tests lines come from the XCTest runner, which finds nothing because these tests use Swift Testing. The ◇ and ✔ lines are the real results, with 3 tests passed and primes(n:) run as 5 separate cases, one per argument. To see a failure, we flipped the last assertion to #expect(isPrime(9)) and ran it again. The run then reported ✘ Test notPrime() recorded an issue at MathKitTests.swift:14:5: Expectation failed: isPrime(9) and swift test exited with status 1, which is all a CI job needs to fail the build.
Run Swift scripts and the REPL
Not everything needs a package. The swift command runs a single source file directly, which suits small automation scripts.
mkdir -p ~/scripts && cd ~/scripts
echo 'print("Hello, Swift on Ubuntu!")' > hello.swift
swift hello.swift
Swift compiles and runs the file in one go, in 0.11 seconds for this one:
Hello, Swift on Ubuntu!
A #!/usr/bin/env swift shebang works too, so a script marked executable runs like any other command. One of ours that imported Foundation took 1.5 seconds per run, because the file is compiled on every call, so keep scripts for glue work and move anything you run often into a package. Typing swift on its own prints a list of subcommands rather than opening a REPL, which catches out anyone following an older guide. Start the REPL by name.
swift repl
Each line is evaluated as you type it, and results come back with their type. Type :quit to leave.
Welcome to Swift version 6.4.1 (swift-6.4.1-RELEASE).
Type :help for assistance.
1> let langs = ["Swift", "Go", "Rust"]
langs: [String] = 3 values {
[0] = "Swift"
[1] = "Go"
[2] = "Rust"
}
2> print(langs.map { $0.uppercased() })
["SWIFT", "GO", "RUST"]
3> langs.count * 2
$R0: Int = 6
4> :quit
You need a real terminal for that view. Piped input still works for a quick check, but then the REPL prints only what your code prints.
Add a package dependency
SwiftPM can now edit Package.swift for you, so adding a library from GitHub takes two commands instead of hand-editing the manifest. This example builds a small CLI on Apple’s swift-argument-parser.
mkdir -p ~/projects/greet && cd ~/projects/greet
swift package init --type executable
swift package add-dependency https://github.com/apple/swift-argument-parser.git --from 1.8.2 #https://github.com/apple/swift-argument-parser/releases
swift package add-target-dependency ArgumentParser greet --package swift-argument-parser
Each add command prints Updating package manifest at Package.swift... done. when it works. Together they list swift-argument-parser under dependencies and wire its ArgumentParser product into the greet target, so the code can import it. Now write the CLI in the generated source file.
vim Sources/greet/greet.swift
Replace it with a command that takes a name, a count option and a flag:
import ArgumentParser
@main
struct Greet: ParsableCommand {
static let configuration = CommandConfiguration(abstract: "Print a greeting.")
@Argument(help: "Who to greet.")
var name: String
@Option(name: .shortAndLong, help: "How many times to print it.")
var count: Int = 1
@Flag(help: "Shout the greeting.")
var shout = false
mutating func run() throws {
var line = "Hello, \(name)!"
if shout { line = line.uppercased() }
for _ in 0..<count { print(line) }
}
}
The first build clones the dependency from GitHub and compiles it, so it’s the slow one. Fetching the repository took about a second here, and most of the build time went on compiling ArgumentParser.
swift build
SwiftPM resolved the newest release in range, recorded its exact revision in Package.resolved and finished with Build complete! (11.47 secs). Commit Package.resolved with your code, because it makes another machine build against the same commit instead of whatever the version range allows next month. Run the CLI through SwiftPM, with the safeExec warnings sent to /dev/null.
swift run greet --help 2>/dev/null
swift run greet Josphat --count 2 --shout 2>/dev/null
ArgumentParser generates the help text from the property wrappers, and the second command shouts twice:
OVERVIEW: Print a greeting.
USAGE: greet <name> [--count <count>] [--shout]
ARGUMENTS:
<name> Who to greet.
OPTIONS:
-c, --count <count> How many times to print it. (default: 1)
--shout Shout the greeting.
-h, --help Show help information.
HELLO, JOSPHAT!
HELLO, JOSPHAT!
To use it outside the project, build the release binary and install it into ~/.local/bin. The last line calls it by full path, so it works even before that directory is on your PATH.
swift build -c release
mkdir -p ~/.local/bin
install -m 0755 .build/release/greet ~/.local/bin/greet
~/.local/bin/greet World
The installed binary printed Hello, World!. Its release build took 26 seconds on 2 vCPUs, because the optimiser compiles ArgumentParser as a whole module, and it came out at 3.5M, still linked to the swiftly runtime like the first example. Run it without a name and it prints Error: Missing expected argument '<name>' with the usage text and exits with status 64, which a calling script can test for.
Manage toolchains with swiftly
Toolchain management is where swiftly earns its place over the tarball. These are the commands we ran on the test VM, all as a normal user.
| Task | Command |
|---|---|
| List installed toolchains | swiftly list |
| List releases built for this Ubuntu | swiftly list-available |
| Install another release | swiftly install 6.4.0 |
| Run one command with a given release | swiftly run swift --version +6.4.0 |
| Set the global default on purpose | swiftly use --global-default 6.4.1 |
| Replace the in-use toolchain with a newer patch release | swiftly update |
| Update swiftly itself | swiftly self-update |
| Remove a toolchain | swiftly uninstall 6.4.0 |
Treat swiftly update as a replacement, not a check. Per the swiftly command reference, it uninstalls the in-use toolchain and installs the newest patch release of the same minor version. A .swift-version file that names the old patch release can then stop resolving, and swiftly update latest may jump to a newer minor release. On our box it only reported Swift 6.4.1 is already up to date, because nothing newer existed yet.
swiftly list-available on 26.04 showed only the 6.4 releases, because swift.org’s Ubuntu 26.04 builds start there. Ask for an older one and swiftly fails straight away.
swiftly install 6.3.3
The command gave up in 1.6 seconds with exit status 1. swiftly had built the download URL for your platform and found nothing there:
Installing Swift 6.3.3
Error: Swift 6.3.3 does not exist at URL https://download.swift.org/swift-6.3.3-release/ubuntu2604/swift-6.3.3-RELEASE/swift-6.3.3-RELEASE-ubuntu26.04.tar.gz, exiting
A project stuck on an older release needs a 24.04 host, where swiftly list-available on our VM listed every release from 6.4.1 back to 5.10.1. For a release that does exist, install it next to the current one before you pin anything, because a pin to a toolchain you don’t have can’t resolve. It took about 26 seconds here, and the existing toolchain stayed as it was.
swiftly install 6.4.0
How you pin a project depends on whether it’s a git repository. Inside one, swiftly use 6.4.0 offers to create a .swift-version file at the repository root, and that file pins the project for everyone who uses swiftly. Outside any git repository there is nowhere to put the file, so the same command changed our global default instead and printed The global default toolchain has been set to `Swift 6.4.0` (was 6.4.1). To change the default on purpose from any directory, use swiftly use --global-default. Pin the hello project after turning it into a git repository:
cd ~/projects/hello
git init -q
swiftly use 6.4.0
swiftly use
swift --version
swiftly asks before it writes the file, and pressing Enter accepts the default Y. Only swiftly use with a version does this: a bare swiftly use in a fresh git repository on our 24.04 VM just printed Swift 6.4.1 (default) and created no file. The project then resolves to the pinned release:
A new file `/home/josphat/projects/hello/.swift-version` will be created to set
the new in-use toolchain for this project. Alternatively, you can set your
default globally with the `--global-default` flag. Proceed with creating this
file?
Proceed? (Y/n):
The file `/home/josphat/projects/hello/.swift-version` has been set to `Swift 6.4.0` (was 6.4.1)
Swift 6.4.0 (/home/josphat/projects/hello/.swift-version)
Swift version 6.4 (swift-6.4-RELEASE)
Target: x86_64-unknown-linux-gnu
swiftly names the file the selection came from, and subdirectories of the repository resolve to the same file. Back in the home directory, swiftly use still reported Swift 6.4.1 (default), so the global default never moved. Watch the version string as well, because the pinned release prints Swift version 6.4 without a trailing zero, so match on that if a script checks versions. Commit .swift-version with the code and everyone on the team who uses swiftly builds with the same toolchain.
Install Swift from the release tarball
Use the tarball when you want exactly one release under /opt and nothing updating it behind your back. You do the signature check yourself, and that’s the step the old instructions get wrong. Install the dependency packages from earlier first, because the tarball needs the same libraries and swift.org’s tarball page has no 26.04 list. Set the release and work out the platform names from /etc/os-release. The version is pinned on purpose, since a fixed toolchain is the reason to pick this method.
export SWIFT_VERSION="6.4.1" # pin: the exact release to install #https://www.swift.org/install/linux/tarball/
UBU_VER=$(. /etc/os-release; echo "$VERSION_ID")
SWIFT_PLATFORM=ubuntu${UBU_VER}
SWIFT_PLATFORM_DIR=ubuntu${UBU_VER//./}
The two platform variables resolve to the names swift.org uses in its download paths:
ubuntu26.04andubuntu2604on Ubuntu 26.04ubuntu24.04andubuntu2404on Ubuntu 24.04- the same names with
-aarch64appended on ARM64 machines
On an ARM64 machine, add that suffix to the end of both variables by hand before you download. Then fetch the tarball and its signature.
mkdir -p ~/swift-tarball && cd ~/swift-tarball
wget -q https://download.swift.org/swift-${SWIFT_VERSION}-release/${SWIFT_PLATFORM_DIR}/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-${SWIFT_PLATFORM}.tar.gz
wget -q https://download.swift.org/swift-${SWIFT_VERSION}-release/${SWIFT_PLATFORM_DIR}/swift-${SWIFT_VERSION}-RELEASE/swift-${SWIFT_VERSION}-RELEASE-${SWIFT_PLATFORM}.tar.gz.sig
At 1.1 GB, the tarball came down in about 10 seconds on both test VMs. Import the release signing keys next. The tarball page lists three fingerprints, and the third one, the Swift 6.x Release Signing Key, is the key that signs current releases.
gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 'A62A E125 BBBF BB96 A6E0 42EC 925C C1CC ED3D 1561' 'E813 C892 820A 6FA1 3755 B268 F167 DF1A CF9C E069' '52BB 7E3D E28A 71BE 22EC 05FF EF80 A866 B47A 981F'
Older guides import only the first two, and that isn’t enough. The Swift 5.x key among them expired in March 2025, and neither of the two signed this release, so with only those in your keyring the check ends in Can't check signature: No public key. The keyserver also talks HKP on TCP port 11371, which some firewalls block, so if --recv-keys hangs, import swift.org’s key file over HTTPS instead (the command is in the troubleshooting section). Then verify the download.
gpg --verify swift-${SWIFT_VERSION}-RELEASE-${SWIFT_PLATFORM}.tar.gz.sig swift-${SWIFT_VERSION}-RELEASE-${SWIFT_PLATFORM}.tar.gz
Look for Good signature and a primary key fingerprint that ends in B47A 981F:
gpg: Signature made Mon Oct 12 18:21:08 2026 UTC
gpg: using RSA key EF80A866B47A981F
gpg: Good signature from "Swift 6.x Release Signing Key <[email protected]>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 52BB 7E3D E28A 71BE 22EC 05FF EF80 A866 B47A 981F
You can ignore the WARNING lines, which only mean you haven’t certified swift.org’s key in your own web of trust, so they show up on every fresh keyring without changing the result. Anything other than Good signature means the file is not what swift.org signed, so delete it and download again. Then extract it into /opt and put it on your PATH.
sudo tar -xzf swift-${SWIFT_VERSION}-RELEASE-${SWIFT_PLATFORM}.tar.gz -C /opt
sudo mv /opt/swift-${SWIFT_VERSION}-RELEASE-${SWIFT_PLATFORM} /opt/swift
export PATH=/opt/swift/usr/bin:"${PATH}"
swift --version
swift --version now reports the release you pinned, in the same format as the swiftly install, and the MathKit tests from earlier passed on this toolchain too. Extraction took about 16 seconds on 26.04 and 25 seconds on 24.04, where the 24.04 tarball verified against the same Swift 6.x key, and on 26.04 the tree is 3.6 GB, about the size of a swiftly toolchain. Your PATH change lasts for this shell only. Add the same export line to ~/.bashrc for your own account, or to /etc/profile.d/swift.sh if every user on the box should get it. Keep the .sh suffix, because Ubuntu’s /etc/profile only sources files there that end in it. We only tested the export in the current shell. Once the toolchain works, rm -rf ~/swift-tarball gives back the 1.1 GB download.
Upgrading means repeating all of this with a new SWIFT_VERSION, plus one extra step. Run sudo rm -rf /opt/swift before the mv, because moving a directory onto an existing /opt/swift puts the new tree inside the old one, and PATH keeps finding the old compiler. Nothing tells you a release is out either, which is why we keep this method for build images rather than dev machines.
Install Swift from the Ubuntu archive
Ubuntu 26.04 ships Swift in the universe component as swiftlang, while Ubuntu 24.04 has no swiftlang package at all, so this method is for 26.04 only. Do not install the package called swift, because that is the server side of OpenStack Swift object storage, and its python3-swiftclient companion installs a different swift command of its own. If object storage is what you are after, the OpenStack command-line client is the tool you want. For the language, install the package and check its version through the /usr/bin/swift link it creates.
sudo apt-get -y install swiftlang
/usr/bin/swift --version
apt pulls in 13 new packages for this one. After its output, the version line shows how far behind the archive build is:
Swift version 6.1.3 (swift-6.1.3-RELEASE)
Target: x86_64-pc-linux-gnu
The 597 MB download is mostly swiftlang itself, and the 13 packages take 2.6 GB on disk, about 2.4 GB of that the toolchain under /usr/libexec/swift. A debconf question (default yes) creates the /usr/bin/swift link. Its template says why it asks: python3-swiftclient also ships a swift command, so answer no if you use that client.
The archive build handles a package it creates itself, and a fresh swift package init built in 3.4 seconds and ran. It refuses any package whose manifest asks for a newer tools version, though, and that includes everything the current toolchain generates. Use it only when Ubuntu mirrors are all your build host can reach, the same convenience-versus-freshness call our Snap vs Flatpak vs APT guide covers for other software. It installs cleanly next to swiftly because the paths differ, and sudo apt-get purge swiftlang libswiftlang removes it again and frees 2.5 GB, although the gcc it pulled in stays behind.
Uninstall Swift and swiftly
Remove things in this order, because swiftly self-uninstall deletes swiftly but deliberately keeps installed toolchains, and the current swiftly release also leaves its line in ~/.profile. Uninstall the toolchains first, then swiftly, then clean the profile and the files the swiftly archive left in your home directory.
swiftly uninstall all --assume-yes
swiftly self-uninstall --assume-yes
sed -i '/# Added by swiftly/d; /swiftly\/env.sh/d' ~/.profile
rm -rf ~/.local/share/swiftly ~/swiftly ~/swiftly-$(uname -m).tar.gz ~/swiftly-$(uname -m).tar.gz.sig ~/LICENSE.txt
rm -rf ~/.swiftpm ~/.cache/org.swift.swiftpm ~/.cache/org.swift.foundation.URLCache ~/.cache/clang ~/.cache/lldb
Afterwards, grep -c swiftly ~/.profile should print 0. The last line clears the per-user caches that building and debugging left behind on our box; keep ~/.cache/clang and ~/.cache/lldb if you use clang or lldb outside Swift. The apt dependency packages and the swift.org keys in your GnuPG keyring stay behind, so remove the packages with apt if nothing else on the box needs them. For the tarball install, sudo rm -rf /opt/swift ~/swift-tarball removes the toolchain and the download, and then you delete the PATH line you added.
Troubleshoot Swift install errors on Ubuntu
Each of these came up on the test VM while this guide was being written. Each heading is an exact piece of the error text, so you can match it against what your terminal shows.
cannot find crtbeginS.o: No such file or directory
swift build fails at the link step on a box that has Swift but not its build dependencies. After an error: 'hello': Invalid manifest line, the output ends like this:
error: link command failed with exit code 1 (use -v to see invocation)
/usr/bin/x86_64-linux-gnu-ld.bfd: cannot find crtbeginS.o: No such file or directory
/usr/bin/x86_64-linux-gnu-ld.bfd: cannot find -lgcc: No such file or directory
/usr/bin/x86_64-linux-gnu-ld.bfd: cannot find -lgcc_s: No such file or directory
clang: error: linker command failed with exit code 1 (use -v to see invocation)
The invalid manifest message is a side effect. SwiftPM compiles Package.swift into a small program before anything else, and that link fails for the same reason. Both crtbeginS.o and libgcc come from the GCC development package (libgcc-15-dev on 26.04), so install the full list from the dependency step and build again. A single-file script doesn’t need that link, which is why swift hello.swift kept working while every package build failed.
bash: line 1: swift: command not found
swiftly loads its environment from ~/.profile, and only login shells read that file. A command such as ssh dev01 swift --version runs a non-login, non-interactive shell, as does cron, so on our box it answered with this:
bash: line 1: swift: command not found
Interactive SSH logins work because they read ~/.profile, and a desktop session picks it up after you log out and back in once. For scripts and cron jobs, either source ~/.local/share/swiftly/env.sh at the top or run the command through a login shell.
bash -lc 'swift --version'
Over the same SSH connection, that form printed the Swift version on the test box. The -l flag makes bash a login shell, so it reads ~/.profile before it runs the command.
gpg: Can’t check signature: No public key
This is the tarball check failing on a keyring that holds only the two keys older guides import. The signature names the key it needs:
gpg: Signature made Mon Oct 12 18:21:08 2026 UTC
gpg: using RSA key EF80A866B47A981F
gpg: Can't check signature: No public key
EF80A866B47A981F is the Swift 6.x Release Signing Key. Import all three fingerprints from the tarball section, or pull swift.org’s complete key file, which imported nine keys on our test box.
wget -q -O - https://swift.org/keys/all-keys.asc | gpg --import -
Run gpg --verify again and it reports Good signature from that key. The other keys in that file are older release keys, most of them long expired, and they do no harm in the keyring.
is using Swift tools version 6.4.0 but the installed version is 6.1.3
You’re building with the archive’s swiftlang rather than a current toolchain. On our box, /usr/bin/swift build on the MathKit package from earlier printed this:
error: 'mathkit': package 'mathkit' is using Swift tools version 6.4.0 but the installed version is 6.1.3
The archive compiler writes // swift-tools-version: 6.1 into the packages it creates, while the current toolchain writes 6.4, so the older one refuses. Check which swift your shell picks up.
which -a swift
The output lists every match in PATH order. With both installed, swiftly’s copy should come first:
/home/josphat/.local/share/swiftly/bin/swift
/usr/bin/swift
/bin/swift
If /usr/bin/swift is listed first, or a build script calls it by full path, point the build at swiftly’s toolchain instead. Purge swiftlang if nothing on the box needs it, so the two can’t be mixed up again.
swiftly/env.sh: No such file or directory
Every new login printed /home/josphat/.profile: line 33: /home/josphat/.local/share/swiftly/env.sh: No such file or directory after swiftly self-uninstall, even though the uninstaller had reported Cleaning up shell profile files... on its way out. The uninstaller deleted env.sh but left the line in ~/.profile that sources it. Remove swiftly’s two lines, and point sed at ~/.bash_profile or ~/.zprofile instead if that’s where init wrote them.
sed -i '/# Added by swiftly/d; /swiftly\/env.sh/d' ~/.profile
After that, grep -c swiftly ~/.profile prints 0 and logins are quiet again. If you ran self-uninstall before swiftly uninstall all, the toolchains are still there too, 3.6 GB each under ~/.local/share/swiftly/toolchains, so delete that directory as well.