In order to deploy embedded software using Guix we first need to teach Guixhow to cross-compile it. Since Guix builds everything from source, thismeans we must teach Guix how to build our cross-compilation toolchain.
The Zephyr Project uses its own fork of GCC with custom configs forthe architectures supported by the project. In this article, wedescribe the cross-compilation toolchain we defined for Zephyr; it isimplemented as a Guixchannel.
Zephyr is a real-time operating system from the Linux Foundation.It aims to provide a common environment which can target even the mostresource constrained devices.
Zephyr introduces a module system which allows third parties to share codein a uniform way. Zephyr uses CMake to perform physical component compositionof these modules. It searches the filesystem and generates scripts whichthe toolchain will use to successfully combine those components into afirmware image.
The fact that Zephyr provides this mechanism is one reason I chose totarget it in the first place.
This separation of modules in an embedded context is a really great thing.It brings many of the advantages that it brings to the Linux world such ascode re-use, smaller binaries, more efficient cache/RAM usage, etc.It also allows us to work as independent groups and composecontributions from many teams.
It also brings all of the complexity. Suddenly most of the problemsthat plague traditional deployment now apply to our embeddedsystem. The fact that the libraries are statically linked at compiletime instead of dynamically at runtime is simply an implementation detail.I say most because everything is statically linked so there is no runtimecomponent discovery that needs to be accounted for.
Toolchains are responsible for taking high level descriptions of programsand lowering them down to a series of equivalent machine instructions.This process involves more than just a compiler. The compiler uses theGNU Binutilsto manipulate its internal representation down to a given architecture.It also needs the use of the C standard library as well as a few other librariesneeded for some compiler optimizations.
The C library provides the interface to the underlying kernel. Systemcalls like write and read are provided by GNU C Library(glibc) on most distributions.
In embedded systems, smaller implementations like RedHat'snewlib andnewlib-nano are used.
In order to compile GCC we need a C library that's been compiled forour target architecture. How can we cross compile our C library if weneed our C library to build a cross compiler? The solution is to builda simpler compiler that doesn't require the C library to function.It will not be capable of as many optimizations and it will be very slow,however it will be able to build the C libraries as well as the complete versionof GCC.
In order to build the simpler compiler we need to compile the Binutils towork with our target architecture.Binutils can be bootstrapped with our host GCC and have no target dependencies.More information is available in thisarticle.
Doesn't sound so bad right? It isn't... in theory.However internet forums since time immemorial have beenlittered with the laments of those who came before.From incorrect versions of ISL to the wrong C library being linkedor the host linker being used, etc.The one commonality between all of these issues is the environment.Building GCC is difficult because isolating build environments is hard.
In fact as of v0.14.2, the Zephyr “software development kit” (SDK) repository took down the buildinstructions and posted a sign that read"Building this is too complicated, don't worry about it."(I'm paraphrasing, butnot by much.)
We will neatly sidestep all of these problems and notrisk destroying or polluting our host system with garbageby using Guix to manage our environments for us.
Our toolchain only requires the first pass compiler becausenewlib(-nano) is statically linked and introduced to the toolchainby normal package composition.
All of the base packages are defined inzephyr/packages/zephyr.scm.Zephyr modules (coming soon!) are defined inzephyr/packages/zephyr-xyz.scm,following the pattern of other module systems implemented by Guix.
First thing we need to build is the arm-zephyr-eabi binutils.This is very easy in Guix.
(define-public arm-zephyr-eabi-binutils (let ((xbinutils (cross-binutils "arm-zephyr-eabi"))) (package (inherit xbinutils) (name "arm-zephyr-eabi-binutils") (version "2.38") (source (origin (method git-fetch) (uri (git-reference (url "https://github.com/zephyrproject-rtos/binutils-gdb") (commit "6a1be1a6a571957fea8b130e4ca2dcc65e753469"))) (file-name (git-file-name name version)) (sha256 (base32 "0ylnl48jj5jk3jrmvfx5zf8byvwg7g7my7jwwyqw3a95qcyh0isr")))) (arguments `(#:tests? #f ,@(substitute-keyword-arguments (package-arguments xbinutils) ((#:configure-flags flags) `(cons "--program-prefix=arm-zephyr-eabi-" ,flags))))) (native-inputs (modify-inputs (package-native-inputs xbinutils) (prepend texinfo bison flex gmp dejagnu))) (home-page "https://zephyrproject.org") (synopsis "Binutils for the Zephyr RTOS"))))
The functioncross-binutilsreturns a package which has been configured for the given GNU triplet.We simply inherit that package and replace the source. The Zephyr buildsystem expects the binutils to be prefixed with arm-zephyr-eabi- whichis accomplished by adding another flag to the #:configure-flagsargument.
We can test our package definition using the -L flag with guix buildto add our packages.
$ guix build -L guix-zephyr zephyr-binutils/gnu/store/...-zephyr-binutils-2.38
This directory contains the results of make install.
This one is a bit more involved. Don't be afraid!This version of GCC wants ISL version 0.15. It's easy enoughto make that happen. Inherit the current version of ISL and swapout the source and update the version. For most packages the build process doesn'tchange that much between versions.
(define-public isl-0.15 (package (inherit isl) (version "0.15") (source (origin (method url-fetch) (uri (list (string-append "mirror://sourceforge/libisl/isl-" version ".tar.gz"))) (sha256 (base32 "11vrpznpdh7w8jp4wm4i8zqhzq2h7nix71xfdddp8xnzhz26gyq2"))))))
Like the binutils, there is a cross-gccfunctionfor creating cross-GCC packages. This one accepts keywords specifyingwhich binutils and libc to use. If libc isn't given (like here), gcc isconfigured with many options disabled to facilitate being built withoutlibc. Therefore we need to add the extra options we want (I got themfrom the SDK configuration scripts in the sdk-ngGit repository as well as thecommits to use for each of the tools).
(define-public gcc-arm-zephyr-eabi-12 (let ((xgcc (cross-gcc "arm-zephyr-eabi" #:xbinutils zephyr-binutils))) (package (inherit xgcc) (version "12.1.0") (source (origin (method git-fetch) (uri (git-reference (url "https://github.com/zephyrproject-rtos/gcc") (commit "0218469df050c33479a1d5be3e5239ac0eb351bf"))) (file-name (git-file-name (package-name xgcc) version)) (sha256 (base32 "1s409qmidlvzaw1ns6jaanigh3azcxisjplzwn7j2n3s33b76zjk")) (patches (search-patches "gcc-12-cross-environment-variables.patch" "gcc-cross-gxx-include-dir.patch")))) (native-inputs (modify-inputs (package-native-inputs xgcc) ;; Get rid of stock ISL (delete "isl") ;; Add additional dependencies that xgcc doesn't have ;; including our special ISL (prepend flex isl-0.15))) (arguments (substitute-keyword-arguments (package-arguments xgcc) ((#:phases phases) `(modify-phases ,phases (add-after 'unpack 'fix-genmultilib (lambda \_ (patch-shebang "gcc/genmultilib"))) (add-after 'set-paths 'augment-CPLUS\_INCLUDE\_PATH (lambda* (#:key inputs #:allow-other-keys) (let ((gcc (assoc-ref inputs "gcc"))) ;; Remove the default compiler from CPLUS\_INCLUDE\_PATH to ;; prevent header conflict with the GCC from native-inputs. (setenv "CPLUS\_INCLUDE\_PATH" (string-join (delete (string-append gcc "/include/c++") (string-split (getenv "CPLUS\_INCLUDE\_PATH") #\:)) ":")) (format #t "environment variable `CPLUS\_INCLUDE\_PATH' changed to `a`%" (getenv "CPLUS\_INCLUDE\_PATH"))))))) ((#:configure-flags flags) ;; The configure flags are largely identical to the flags used by the ;; "GCC ARM embedded" project. `(append (list "--enable-multilib" "--with-newlib" "--with-multilib-list=rmprofile" "--with-host-libstdcxx=-static-libgcc -Wl,-Bstatic,-lstdc++,-Bdynamic -lm" "--enable-plugins" "--disable-decimal-float" "--disable-libffi" "--disable-libgomp" "--disable-libmudflap" "--disable-libquadmath" "--disable-libssp" "--disable-libstdcxx-pch" "--disable-nls" "--disable-shared" "--disable-threads" "--disable-tls" "--with-gnu-ld" "--with-gnu-as" "--enable-initfini-array") (delete "--disable-multilib" ,flags))))) (native-search-paths (list (search-path-specification (variable "CROSS\_C\_INCLUDE\_PATH") (files '("arm-zephyr-eabi/include"))) (search-path-specification (variable "CROSS\_CPLUS\_INCLUDE\_PATH") (files '("arm-zephyr-eabi/include" "arm-zephyr-eabi/c++" "arm-zephyr-eabi/c++/arm-zephyr-eabi"))) (search-path-specification (variable "CROSS\_LIBRARY\_PATH") (files '("arm-zephyr-eabi/lib"))))) (home-page "https://zephyrproject.org") (synopsis "GCC for the Zephyr RTOS"))))
This GCC can be built like so.
$ guix build -L guix-zephyr gcc-cross-sans-libc-arm-zephyr-eabi/gnu/store/...-gcc-cross-sans-libc-arm-zephyr-eabi-12.1.0-lib/gnu/store/...-gcc-cross-sans-libc-arm-zephyr-eabi-12.1.0
Great! We now have our stage-1 compiler.
The newlib package package is quite straight forward (relatively).It is mostly adding in the relevent configuration flags and patchingthe files the patch-shebangs phase missed.
(define-public zephyr-newlib (package (name "zephyr-newlib") (version "3.3") (source (origin (method git-fetch) (uri (git-reference (url "https://github.com/zephyrproject-rtos/newlib-cygwin") (commit "4e150303bcc1e44f4d90f3489a4417433980d5ff"))) (sha256 (base32 "08qwjpj5jhpc3p7a5mbl7n6z7rav5yqlydqanm6nny42qpa8kxij")))) (build-system gnu-build-system) (arguments `(#:out-of-source? #t #:configure-flags '("--target=arm-zephyr-eabi" "--enable-newlib-io-long-long" "--enable-newlib-io-float" "--enable-newlib-io-c99-formats" "--enable-newlib-retargetable-locking" "--enable-newlib-lite-exit" "--enable-newlib-multithread" "--enable-newlib-register-fini" "--enable-newlib-extra-sections" "--disable-newlib-wide-orient" "--disable-newlib-fseek-optimization" "--disable-newlib-supplied-syscalls" "--disable-newlib-target-optspace" "--disable-nls") #:phases (modify-phases %standard-phases (add-after 'unpack 'fix-references-to-/bin/sh (lambda \_ (substitute# '("libgloss/arm/cpu-init/Makefile.in" "libgloss/arm/Makefile.in" "libgloss/libnosys/Makefile.in" "libgloss/Makefile.in") (("/bin/sh") (which "sh"))) #t))))) (native-inputs `(("xbinutils" ,zephyr-binutils) ("xgcc" ,gcc-arm-zephyr-eabi-12) ("texinfo" ,texinfo))) (home-page "https://www.sourceware.org/newlib/") (synopsis "C library for use on embedded systems") (description "Newlib is a C library intended for use on embeddedsystems. It is a conglomeration of several library parts that are easilyusable on embedded products.") (license (license:non-copyleft "https://www.sourceware.org/newlib/COPYING.NEWLIB"))))
And the build.
$ guix build -L guix-zephyr zephyr-newlib/gnu/store/...-zephyr-newlib-3.3
Mostly complete. libstdc++ does not build becausearm-zephyr-eabi is not arm-none-eabi so a dynamic link check isperformed/failed. I cannot figure out how crosstool-ng handles this.
Now that we've got the individual tools it's time to create our complete toolchain.For this we need to do some package transformations.Because these transformations are going to have to be done for every combination ofbinutils/gcc/newlib it is best to create a function which we can reuse for every versionof the SDK.
(define (arm-zephyr-eabi-toolchain xgcc newlib version) "Produce a cross-compiler zephyr toolchain package with the compiler XGCC and the C\n library variant NEWLIB." (let ((newlib-with-xgcc (package (inherit newlib) (native-inputs (modify-inputs (package-native-inputs newlib) (replace "xgcc" xgcc)))))) (package (name (string-append "arm-zephyr-eabi" (if (string=? (package-name newlib-with-xgcc) "newlib-nano") "-nano" "") "-toolchain")) (version version) (source #f) (build-system trivial-build-system) (arguments '(#:modules ((guix build union) (guix build utils)) #:builder (begin (use-modules (ice-9 match) (guix build union) (guix build utils)) (let ((out (assoc-ref %outputs "out"))) (mkdir-p out) (match %build-inputs (((names . directories) ...) (union-build (string-append out "/arm-zephyr-eabi") directories))))))) (inputs `(("binutils" ,zephyr-binutils) ("gcc" ,xgcc) ("newlib" ,newlib-with-xgcc))) (synopsis "Complete GCC tool chain for ARM zephyrRTOS development") (description "This package provides a complete GCC tool chain for ARM bare metal development with zephyr rtos. This includes the GCC arm-zephyr-eabi cross compiler and newlib (or newlib-nano) as the C library. The supported programming language is C.") (home-page (package-home-page xgcc)) (license (package-license xgcc)))))
This function creates a special package which consists of the toolchainin a special directory hierarchy, i.e arm-zephyr-eabi/.Our complete toolchain definition looks like this.
(define-public arm-zephyr-eabi-toolchain-0.15.0 (arm-zephyr-eabi-toolchain gcc-arm-zephyr-eabi-12 zephyr-newlib "0.15.0"))
To build:
$ guix build -L guix-zephyr arm-zephyr-eabi-toolchain/gnu/store/...-arm-zephyr-eabi-toolchain-0.15.0
Note: Guix now includes a mechanism to describeplatformsat a high level, and which the
--systemand--targetbuildoptionsbuild upon. It is not used here but could be a way to betterintegrate Zephyr support in the future.
Zephyr uses CMake as its build system. It contains numerous CMake files in both the so-called ZEPHYR\_BASE,the zephyr source code repository, as well as a handful in the SDK which help select the correct toolchainfor a given board.
There are standard locations the build system will look for the SDK. We are not using any of them.Our SDK lives in the store, immutable forever.According to the Zephyr documentation, the variable ZEPHYR\_SDK\_INSTALL\_DIR needs to point to our custom spot.
We also need to grab the CMake files from therepositoryand create a file, sdk\_version, whichcontains the version string ZEPHYR\_BASE uses to find a compatible SDK.
Along with the SDK proper we need to include a number ofpython packages required by the build system.
(define-public zephyr-sdk (package (name "zephyr-sdk") (version "0.15.0") (home-page "https://zephyrproject.org") (source (origin (method git-fetch) (uri (git-reference (url "https://github.com/zephyrproject-rtos/sdk-ng") (commit "v0.15.0"))) (file-name (git-file-name name version)) (sha256 (base32 "04gsvh20y820dkv5lrwppbj7w3wdqvd8hcanm8hl4wi907lwlmwi")))) (build-system trivial-build-system) (arguments `(#:modules ((guix build union) (guix build utils)) #:builder (begin (use-modules (guix build union) (ice-9 match) (guix build utils)) (let ((out (assoc-ref %outputs "out")) (cmake-scripts (string-append (assoc-ref %build-inputs "source") "/cmake")) (sdk-out (string-append out "/zephyr-sdk-0.15.0"))) (mkdir-p out) (match (assoc-remove! %build-inputs "source") (((names . directories) ...) (union-build sdk-out directories))) (copy-recursively cmake-scripts (string-append sdk-out "/cmake")) (with-directory-excursion sdk-out (call-with-output-file "sdk\_version" (lambda (p) (format p "0.15.0")))))))) (propagated-inputs (list arm-zephyr-eabi-toolchain-0.15.0 zephyr-binutils dtc python-3 python-pyelftools python-pykwalify python-pyyaml python-packaging)) (native-search-paths (list (search-path-specification (variable "ZEPHYR\_SDK\_INSTALL\_DIR") (separator #f) (files '(""))))) (synopsis "Zephyr SDK") (description "zephyr-sdk contains bundles a complete gcc toolchain as wellas host tools like dtc, openocd, qemu, and required python packages.") (license license:apsl2)))
In order to test we will need an environment with the SDK installed.We can take advantage of guix shell to avoid installing test packages intoour home environment. This way if it causes problems we can just exit the shelland try again.
guix shell -L guix-zephyr zephyr-sdk cmake ninja git
ZEPHYR\_BASE can be cloned into a temporary workspace to test our toolchain functionality.(For now. Eventually we will need to create a package for zephyr-base thatour Guix zephyr-build-system can use.)
mkdir /tmp/zephyr-projectcd /tmp/zephyr-projectgit clone https://github.com/zephyrproject-rtos/zephyrexport ZEPHYR\_BASE=/tmp/zephyr-project/zephyr
In order to build for the test board (k64f in this case) we need to get a hold of the vendorHardware Abstraction Layers and CMSIS.(These will also need to become Guix packages to allow the build system to compose modules).
git clone https://github.com/zephyrproject-rtos/hal\_nxp && \git clone https://github.com/zephyrproject-rtos/cmsis
To inform the build system about this module we pass it in with -DZEPHYR\_MODULES= which isa semicolon separated list of paths containing a module.yml file.
To build the hello world sample we use the following incantation.
cmake -Bbuild $ZEPHYR\_BASE/samples/hello\_world \ -GNinja \ -DBOARD=frdm\_k64f \ -DBUILD\_VERSION=3.1.0 \ -DZEPHYR\_MODULES="/tmp/zephyr-project/hal\_nxp;/tmp/zephyr-project/cmsis" \ && ninja -Cbuild
If everything is set up correctly we will end up with a ./builddirectory with all our build artifacts. The SDK is correctly installed!
A customized cross toolchain is one of the most difficult pieces ofsoftware to build. Using Guix, we do not need to be afraid of thecomplexity! We can fiddle with settings, swap out components, and dothe most brain dead things to our environments without a care in theworld. Just exit the environment and it's like it never happened atall.
It highlights one of my favorite aspects of Guix, every package is aworking reference design for you to modify and learn from.
GNU Guix is a transactional package manager andan advanced distribution of the GNU system that respects userfreedom.Guix can be used on top of any system running the Hurd or the Linuxkernel, or it can be used as a standalone operating system distributionfor i686, x86\_64, ARMv7, AArch64 and POWER9 machines.
In addition to standard package management features, Guix supportstransactional upgrades and roll-backs, unprivileged package management,per-user profiles, and garbage collection. When used as a standaloneGNU/Linux distribution, Guix offers a declarative, stateless approach tooperating system configuration management. Guix is highly customizableand hackable through Guileprogramming interfaces and extensions to theScheme language.