title:Building Toolchains with Guix
author:Mitchell Schmeisser <mitchellschmeisser@librem.one>
date: 2023-02-23 23:00
tags: Guix toolchains
summary: Guix is awesome at building toolchains.
---


Today's post is a guest post from my new internet friend Mitchell. We met on the
#guix irc channel, and I offered to post a few of his blog posts on this blog.  Without further ado,
here is Michell's first blog post (it's pretty fantastic)!

# Overview

In order to deploy embedded software using Guix we first need to teach Guix
how to build it. Since Guix bootstraps everything this means we must teach Guix
how to build our toolchain.

The [Zephyr Project](https://zephyrproject.org) uses its own fork of GCC with custom configs for
the architectures supported by the project.


# Anatomy of a toolchain

Toolchains are responsible for taking high level descriptions of programs and
lowering them down to a series of equivalent machine instructions. This process
involves more than just a compiler. The compiler uses the `binutils` to
manipulate it&rsquo;s internal representation down to a given architecture. It
also needs the C standard library as well as a few other libraries needed for
some compiler optimizations.

The C library provides the interface to the underlying kernel. System calls like `write`
and `read` are provided by `Glibc` on most Linux distributions.

In embedded systems smaller implementations like `newlib` and `newlib-nano` are used.


# Bootstrapping a Toolchain

In order to compile GCC we need a C library that&rsquo;s been compiled for
our target architecture. How can we cross compile our C library if we
need our C library to build a cross compiler? The solution is to build
a simpler compiler that doesn&rsquo;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 version
of GCC.

In order to build the simpler compiler we need to compile the `binutils` to
work with our target architecture.
The `binutils` can be bootstrapped with our host GCC and have no target dependencies.

[For more information read this.](https://crosstool-ng.github.io/docs/toolchain-construction/)

Doesn&rsquo;t sound so bad right? It isn&rsquo;t&#x2026; in theory.
However internet forums since time immemorial have been
littered with the laments of those who came before.
From incorrect versions of `ISL` to the wrong C library being linked
or 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 SDK repository took down the build
instructions and posted a sign that read &ldquo;Building this is too
complicated, don&rsquo;t worry about it.&rdquo; (I&rsquo;m paraphrasing, but
[not by
much](https://github.com/zephyrproject-rtos/sdk-ng/tree/v0.14.2#build-process).)

We will neatly side step all of these problems and not
risk destroying or polluting our host system with garbage
by using Guix to manage our environments for us.

Our toolchain only requires the first pass compiler because
newlib(-nano) is statically linked and introduced to the toolchain
by normal package composition.


# Defining the Packages

All of the base packages are defined in `zephyr/packages/zephyr.scm`.
Zephyr modules are defined in `zephyr/packages/zephyr-xyz.scm`, following
the pattern of other module systems implemented by Guix.


## Binutils

First thing we need to build is the `arm-zephyr-eabi` binutils.
This is very easy in Guix.

    (define-module (zephyr packages zephyr)
        #:use-module (guix packages)
    
    (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
       (append
    	(list texinfo
    	  bison
    	  flex
    	  gmp
    	  dejagnu)
    	(package-native-inputs xbinutils)))
      (home-page "https://zephyrproject.org")
      (synopsis "binutils for zephyr RTOS"))))

The function `cross-binutils` returns a package which has been
configured for the given gnu triplet.  We simply inherit that package
and replace the source.
The zephyr build system expects the binutils to be prefixed with
`arm-zephyr-eabi-` which is accomplished by adding another flag to the
`#:configure-flags` argument.

We can test our package definition using the `-L` flag with `guix build`
to add our packages.

    guix build -L guix-zephyr zephyr-binutils

    /gnu/store/a947nb4rb2vymz2gaqnafgm1bsq4ipqp-zephyr-binutils-2.38

This directory contains the results of `make install`.


## GCC sans libc

This one is a bit more involved. Don&rsquo;t be afraid!
This version of GCC wants ISL version 0.15. It&rsquo;s easy enough
to make that happen. Inherit the current version of ISL and swap
out the source and update the version. For most packages the build process doesn&rsquo;t
change 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 function for creating cross-gcc packages. This one
accepts keywords specifying which binutils and libc to use. If libc isn&rsquo;t
given (like here), gcc is configured with many options disabled to facilitate
being built without libc. Therefore we need to add the extra options we want (I
got them from the SDK configuration scripts on the [sdk
github](https://github.com/zephyrproject-rtos/sdk-ng) as well as the commits 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
    		  perl
    		  python-3
    		  gmp
    		  isl-0.15
    		  texinfo
    		  python
    		  mpc
    		  mpfr
    		  zlib)))
      (arguments
       (substitute-keyword-arguments (package-arguments xgcc)
    	 ((#:phases phases)
    	  `(modify-phases ,phases
    	     (add-after 'unpack 'fix-genmultilib
    	   (lambda _
    		 (substitute* "gcc/genmultilib"
    		   (("#!/bin/sh") (string-append "#!" (which "sh"))))
    		 #t))
    
    	     (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"))
    		   #t)))))
    
    	 ((#: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 zephyr RTOS"))))

This GCC can be built like so.

    guix build -L guix-zephyr gcc-cross-sans-libc-arm-zephyr-eabi

    /gnu/store/qmp8bzmwwimw0r6fh165hgfhkxkxilpj-gcc-cross-sans-libc-arm-zephyr-eabi-12.1.0-lib
    /gnu/store/38rli0rbn7ksmym3wq99cr4p2cjdz4a7-gcc-cross-sans-libc-arm-zephyr-eabi-12.1.0

Great! We now have our stage-1 compiler.


## Newlib(-nano)

The newlib package is quite straight forward (relatively).
It is mostly adding in the relevent configuration flags and patching
the 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 embedded
    systems.  It is a conglomeration of several library parts that are easily
    usable 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/4lx37gga1jv3ckykrxsfgwy9slaamln4-zephyr-newlib-3.3


## Complete toolchain

Note that the toolchain is *Mostly* complete. libstdc++ does not build because
\`arm-zephyr-eabi\` is not \`arm-none-eabi\` so a dynamic link check is
performed/failed. I cannot figure out how crosstool-ng handles this.

Anyway, now that we&rsquo;ve got the individual tools it&rsquo;s time to create
our complete toolchain. For this we need to do some package transformations.
Because these transformations are must be done for every combination of
binutils/gcc/newlib, it is best to create a function which we can reuse for
every version of the SDK.

    (define (arm-zephyr-eabi-toolchain xgcc newlib version)
      "Produce a cross-compiler zephyr toolchain package with the compiler XGCC and the C
    library variant NEWLIB."
      (let ((newlib-with-xgcc (package (inherit newlib)
    				   (native-inputs
    				    (alist-replace "xgcc" (list xgcc)
    						   (package-native-inputs newlib))))))
        (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)
    		#t))))))
      (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 toolchain in 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/9jnanr27v6na5qq3dlgljraysn8r1sad-arm-zephyr-eabi-toolchain-0.15.0


# Integrating with Zephyr Build System

Zephyr uses CMake as it&rsquo;s 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 toolchain
for 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
[this
webpage](https://docs.zephyrproject.org/latest/develop/west/without-west.html),
the variable `ZEPHYR_SDK_INSTALL_DIR` needs to point to our custom spot.

We also need to grab the cmake files from the [repository](https://github.com/zephyrproject-rtos/sdk-ng) and create a file `sdk_version` which
contains the version string `ZEPHYR_BASE` uses to find a compatible SDK.

Along with the SDK proper we need to include a number of python 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")))
    	     #t)))))
        (propagated-inputs
         (list
          arm-zephyr-eabi-toolchain-0.15.0
          zephyr-binutils
          dtc))
        (native-search-paths
         (list (search-path-specification
    	    (variable "ZEPHYR_SDK_INSTALL_DIR")
    	    (files '("")))))
        (synopsis "SDK for zephyrRTOS")
        (description "zephyr-sdk contains bundles a complete gcc toolchain as well
    as host tools like dtc, openocd, qemu, and required python packages.")
        (license license:apsl2)))


## Testing

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 into
our home environment. This way, if it causes problems, we can just exit the shell
and 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` that our guix zephyr-build-system can use).

    mkdir /tmp/zephyr-project
    cd /tmp/zephyr-project
    git clone https://github.com/zephyrproject-rtos/zephyr
    export 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 vendor Hardware 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 is
a 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 `./build`
directory with all our build artifacts. The SDK is installed correctly!


