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
- Compile a cross-compiling toolchain targeting Alpine
- Create a Debian chroot and install dependencies (including the cross-compiling toolchain)
- Build
ncurses
so that GHC can be built with terminfo support included - Compile GHC
- Bootstrap & Install
cabal-install
- Use the installed
cabal-install
to installshellcheck
- When linking
-no-pie
executables withld.gold
against musl, the output executables always segfault. That's why in the configure for GHC it usesld.bfd
- No matter the combination of
host
/target
or force settingCrossCompiling = YES
inmk/build.mk
, after running./configure
the output filemk/config.mk
still containsCrossCompiling = NO
. The problem with that is partway through the compilemake
tries to run binaries compiled for the target. That's why I just replacedCrossCompiling = @CrossCompiling@
withCrossCompiling = YES
inmk/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)
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.
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
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
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.
(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
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
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
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
export PATH=$PATH:$HOME/ghc/bin
You can also run ghci
.
cat >Hello.hs <<'EOF'
main = putStrLn "Hello, world!"
EOF
ghc Hello.hs
./Hello
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/
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
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
(These steps are taken more or less verbatim from https://xmonad.org/INSTALL.html)
doas apk add libx11-dev libxft-dev libxinerama-dev libxrandr-dev libxscrnsaver-dev ncurses-dev
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
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
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
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!