Skip to content

Instantly share code, notes, and snippets.

@ktprograms
Last active November 30, 2021 09:24
Show Gist options
  • Save ktprograms/011000bda16fbcdd5e00002b42d051d5 to your computer and use it in GitHub Desktop.
Save ktprograms/011000bda16fbcdd5e00002b42d051d5 to your computer and use it in GitHub Desktop.

Steps to GHC + Shellcheck on Alpine Arm64

This is everything I had to do in order to get a working GHC running on Alpine aarch64, which I could then use to bootstrap cabal-install and compile shellcheck. I will be compiling GHC 8.10.7 because (a) cabal-install doesn't support GHC 9.x, and (b) there seems to be a linker bug on GHC 9.x.

Note: I am using doas in these instructions (except when in the chroot), but feel free to replace it with sudo

Here's a brief overview of the steps needed:

The errors I got and the explanation for the hacky steps below

  • When linking -no-pie executables with ld.gold against musl, the output executables always segfault. That's why in the configure for GHC it uses ld.bfd
  • No matter the combination of host/target or force setting CrossCompiling = YES in mk/build.mk, after running ./configure the output file mk/config.mk still contains CrossCompiling = NO. The problem with that is partway through the compile make tries to run binaries compiled for the target. That's why I just replaced CrossCompiling = @CrossCompiling@ with CrossCompiling = YES in mk/config.mk.in.
  • GMP (or at least the intree version)'s autoconf script doesn't properly detect cross compilation. That's where the second GHC patch comes in (it passes cross_compiling=yes instead of letting autoconf decide)

Compile a cross-compiling toolchain targeting Alpine

Fortunately, this step is very simple since bootstrap exists. bootstrap is a project with a script that compiles a bunch of dependencies, then also compiles musl-cross-make. The end result is a nice toolchain in the mcmtools/musl/bin/ folder.

First, clone the bootstrap repository:

git clone --depth=1 https://git.zv.io/toolchains/bootstrap.git
cd bootstrap

To keep the build self-contained, I used the docker build script (you'll need docker installed):

cat | git apply <<'EOF'
diff --git a/config.env b/config.env
index 97a6162..9ee8999 100644
--- a/config.env
+++ b/config.env
@@ -1,3 +1,2 @@
 DEST=$HOME/mcmtools
-ARCH=i386-linux-musl
-EMUS=$HOME/mcmtools/emu
+ARCH=aarch64-linux-musl
diff --git a/docker/ubuntu b/docker/ubuntu
index 0010bc4..990d3b5 100755
--- a/docker/ubuntu
+++ b/docker/ubuntu
@@ -11,6 +11,6 @@ set -e;

 apt-get update;
 apt-get -y upgrade;
-apt-get -y install bash bison bzip2 curl coreutils diffutils flex g++ gcc-multilib make perl xz-utils; # bootstrap
+apt-get -y install bash bison bzip2 curl coreutils diffutils flex g++ make perl xz-utils; # bootstrap

 ./bootstrap;
EOF

./docker/ubuntu

mkdir $HOME/mcmtools
mv mcmtools/musl/ $HOME/mcmtools/

You can delete the mcmtools/host/ and mcmtools/sys/ directories since they aren't used in this guide.

One important note: Since this will be built using docker, DO NOT interrupt the compilation as it will need to restart from the beginning.

Create a Debian chroot and install dependencies (including the cross-compiling toolchain)

In order to compile GHC, you need a system with an available GHC. I chose to use Debian since it has good arm64 support.

First, you'll need to install debootstrap:

doas apk add debootstrap

Next, use debootstrap to create a Debian Unstable chroot (installing perl is needed for debootstrap to function):

doas apk add perl
mkdir $HOME/chroot
doas chown root:root $HOME/chroot/
doas debootstrap --arch=arm64 testing $HOME/chroot/ http://deb.debian.org/debian/

Optionally: create a script to mount the virtual file systems into the chroot folder (you MUST replace $HOME with the path to your home directory since this script must be run as root):

cat >mount_chroot.sh <<'EOF'
#!/bin/sh
CHROOT="$HOME/chroot"
mount -t proc proc $CHROOT/proc/
mount -t sysfs sys $CHROOT/sys/
mount -o bind /dev/ $CHROOT/dev/
mount -o bind /dev/pts/ $CHROOT/dev/pts/
mount -o bind /run $CHROOT/run/
EOF

chmod +x mount_chroot.sh
doas ./mount_chroot.sh

Enter the chroot and install deps

doas chroot $HOME/chroot/ bash -l

Add a non-root user and add it to the sudo group (replace $USER with the name you want):

apt install sudo
adduser $USER
adduser $USER sudo

Now, you can exit out of the chroot, and from now on enter the chroot with

doas chroot $HOME/chroot/ su - $USER

In the chroot, enable the experimental repository (A recent enough version of GHC is only available in experimental):

cat | sudo tee -a /etc/apt/sources.list <<'EOF'
deb-src http://deb.debian.org/debian testing main

deb http://deb.debian.org/debian experimental main
deb-src http://deb.debian.org/debian experimental main
EOF

sudo apt update
sudo apt upgrade

Next, install the build dependencies for GHC:

sudo apt build-dep ghc
sudo apt -t experimental install ghc
sudo apt autoremove

Copy the cross-compiling toolchain into the chroot

Exit the chroot. Then copy the $HOME/mcmtools/ folder into $HOME/chroot/home/$USER/:

cp -r $HOME/mcmtools/ $HOME/chroot/home/$USER/

Re-enter the chroot, then set $PATH to include the musl tools:

export PATH=$PATH:$HOME/mcmtools/musl/bin

At this point, you should have all the dependencies needed except for ncurses, which you now proceed to build in the next step.

Build ncurses so that GHC can be built with terminfo support included

(The configure arguments here are stolen modified from the ncurses APKBUILD)

Install curl, download and extract the ncurses tarball:

sudo apt install curl
curl -LO "https://invisible-mirror.net/archives/ncurses/current/ncurses-6.3-20211115.tgz"
tar xf ncurses-6.3-20211115.tgz
rm ncurses-6.3-20211115.tgz
cd ncurses-6.3-20211115

configure and make the ncurses library:

./configure --build=aarch64-unknown-linux-gnu --host=aarch64-linux-musl --with-terminfo-dirs="/etc/terminfo:/usr/share/terminfo:/lib/terminfo:/usr/lib/terminfo" --with-default-terminfo-dir="/etc/terminfo"
make -j$(nproc)
mkdir $HOME/ncurses
make DESTDIR=$HOME/ncurses install

Add a wrapper gcc that adds --sysroot $HOME/ncurses for use when compiling GHC:

cat >$HOME/mcmtools/musl/bin/aarch64-linux-musl-gcc-ncurses <<'EOF'
#!/bin/bash
aarch64-linux-musl-gcc --sysroot $HOME/ncurses "$@"
EOF

chmod +x $HOME/mcmtools/musl/bin/aarch64-linux-musl-gcc-ncurses

Optional: Compile a ncurses test program

aarch64-linux-musl-gcc-ncurses -I $HOME/ncurses-6.3-20211115/test/ -I $HOME/ncurses-6.3-20211115/include/ $HOME/ncurses-6.3-20211115/test/hanoi.c -lncurses -o $HOME/hanoi

Outside the chroot, run the test program:

$HOME/chroot/home/$USER/hanoi

Compile GHC

Download and extract the GHC 8.10.7 source tarball:

curl -LO "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz"
tar xf ghc-8.10.7-src.tar.xz
rm ghc-8.10.7-src.tar.xz
cd ghc-8.10.7

Apply this hacky patch that forces CrossCompiling = YES:

patch -p0 <<'EOF'
--- mk/config.mk.in
+++ mk/config.mk.in
@@ -571,7 +571,7 @@

 # Cross-compiling options
 # See Note [CrossCompiling vs Stage1Only]
-CrossCompiling        = @CrossCompiling@
+CrossCompiling        = YES

 # Change this to YES if you're building a cross-compiler and don't
 # want to build stage 2.
@@ -885,7 +885,7 @@
 compiler_ALEX_OPTS = --latin1

 # Should we build haddock docs?
-HADDOCK_DOCS = YES
+HADDOCK_DOCS = NO
 # And HsColour the sources?
 ifeq "$(HSCOLOUR_CMD)" ""
 HSCOLOUR_SRCS = NO
EOF

And this patch to force cross_compiling=yes to gmp's configure:

patch -p0 <<'EOF'
--- libraries/integer-gmp/gmp/ghc.mk
+++ libraries/integer-gmp/gmp/ghc.mk
@@ -134,6 +134,7 @@
 	#        for gmp test programs. (See gmp's configure)
 	cd libraries/integer-gmp/gmp/gmpbuild; \
 	    CC=$(CCX) CXX=$(CCX) NM=$(NM) AR=$(AR_STAGE1) ./configure \
+	          cross_compiling=yes \
 	          --enable-shared=no --with-pic=yes --with-readline=no \
 	          --host=$(TARGETPLATFORM) --build=$(BUILDPLATFORM)
 	$(MAKE) -C libraries/integer-gmp/gmp/gmpbuild MAKEFLAGS=
EOF

Pay attention with the ./configure here. The prefix you specify will need to be the same as on your Alpine system, so if $HOME is different, then it'll end up in the wrong place and probably not work. You can either specify an absolute directory (such as /opt/ghc/), or after the build is done, run make binary-dist and follow the GHC wiki's instructions on installing from a binary distribution. Next, a pretty standard ./configure and make (but be prepared to wait a long time):

./configure --prefix=$HOME/ghc/ --host=aarch64-unknown-linux-gnu --target=aarch64-linux-musl CC=aarch64-linux-musl-gcc-ncurses LD=aarch64-linux-musl-ld.bfd 2>&1 | tee build.log
make -j$(nproc) 2>&1 | tee -a build.log

I found that with 8GB of RAM and 8 cores, once in a while make would get OOM killed, but just continuing it with the same command works.

NOTE: It would probably be better to install GHC by making a bindist so that compiler settings are correct to the host system (such as not having the aarch64-linux-musl- prefix for the compilers). You can follow the instructions further down in this page (stop before the Compiling XMonad section)

And now to install GHC:

mkdir $HOME/ghc
make install 2>&1 | tee -a build.log

Exit the chroot. Then, symlink the built and installed GHC from the chroot directory onto the main Alpine system (if you used a different --prefix in ./configure, change it here):

ln -s $HOME/chroot/home/$USER/ghc/ $HOME

Install the runtime dependencies on the host system

musl-dev is needed for inttypes.h:

doas apk add gmp-dev musl-dev gcc llvm12

A few changes need to be made to GHC's settings:

sed -i "s|aarch64-linux-musl-gcc-ncurses|cc|" $HOME/ghc/lib/ghc-8.10.7/settings
sed -i "s|aarch64-linux-musl-||" $HOME/ghc/lib/ghc-8.10.7/settings
sed -i "s|llc-12|llc|" $HOME/ghc/lib/ghc-8.10.7/settings
sed -i "s|opt-12|opt|" $HOME/ghc/lib/ghc-8.10.7/settings

The exciting part: Add the newly built GHC binaries to $PATH and compile Hello World

export PATH=$PATH:$HOME/ghc/bin

You can also run ghci.

cat >Hello.hs <<'EOF'
main = putStrLn "Hello, world!"
EOF

ghc Hello.hs
./Hello

Bootstrap & Install cabal-install

First, install dependencies and clone the cabal repository:

doas apk add python3 zlib-dev wget
git clone --depth=1 https://github.com/haskell/cabal.git
cd cabal/

Now, run the bootstrap.py file to compile cabal-install to _build/bin/cabal:

./bootstrap/bootstrap.py -d ./bootstrap/linux-8.10.7.json
./_build/bin/cabal update
./_build/bin/cabal install cabal-install
export PATH=$PATH:$HOME/.cabal/bin

Clean up the cabal folder:

cd ..
rm -rf cabal/

Use the installed cabal-install to install shellcheck

First, clone the shellcheck repository:

git clone --depth=1 https://github.com/koalaman/shellcheck.git
cd shellcheck

Then just run cabal install and you'll have a working shellcheck binary in $HOME/.cabal/bin/shellcheck:

cabal install

cat >badscript <<'EOF'
ls $HOME/ghc-cross
echo $@
EOF

shellcheck badscript

BONUS: Make a bindist, move it to a clean Alpine system and compile XMonad

Make the bindist (run this inside the chroot in the ghc-8.10.7 directory):

make bindist 2>&1 | tee -a build.log

Install all the dependencies for GHC and cabal-install:

doas apk add make gmp-dev musl-dev gcc llvm12 git python3 zlib-dev wget

Copy the bindist tarball to $HOME/ghc-8.10.7-aarch64-unknown-linux.tar.xz, then run:

tar xf ghc-8.10.7-aarch64-unknown-linux.tar.xz
rm ghc-8.10.7-aarch64-unknown-linux.tar.xz
cd ghc-8.10.7
mkdir $HOME/ghc
./configure --prefix=$HOME/ghc
make install
cd ..
rm -rf ghc-8.10.7/
export PATH=$PATH:$HOME/ghc/bin

Then follow the steps to Bootstrap & Install cabal-install

Compiling XMonad

(These steps are taken more or less verbatim from https://xmonad.org/INSTALL.html)

Installing XMonad dependencies

doas apk add libx11-dev libxft-dev libxinerama-dev libxrandr-dev libxscrnsaver-dev ncurses-dev

Preparation

Make a xmonad directory in $XDG_CONFIG_HOME (which is $HOME/.config/ normally):

mkdir -p $HOME/.config/xmonad
cd $HOME/.config/xmonad/

Create a bare minimum xmonad.hs configuration file:

cat >xmonad.hs <<'EOF'
import XMonad

main :: IO ()
main = xmonad def
EOF

Download XMonad sources

Still in $HOME/.config/xmonad, clone the xmonad and xmonad-contrib repositories:

git clone --depth=1 https://github.com/xmonad/xmonad
git clone --depth=1 https://github.com/xmonad/xmonad-contrib

Build XMonad using cabal-install

Create a new cabal.project file in the $HOME/.config/xmonad directory (since there's a xmonad and a xmonad-contrib folder here, cabal will use them):

cat >cabal.project <<'EOF'
packages: */*.cabal
EOF

Install xmonad and xmonad-contrib:

cabal update
cabal install --package-env=$HOME/.config/xmonad --lib xmonad xmonad-contrib
cabal install --package-env=$HOME/.config/xmonad xmonad

Run XMonad !!!

Install xorg and related packages:

doas apk add xorg-server xf86-input-libinput xf86-video-fbdev eudev mesa ttf-dejavu xterm dmenu
doas setup-udev

Make it such that xmonad runs when $HOME/.xinitrc is called:

cat >$HOME/.xinitrc <<'EOF'
#!/bin/sh
exec xmonad
EOF

chmod +x $HOME/.xinitrc

Currently (as of Nov 19 2021), the startx binary doesn't have the suid bit set, so do this:

doas chmod +s /usr/bin/startx

Also Xorg errors out with open /dev/fb0: Permission denied. This should fix it:

doas tee -a /etc/X11/Xwrapper.config <<'EOF'
allowed_users=anybody
needs_root_rights=yes
EOF

Note: While everything else up to here can be done in an ssh session, startx must be done in the actual framebuffer text console

Now, just run startx and you'll be in!

Alpine Arm64 XMonad

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment