Updated 12-7-2019 with instructions on how to create an alias script for programs like Frescobaldi.

Like with all great things, updating operating systems breaks them. 🤷‍ This time, there is an issue with Apple’s decision to discontinue the support of 32bit applications running on their x86_64 hardware. Generally, this is “easily” fixed by either compiling the program to support 64bit or running the app in a container that simulates the old 32bit processor/operating system. If you have worked with Windows in the last couple of years they have the great name of “compatibility mode.”

I want to preface my dismay by saying that Lilypond is great and I use it all of the time. I think that the typesetting you get out of it it second to none. However, the code base is old. There are a lot of highly outdated dependencies being used (e.g., guile 1.8 where the current is 2.2. 1.8 was released in 2006). There is also complications with Apple’s new XCode license and the GUB system that builds the Lilypond binaries. There is an article written by Daniel Johnson that tries to include the older versions of dependencies using Homebrew, but now there seems to be problems with the XCode version of gcc not correctly handling headers. So I was poking around and found that my favorite Lilypond “bin” Hacklily uses Lilypond in a Docker container. Well, then why don’t I just do that?

I was a bit apprehensive as Docker is a new technology for me, but in the end, it was simpler than I thought that will be a more robust solution regardless if Lilypond every gets around to modernizing their codebase to work with the 64bit MacOS.

What is Docker image? Container?

Just briefly, because I know that this is discussed at length in other parts of the internet, the biggest confusion I had was what is a container and what is an image. An image is the base system that you are running and has no writable layer. So all of the setup happens in this image – the building of dependencies, setting up the needed files for the base operating system, etc. The container is what runs these images and provides a writable layer to the images.

Installing Docker

On MacOS this couldn’t be simpler. Install instructions from Docker Just download, drag-and-drop, and login.

Creating the image

While not essential to running the Docker image, here’s a little behind the curtain. There is currently a couple Lilypond images but they are old and the API has changed slightly. So I decided to just create my own. Below is the first iteration which worked almost flawlessly.

# Dockerfile
# Select the base system 
FROM ubuntu:bionic

# Setup the locales for the Ubuntu system.  Because the base image is a bare bones 
# setup, this is needed to get things in the correct language. 
# https://hub.docker.com/_/ubuntu/ 
# Always use update with the install subcommand
# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
RUN apt-get update && apt-get install -y locales && rm -rf /var/lib/apt/lists/* \
    && localedef -i en_US -c -f UTF-8 -A /usr/share/locale/locale.alias en_US.UTF-8

# Set LANG to us.utf8
ENV LANG en_US.utf8

# Set tell the installer that we are working in a noninteractive ENV 
ENV DEBIAN_FRONTEND noninteractive

# Install Lilypond. 
RUN apt-get update && apt-get -y install lilypond

No – seriously – that’s it.

Creating the container?…

The next step I was ready to do was to get a container spun up and running and then start throwing it files. The exciting part is that since the image itself is “operating systemless,” there is no boot up and we can just throw it commands. After installing Docker, all you need to do is run the following command where FILE is the file you’re processing.

docker run --rm -v $(pwd):/app -w /app gpit2286/lilypond lilypond FILE

If it is the first time running the image in a container, it will download the image and install it. After the first run, the above command should run almost instantaneously. More exciting, it should also allow you to use any of the Lilypond executables by just changing the last lilypond to whichever binary you’re looking for. (lilypond-book, musicxml2ly, etc.)

The last thing that is interesting to me though is the container we create is there and then it’s not. There is no persistent container in this case and is actually only present while Lilypond is creating the output. The options for docker run are:

  • --rm: Remove the container as soon as the process is done executing
  • -v $(pwd):/app: This connects the working directory to the /app directory on the container
  • -w /app: This tells the container that all of the work we want to do should take place in /app in the container
  • gpit2286/lilypond: The image to use int he container
  • lilypond FILE: The command sent to the container

Using the container in other ways

One of the problems some people have been running into is using the container in places where a single executable is expected. Below is a shell script that can be added to your PATH. This one was made specifically for use with Frescobaldi, although it should work with any program that calls Lilypond after the above Docker config is setup.

Steps to create script

  1. Enter echo $PATH and make sure the /usr/local/bin is there. If not, add it to your path.

  2. Enter nano /usr/local/bin/lilypond-link and copy-paste the below code. Use CTRL+X to exit, y to confirm changes, and hit enter.

# Set variable for edited args
NEWARGS=()

# This removes /private from the temp folder directory
# This will make sure that it will match the tmp file
# if it starts with /var or /private
NEWDIR=${PWD/\/private/}

for ARG in $@
do
        # If the temp folder location is in the arg, remove it.
        # We need to do this as docker expects a relative path inside the
        # container and not the absolute outside the container
        if [[ $ARG =~ $NEWDIR ]]
        then
                NEWARGS+=("${ARG/$NEWDIR/.}")
        else
                NEWARGS+=("$ARG")
        fi
done

# Run Lilypond in the container with the new options.
/usr/local/bin/docker run --rm -v $PWD:/app -w /app gpit2286/lilypond lilypond "${NEWARGS[@]}"
  1. Make the new script executable. chmod +x /usr/local/bin/lilypond-link

  2. In Frescobaldi, under the main Frescobaldi menu, select Preferences. Under Lilypond Preferences, remove all of the entries under Lilypond version to use.

  3. Click Add+ and for the “lilypond command” enter /usr/local/bin/lilypond-link