Cross Compiling Swift 5.10.1 for Linux ARMv7
One of the big focuses this year for me has been looking at being able to compile Swift for linux-armv7. So, the toolchain appears to support linux-armv7, but for now it seems that the only *productive* way to actually compile for the platform is cross compiling. Therefore, I spent quite a few hours looking into how to do this, and finally have something that works.
As a hobbyist, my main interest in this is being able to cross-compile Swift applications and deploy them to Raspberry Pi 32-bit distributions. But there are many other armv7 boards that can benefit from being able to cross compile Swift for them, that are “real” 32-bit devices that aren’t just backwards compatible with armv7/armhf, like the Raspberry Pi boards are.
To get started, you’ll need a sysroot. For cross compiling this is the most surefire way to make sure it’ll work, since trying to install dependencies in a cross compiling container or even on your current build host seems to be prone to all kinds of errors. The easiest method to get a sysroot is by using Docker to get a pre-built container and install needed dependencies. I chose Debian 12 Bookworm since it has official armv7 support (and it is the same as what the latest Raspberry Pi OS 32-bit install uses), and it’s pretty easy to grab the container and install what is needed.
UPDATE: With some help from the community of Swift on ARM, I was able to update the build profile used in this guide to completely remove the step for building LLVM, which is not actually needed to cross compile Swift for ARMv7. Thanks to finalgolfin (https://github.com/finagolfin) for this awesome help in getting this build profile improved!
To get started, make sure that qemu emulation with Docker is enabled on your platform. Since I use Linux with the Docker Engine directly, I ran the command:
docker run --privileged --rm tonistiigi/binfmt --install allIf using Docker Desktop instead, check the documentation from the Docker website: https://docs.docker.com/build/building/multi-platform/
Next, run the linux/arm/v7 container. It should take you to a root shell:
DEBIAN_VERSION=bookworm
docker run --platform linux/arm/v7 -it --name debian-$DEBIAN_VERSION-armv7 arm32v7/debian:$DEBIAN_VERSION /bin/bash
root@1147269e0d5b:/# uname -m
armv7l
root@1147269e0d5b:/#In this root shell, install the needed dependencies. This will probably feel slow since armv7 is being emulated through qemu:
apt update
apt install build-essential libatomic1 libxml2-dev libcurl4-openssl-dev libz-devNext, exit the container shell, then export the needed files from the container to your local filesystem:
DEBIAN_VERSION=bookworm
mkdir -p sysroot/usr/
docker cp debian-$DEBIAN_VERSION-armv7:/lib sysroot/lib
docker cp debian-$DEBIAN_VERSION-armv7:/usr/lib sysroot/usr/lib
docker cp debian-$DEBIAN_VERSION-armv7:/usr/include sysroot/usr/includeWe avoid copying any bin/ directories over, since cmake will try to run binaries in the sysroot (if CMAKE_SYSROOT is passed, for example) and that causes issues. So now, you should have a sysroot that looks like this:
sysroot
├── lib -> usr/lib
└── usr
├── include
│ ├── arm-linux-gnueabihf
│ ├── arpa
│ ├── asm-generic
│ ├── c++
│ ├── finclude
│ ├── libxml2
│ ├── linux
│ ├── misc
│ ├── mtd
│ ├── net
│ ├── netash
│ ├── netatalk
│ ├── netax25
│ ├── neteconet
│ ├── netinet
│ ├── netipx
│ ├── netiucv
│ ├── netpacket
│ ├── netrom
│ ├── netrose
│ ├── nfs
│ ├── protocols
│ ├── rdma
│ ├── rpc
│ ├── rpcsvc
│ ├── scsi
│ ├── sound
│ ├── tirpc
│ ├── unicode
│ ├── video
│ └── xen
└── lib
├── apt
├── arm-linux-gnueabihf
├── bfd-plugins
├── compat-ld
├── dpkg
├── gcc
├── gnupg
├── gnupg2
├── gold-ld
├── init
├── locale
├── lsb
├── mime
├── sasl2
├── ssl
├── systemd
├── terminfo
├── tmpfiles.d
└── udevNext, make sure you have Swift 5.10.1 installed in your build host and that clang is also available. This can done with swiftly or by using the tar distribution, but for my setup I just installed Swift in my Ubuntu 22.04 host using the community-driven deb repository:
curl -s https://archive.swiftlang.xyz/install.sh | sudo bash
sudo apt install swiftlangMake sure to have all the correct dependencies as well:
sudo apt install \
build-essential \
cmake \
git \
python3 \
clang \
lld \
uuid-devIf using swiftly (https://github.com/swiftlang/swiftly) or the tar method, the clang and lld dependencies above are not required since they are included in the Swift distribution for you.
Next, it’s time to grab the Swift repo and all the dependencies for swift-5.10.1-RELEASE, using my custom feature/swift-5.10-linux-armv7 branch:
git clone https://github.com/xtremekforever/swift -b feature/swift-5.10-linux-armv7
./utils/update-checkout --clone --tag swift-5.10.1-RELEASE --skip-repository cmakeThis step usually takes several minutes. Once it’s complete, now we can start building!
Using the Swift build-script, use the buildbot_linux_crosscompile_armv7,stdlib,corelibs preset to build the Swift stdlib and corelibs, like this:
./swift/utils/build-script --preset=buildbot_linux_crosscompile_armv7,stdlib,corelibs \
install_destdir=$(pwd)/install \
installable_package=$(pwd)/swift-5.10.1-linux-armv7-runtime.tar.gz \
sysroot=$(pwd)/sysroot \
workspace_dir=$(pwd) \
toolchain_path=/usr/bin- The
install_destdirandinstallable_packagefields can be configured to point anywhere you like. They are there to make it easy to take the build artifacts and use them however is desired. - The
sysrootfield points to where thedebian:bookwormsysroot forlinux/arm/v7was extracted earlier. This is required for the preset to be able to cross compile for armv7 and find everything it needs. - If using a toolchain that is installed somewhere other that
/usr/bin, set thetoolchain_pathto where the Swift toolchain binaries are located on your system. Could be~/.local/share/swiftly/toolchains/5.10.1/usr/binor/opt/swift/5.10.1/usr/bin, for example.
This profile took my i7–8700K (overclocked to 4.7GHz) desktop around 7 minutes to complete. On slower CPUs it may take longer, but I think a lot of build time can be attributed to some of the single-core compilation that happens when building the Swift stdlib. Either way, if this completes successfully, it should generate the following directory structure in the install directory:
install
└── usr
├── bin
├── lib
│ ├── swift
│ │ ├── apinotes
│ │ ├── Block
│ │ ├── CFURLSessionInterface
│ │ ├── CFXMLInterface
│ │ ├── clang
│ │ ├── CoreFoundation
│ │ ├── dispatch
│ │ ├── linux
│ │ ├── os
│ │ └── shims
│ └── swift_static
│ ├── Block
│ ├── CFURLSessionInterface
│ ├── CFXMLInterface
│ ├── clang
│ ├── CoreFoundation
│ ├── dispatch
│ ├── linux
│ ├── os
│ └── shims
└── share
├── doc
│ └── swift
└── man
└── man1Then, inside of the usr/lib/swift/linux directory, the following files should be present:
install/usr/lib/swift/linux
├── armv7
│ ├── glibc.modulemap
│ ├── SwiftGlibc.h
│ └── swiftrt.o
├── _Concurrency.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── CxxStdlib.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── Cxx.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── _Differentiation.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── Distributed.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── Glibc.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── libBlocksRuntime.so
├── libcxxshim.h
├── libcxxshim.modulemap
├── libcxxstdlibshim.h
├── libdispatch.so
├── libFoundationNetworking.so
├── libFoundation.so
├── libFoundationXML.so
├── libstdcxx.h
├── libstdcxx.modulemap
├── libswift_Concurrency.so
├── libswiftCore.so
├── libswiftCxx.a
├── libswiftCxxStdlib.a
├── libswift_Differentiation.so
├── libswiftDispatch.so
├── libswiftDistributed.so
├── libswiftGlibc.so
├── libswiftObservation.so
├── libswiftRegexBuilder.so
├── libswift_RegexParser.so
├── libswiftRemoteMirror.so
├── libswift_StringProcessing.so
├── libswiftSwiftOnoneSupport.so
├── libXCTest.so
├── Observation.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── RegexBuilder.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── _RegexParser.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── _StringProcessing.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── SwiftOnoneSupport.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
├── Swift.swiftmodule
│ ├── armv7-unknown-linux-gnueabihf.abi.json
│ ├── armv7-unknown-linux-gnueabihf.private.swiftinterface
│ ├── armv7-unknown-linux-gnueabihf.swiftdoc
│ ├── armv7-unknown-linux-gnueabihf.swiftinterface
│ └── armv7-unknown-linux-gnueabihf.swiftmodule
└── x86_64
├── Dispatch.swiftdoc
├── Dispatch.swiftmodule
├── FoundationNetworking.swiftdoc
├── FoundationNetworking.swiftmodule
├── Foundation.swiftdoc
├── Foundation.swiftmodule
├── FoundationXML.swiftdoc
├── FoundationXML.swiftmodule
├── XCTest.swiftdoc
└── XCTest.swiftmoduleSo, all that was built from this profile are the Swift stdlibs, dispatch, foundation, and xctest. This is enough to cross compile any application from a Swift toolchain on the host for the armv7 target. However, to do this, some more work needs to be done for Swift to be able to cross compile for this target. In my case, I created a JSON file like the following:
{
"version":1,
"sdk":"/path/to/swift-5.10-linux-armv7/sysroot",
"toolchain-bin-dir":"/usr/bin",
"target":"armv7-unknown-linux-gnueabihf",
"dynamic-library-extension":"so",
"extra-cc-flags":[
"-fPIC"
],
"extra-swiftc-flags":[
"-target", "armv7-unknown-linux-gnueabihf",
"-use-ld=lld",
"-Xlinker", "-rpath", "-Xlinker", "/usr/lib/swift/linux",
"-Xlinker", "-rpath", "-Xlinker", "/usr/lib/swift/linux/armv7",
"-resource-dir", "/path/to/swift-5.10-linux-armv7/install/usr/lib/swift",
"-sdk", "/path/to/swift-5.10-linux-armv7/sysroot",
"-Xcc", "--gcc-toolchain=/path/to/swift-5.10-linux-armv7/sysroot/usr"
],
"extra-cpp-flags":[
]
}All of the “/path/to” entries need to be replaced with the actual path of where you put your sysroot and put the install_destdir at. Also, the toolchain-bin-dir should be made to match the toolchain_path that was used for the Swift build, although I’ve found it doesn’t matter so much if it doesn’t match.
This should be all that is required to now cross compile an application for armv7! Create a new project to test it:
mkdir swift-hello && cd swift-hello
swift package init --type=executableThis can be built for the target by providing the --destination flag pointing to the JSON file that was created above:
> swift build --destination /path/to/swift-5.10-linux-armv7/toolchain.json
Building for debugging...
warning: Could not read SDKSettings.json for SDK at: /path/to/swift-5.10-linux-armv7/sysroot
warning: Could not read SDKSettings.json for SDK at: /path/to/swift-5.10-linux-armv7/sysroot
[8/8] Linking swift-hello
Build complete! (1.36s)Check that the generated swift-hello binary is the correct type:
> file .build/debug/swift-hello
.build/debug/swift-hello: ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, with debug_info, not strippedThen, it should be ready to deploy and run directly on the target! In my case I used a Raspberry Pi 3B+ with Raspberry Pi OS 12 (32-bit) installed. To be able to run the Swift app, the *.tar.gz that was output from building needs to be copied to the board and installed:
user@host:swift-5.10-linux-armv7 $ scp swift-5.10.1-linux-armv7.tar.gz user@192.168.0.23:~
user@host:swift-5.10-linux-armv7 $ ssh user@192.168.0.23:~
user@rpi3-bookworm32:~ $ sudo tar -xf swift-5.10.1-linux-armv7.tar.gz --directory /Next, the swift-hello binary can be copied to the board, then executed:
user@host:swift-hello $ scp .build/debug/swift-hello user@192.168.0.23:~
user@host:swift-5.10-linux-armv7 $ ssh user@192.168.0.23:~
user@rpi3-bookworm32:~ $ ./swift-hello
Hello, world!And there you go. A completely working armv7 build for the Raspberry Pi and a working binary as well.
My hope is that this guide can be used as a sort of “starting point” for building Swift for cross compiling to armv7 hosts. Since we don’t have an official armv7 build for Ubuntu/Debian/RHEL yet, this is the best we have to be able to at least “generate” something that can be used until then. Of course, I dearly hope that eventually this could become official, since there are official distributions that still support armhf and armv7 out in the wild.
