Up to Main Index                                       Up to Annexed Works

         BUILDING THE GO TOOLCHAIN FROM SOURCE ON A RASPBERRY PI ZERO

INTRODUCTION
‾‾‾‾‾‾‾‾‾‾‾‾
Building Go from source on a Raspberry Pi, especially a Raspberry Pi Zero, can
be a challenge. These instructions are from my experience building the Go
toolchain (compiler, other tools, standard library) on machines with limited
resources. The toolchain sources are downloaded from https://golang.org. This
is not about compiling Go programs from code you write…

You are expected to have at least a little experience with the Linux command
line. If you can open a terminal window and type commands you should be okay.
These instructions may seem overly verbose. However, they attempt to explain
what to do and why. This should make adapting the instructions for your setup,
or for different tiny computers, much easier.

The original Raspberry Pi Zero, and Zero W, are cheap and tiny computers with
limited resources that are capable of running a full Linux distribution. They
have a single core BCM2835 32-bit processor running at 1GHz with 512Mb RAM.
The storage is usually limited to an SD card, although an SSD or thumb-drive
can be attached via the single micro USB port using an OTG adapter.

If Go can be built on these diminutive systems, then we should be able to
build Go on any model of Raspberry Pi or similar single-board computer.

For the build described here, a Raspberry Pi Zero W was used. The operating
system used was an official Raspberry Pi OS image. The version of Go built was
Go 1.23.3. The SDCard used in the Raspberry Pi was a 16Gb Class 10 A1 SanDisk.

A previous build of Go 1.21.5 was used to build the Go 1.23.3 toolchain. If
there is no previous or recent version of Go installed, the easiest method is
to download a recent binary release of Go. If that is not possible then you
are in for a long haul:


    · download and build the latest Go 1.4.x
    · use Go  1.4.x to build the latest Go 1.19.x
    · use Go 1.19.x to build the latest Go 1.20.x
    · use Go 1.20.x to build Go 1.23.3


Depending on which Go compiler versions you already have available, hopefully
don’t have to build the whole Go 1.4, Go 1.19 & Go 1.20 chain of compilers :(

Additional information on building Go from source, and which compiler versions
are needed to compile different versions of Go, can be found on the official
Go website: https://go.dev/doc/install/source

Feel free to take and adapt these notes as you see fit. Feedback in the form
of comments, improvements or errata is always welcome: diddymus@wolfmud.org


OVERVIEW
‾‾‾‾‾‾‾‾
A quick overview of the following process:


    · Reduce video memory used
    · Setup swap space
    · Create temporary directory
    · Unpack Go toolchain source
    · Setup stacks
    · Build Go toolchain
    · Setup PATH and symlinks
    · Troubleshooting tips


REDUCE VIDEO MEMORY
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
The first step should be to setup the RAM reserved for graphics to be as small
as possible. This frees up more memory for the operating system and Go build
to use. As root, the current gpu_mem setting can be checked using:


    >vcgencmd get_mem gpu
    gpu=16M
    >


If it is not 16M then edit the /boot/config.txt file. Set the gpu_mem setting
to 16, take note of its current value so it can be restored after the build:


    gpu_mem=16


If your config.txt does not contain a gpu_mem line, add it to the end of the
file. A reboot is required for the change to take effect. After rebooting,
check the value of gpu_mem again, reported by the vcgencmd command above, and
make sure it has been updated.

Once rebooted, as many background services as possible should be stopped to
free up additional memory. Which services are running on your Raspberry Pi
will depend on what software has been installed. Logging in to the Raspberry
Pi from the console or over SSH, instead of running a graphical interface, is
highly recommended. This will free up more memory for the build. The more free
memory the quicker the build will be.


SETUP SWAP SPACE
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
A small swap space will be provided by a memory backed zram device that
compresses and decompresses its content automatically. Setup the swap space,
as root, using:


    >swapoff -a
    >zramctl --find -a lz4 -s 32M
    /dev/zram0
    >mkswap /dev/zram0
    Setting up swapspace version 1, size = 32 MiB (33550336 bytes)
    no label, UUID=d43824c2-feab-4b48-b162-049d129dbac3
    >swapon --priority 100 /dev/zram0
    >


Note that if the size of the zram device is too large, the build will spend
more time swapping as less of the build can be kept in memory.

Building Go will require more swap space than is available on the zram device
alone. For additional swap space a temporary 1Gb swap file will be used. To
create and mount a temporary swap file, as root, use:


    >dd if=/dev/zero of=/tmpswap bs=1M count=1024
    2097152+0 records in
    2097152+0 records out
    1073741824 bytes (1.1 GB, 1.0 GiB) copied, 558.15 s, 1.9 MB/s
    >mkswap /tmpswap
    Setting up swapspace version 1, size = 1024 MiB (1073737728 bytes)
    no label, UUID=45122e1d-9bcd-4bf4-81cb-0a5b3aec51ec
    >swapon /tmpswap
    >


Check the zram and swap file are now being used:


    >swapon -s
    Filename      Type            Size            Used            Priority
    /dev/zram0    partition       32764           0               100
    /tmpswap      file            1048572         0               -2
    >


This allows frequent small swaps to memory with only the overflow going to the
SDCard. This should speed up the build and help preserve the SDCard.


TEMPORARY DIRECTORY
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
A temporary directory will be needed for the build. As /tmp is normally mapped
to tmpfs and memory backed, /tmp is unsuitable. Instead a temporary directory
in the user’s home directory is used. As a normal user:


    >mkdir ~/tmp
    >


UNPACK TOOLCHAIN SOURCE
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
The Go source code should be downloaded and unpacked into a directory named as
per the release being built. In this case go1.23.3 which has been downloaded
from https://go.dev/dl/ to the user’s downloads folder:


    >cd ~
    >mkdir go1.23.3
    >cd go1.23.3
    >tar --strip-components=1 -zxf ~/downloads/go1.23.3.src.tar.gz
    >


Note: prior to Go 1.21.0 the first release of a Go version was named without a
minor number. For example: instead of ‘go1.20.0’, ‘go1.20’ was used. We always
want a minor number and append ‘.0’ to the directory name if required. See the
later discussion on symbolic links for the reasoning as to why we always want
to use directories with a minor number.


SETUP STACKS
‾‾‾‾‾‾‾‾‾‾‾‾
By default, stacks are created with a size of 8Mb, however the limit can be
lowered saving memory during the build. This will also reduce the build time
considerably. A setting of 1Mb seems to work well. To set, and then check, the
initial stack limit, as a normal user:


    >ulimit -s 1024


The current setting can be checked using:


    >ulimit -s
    1024
    >


BUILD GO TOOLCHAIN
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
For the build we use the tee command so we can keep an eye on the progress and
also have the output sent to ./compile.log in case we need to troubleshoot. To
build Go, using the temporary ~/tmp directory, as a normal user:


    >cd ~/go1.23.3/src
    >time GO_TEST_TIMEOUT_SCALE=10 CGO_ENABLE=1 GOROOT_BOOTSTRAP=~/go1.21.5 \
      GOARM=6 GOTMPDIR=~/tmp TMPDIR=~/tmp ./all.bash | tee ./compile.log
    :
    : wait 6 hours, the follow messages will start to appear during this time…
    :
    Building Go cmd/dist using /home/diddymus/go1.21.5. (go1.21.5 linux/arm)
    Building Go toolchain1 using /home/diddymus/go1.21.5.
    Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
    Building Go toolchain2 using go_bootstrap and Go toolchain1.
    Building Go toolchain3 using go_bootstrap and Go toolchain2.
    Building packages and commands for linux/arm.
    :
    : the toolchain is compiled, next the tests are run…
    :
    ##### Test execution environment.
    # GOARCH: arm
    # CPU: ARMv6-compatible processor rev 7 (v6l)
    # GOOS: linux
    # OS Version: Linux 6.1.21+ #1642 Mon Apr  3 17:19:14 BST 2023 armv6l

    ##### Testing packages.
    ok      archive/tar     2.083s
    ok      archive/zip     2.478s
    ok      bufio   0.841s
    ok      bytes   4.598s
    ok      cmp     0.020s
    :
    : finally the build will finish…
    :
    ALL TESTS PASSED
    ---
    Installed Go for linux/arm in /home/diddymus/go1.23.3
    Installed commands in /home/diddymus/go1.23.3/bin
    *** You need to add /home/diddymus/go1.23.3/bin to your PATH.

    real    351m12.649s
    user    298m32.391s
    sys     27m0.611s
    >


Once the build has completed, revert the change to gpu_mem in /boot/config.txt
and set gpu_mem back to its original value and reboot. After the reboot the
changes to swap will be lost and ulimit settings returned to normal. As root
the /tmpswap file can also be removed.

If the compile fails, you can look at the build log in compile.log to find out
what went wrong. See also the troubleshooting tips section below.


TROUBLESHOOTING TIPS
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
I had to specify ARM=6 when building the toolchain for the Raspberry Pi Zero.
However, this does not always work and I have had to use ARM=5 before. This
seems to depend a lot on the current kernel, libraries and how they have been
compiled for the current version of Raspberry Pi OS. If compiling with ARM=6
fails then try again with ARM=5.

This weirdness can also be seen when compiling on a Raspberry Pi 4 with a
32-bit Raspberry Pi OS installed — a 64-bit CPU running a 32-bit OS. The
compiler tries to build Go for the arm64 CPU but we need 32-bit binaries.
Setting GOHOSTARCH=arm for the compile fixes this.

I think this is also the reason why the runtime/vdso_test.go fails on a
Raspberry Pi 4 with a 32-bit OS:


    vdso_test.go:124: did not use VDSO system call


To work around this I edited src/runtime/vdso_test.go and at the end of the
function TestUsingVDSO changed:


    // The Go program used the system call but the C
    // program did not. This is a VDSO failure for Go.
    t.Errorf("did not use VDSO system call")


To be Log instead of Errorf:


    // The Go program used the system call but the C
    // program did not. This is a VDSO failure for Go.
    t.Log("did not use VDSO system call")


Save the change and compile… again.


PATH AND SYMLINKS
‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
For myself, ~/golang is a symlink to a major Go version, currently Go 1.21 for
this build, and this is a symlink to the latest minor Go version, in this case
Go 1.21.5:


  >ls -ld ~/go*
  lrwxrwxrwx  1 diddymus diddymus    8 Dec 12  2023 go1.21 -> go1.21.5
  drwxr-xr-x 10 diddymus diddymus 4096 Dec 11  2023 go1.21.5
  lrwxrwxrwx  1 diddymus diddymus    6 Nov  9 17:29 golang -> go1.21
  >


For the newly compiled toolchain I symlink go1.23 to go1.23.3:


  >ln -s go1.23.3 go1.23
  >ls -ld ~/go*
  lrwxrwxrwx  1 diddymus diddymus    8 Dec 12  2023 go1.21 -> go1.21.5
  drwxr-xr-x 10 diddymus diddymus 4096 Dec 11  2023 go1.21.5
  lrwxrwxrwx  1 diddymus diddymus    6 Dec 11  2023 golang -> go1.21
  lrwxrwxrwx  1 diddymus diddymus    8 Nov  9 17:28 go1.23 -> go1.23.3
  drwxrwxr-x 10 diddymus diddymus 4096 Nov  7 12:16 go1.23.3
  >


Then to switch from go1.21 to go1.23 I update the symlink for golang:


  >rm golang
  >ln -s go1.23 golang
  >ls -ld ~/golang
  lrwxrwxrwx  1 diddymus diddymus    8 Dec 12  2023 go1.21 -> go1.21.5
  drwxr-xr-x 10 diddymus diddymus 4096 Dec 11  2023 go1.21.5
  lrwxrwxrwx  1 diddymus diddymus    6 Nov  9 17:29 golang -> go1.23
  lrwxrwxrwx  1 diddymus diddymus    8 Nov  9 17:28 go1.23 -> go1.23.3
  drwxrwxr-x 10 diddymus diddymus 4096 Nov  7 12:16 go1.23.3
  >


The symlink ~/golang/bin should be added to the PATH environment variable in
~/.bashrc so that the Go version ~/golang currently points to can be found:


    export PATH=~/bin:$PATH:~/golang/bin


This makes switching between Go versions very easy. To change major version I
update the symlink for golang. To change minor version I update the symlink
for the go1.x version. For example, to switch from Go 1.23 back to Go 1.21:


    >go version
    go version go1.23.3 linux/arm
    >rm golang
    >ln -s go1.21 golang
    >go version
    go version go1.21.5 linux/arm
    >


For each new Go release the build is put into a go1.x.x directory and the
go1.x symlink added or updated. The golang symlink is only updated for new
go1.x releases, or if I need a specific version of Go for testing.


FINISH
‾‾‾‾‾‾
The instructions presented here can be used for other models of Raspberry Pi
and other small, low power, resource constrained systems. In which case you
may not need additional swap, a separate ~/tmp directory or reduced stacks.

These are the build times for Go 1.23.3, including running all the tests, on
different Raspberry Pi models, with the tweaks used, and a desktop PC for
comparison:


            System/OS/Memory     Time HMS  Tweaks Used
            -------------------- --------  ------------------------
            RPi0 32-bit   512Mb: 05:51:12  tmpdir, ulimit, swapfile
            RPI3 32-bit     1Gb:    34:49  tmpdir, ulimit, swapfile
            RPi4 32-bit     4Gb:    25:55  tmpdir
            RPi4 64-bit     8Gb:    38:10  tmpdir
            Desktop 64-bit 64Gb:     3:10


Except for the Raspberry Pi Zero, which uses an SDCard, the other Raspberry Pi
models used a USB to SATA adapter with a WD Green SSD.

The desktop used was an Intel i9-12900T — 12 core/24 threads (8P/16T @ 4.8Ghz,
4E/8T @ 3.6Ghz) with 64Gb RAM.

--
Diddymus


  Up to Main Index                                       Up to Annexed Works