ghc-musl Part 4: lsupg With Cabal
Now that I am able to use ghc-musl to build fully
static executables of lsupg using
Stack (see ghc-musl Part
3: lsupg With Stack), it is time to get it working using Cabal. It is easy to use Cabal
to build within a ghc-musl container, but
some orchestration is needed so that one can just run a
make
command that runs a container, builds the executable,
and puts the executable in an appropriate location (with the correct
permissions). Stack handles
all of this, but custom orchestration is required for Cabal.
Image Selection
One decision to make is how to select the Docker image to use for the
static build. A simple solution would be to pass the image as an
argument to the make
command, but I do not like this
option. When using Cabal, my Makefile
s use a
GHC_VERSION
variable. If specifying both IMAGE
and GHC_VERSION
(which defaults to the version of
ghc
in your PATH
), it is possible that they
will not match. This is not a problem if the GHC_VERSION
is
recalculated within the container, so that the correct
cabal.project
file is selected, but this complicates
things. More importantly, from a user perspective, I do not want to have
to lookup the correct image name when I run the command. I would rather
configure the images and select one by GHC version.
See cabal.project Per
GHC Version (and Part 2)
for discussion about the necessity of using separate
cabal.project
files for different GHC versions in some
cases. The Stack design where the GHC version is determined by the build
configuration (stack.yaml
file) is really
convenient. With Cabal, care must be taken to use the appropriate
cabal.project
file for the version of GHC being used.
So, I will use GHC_VERSION
to determine which Docker
image to use. Where should these images be configured? Since the images
are already configured in the stack.yaml
files, I
considered parsing the image name from the file corresponding to
GHC_VERSION
when in Cabal mode. This was easy to implement,
but there is an issue with this method. Cabal supports new versions of
GHC as soon as they are released, but it takes some time before support
for a new version is added to Stack. Is it acceptable to limit Cabal
versions to those supported by Stack? I would rather not.
An alternative is to just configure the images in the
Makefile
. If they are configured by
GHC_VERSION
, then it is trivial to select an image when
using Cabal mode. Unfortunately it is not straightforward when using
Stack mode because the GHC version is determined by the
stack.yaml
file being used. I could parse the filename (and
symbolic link) to determine the version, but that does not work for all
Stack configuration. (I sometimes have configuration for special
builds/tests that do not include the GHC version in the filename.)
Another alternative is to configure the images in the
Makefile
as well as in the stack.yaml
files.
Configuring this in multiple locations is not optimal, but an advantage
is that it would then be possible to use different images for the same
version of GHC depending on which tool is being used to build. (I hope
that is never necessary, but it is possible.) Since this is the only
method that supports everything that I (might) want to do, I will go
with it.
Building
The project directory can be mounted when the ghc-musl container is
run, so that the build uses the current state of all the project files.
(Using Git or a source tarball would not work well.) Unfortunately, this
raises the common Docker issue of file ownership. The ghc-musl containers use
the root
user, so any files written to the mounted
directory will be owned by root
. These files should be
owned by the host user.
There are various solutions to the ownership problem.
- The ownership can be fixed after the container stops. Unfortunately,
this requires
sudo
. - The ownership can be fixed within the container. The user
UID
andGID
can be passed as arguments, or a common shortcut is to use theUID
andGID
of the mounted directory. - The container can use a non-root user, and the
UID
andGID
of that user can be changed appropriately when the container starts. This is the approach taken by the fixuid utility. Note that this would require significant changes to the ghc-musl images. - The image can be built using a non-root user with the appropriate
UID
andGID
set from the beginning. This is what I do in the docker-ghc and docker-pkg projects. Note that this only works for people who build their own images, so it is not appropriate for the ghc-musl images, which are hosted on Docker Hub.
I think it would be a huge improvement to not build as
root
, but I am currently not considering the option of
significantly changing ghc-musl. I prefer to
avoid the use of sudo
(on the host), so I will go with the
second option.
I tried it out manually before writing a script, and everything is as
expected. Note that I call cabal v2-build
with the
--enable-executable-static
option as well as set the
static
flag that is configured in lsupg.cabal
.
This project currently uses a common cabal.project
file
that works with all supported GHC versions, so a version-specific
cabal.project
file does not need to be specified. The built
executable is not stripped, so this needs to be done as well. Setting my
(1331) UID
and
GID
works without issue.
[lsupg] $ docker run --rm -it -v $(pwd):/host \
extremais/ghc-musl:v23-ghc922 /bin/bash
bash-5.1# ls -ld /host
drwxr-xr-x 9 1331 1331 4096 Mar 25 02:25 /host
bash-5.1# cd /host
bash-5.1# cabal v2-build --enable-executable-static --flags=static
...
Linking /host/dist-newstyle/.../lsupg ...
bash-5.1# ls -ld dist-newstyle/
drwxr-xr-x 6 root root 4096 Mar 25 02:31 dist-newstyle/
bash-5.1# chown -R 1331:1331 dist-newstyle/
bash-5.1# exit
[lsupg] $ find dist-newstyle -type f -name lsupg | xargs ls -l
-rwxr-xr-x 1 tcard tcard 47572512 3月 25 11:34 dist-newstyle/.../lsupg
[lsupg] $ find dist-newstyle -type f -name lsupg | xargs file
dist-newstyle/.../lsupg: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
statically linked, with debug_info, not stripped
[lsupg] $ find dist-newstyle -type f -name lsupg | xargs strip
[lsupg] $ find dist-newstyle -type f -name lsupg | xargs ls -l
-rwxr-xr-x 1 tcard tcard 29494152 3月 25 11:39 dist-newstyle/.../lsupg
[lsupg] $ $(find dist-newstyle -type f -name lsupg) --version
lsupg-haskell 0.3.0.1
I wrote a script to run inside a ghc-musl container, trying to keep things simple. The script just builds the executable and corrects the ownership. Stripping the executable can be handled on the host.
#!/usr/bin/env bash
set -o errexit
set -o nounset
set -o pipefail
#set -o xtrace
die () {
echo "error: $*" >&2
exit 2
}
cd "/host" || die "/host not found"
cabal v2-build --enable-executable-static "$@" || die "cabal: exit code $?"
PUID="$(stat -c '%u' /host)"
PGID="$(stat -c '%g' /host)"
chown -R "${PUID}:${PGID}" "dist-newstyle" || die "chown: exit code $?"
I am not going to put the whole Makefile
command here,
because it depends on other parts of the Makefile
. (I hope
to publish the Makefile
article series soon!) The relevant
part of the command is how the container is run.
docker run --rm -it -v "$(CURDIR):/host" "$(CABAL_STATIC_REPO)" \
"/host/script/build-cabal-static.sh" --flags=static $(CABAL_ARGS)
This command runs a container with an interactive terminal so that it
is automatically removed when it stops. The current directory is mounted
at /host
within the container. The
CABAL_STATIC_REPO
variable specifies the selected image.
The above script is executed using the absolute path within the
container. The --flags=static
option is passed to the
script, as well as any options configured by the Makefile
in the CABAL_ARGS
variable. This includes version-specific
cabal.project
configuration when necessary.
I tested with all four of the supported GHC versions, and everything builds without issue!