Epoxy is a small library that GTK+,
and other projects, use in order to access the OpenGL API in somewhat sane
fashion, hiding all the awful bits of craziness that actually need to happen
because apparently somebody dosed the water supply at SGI with large
quantities of LSD in the mid-‘90s, or something.
As an added advantage, Epoxy is also portable on different platforms, which
is a plus for GTK+.
Since I’ve started using Meson for my personal (and
some work-related) projects as well, I’ve
been on the lookout for adding Meson build rules to other free and open
source software projects, in order to improve both their build time and
portability, and to improve Meson itself.
As a small, portable project, Epoxy sounded like a good candidate for the
port of its build system from autotools to Meson.
To the Bat Build Machine!
Since you may be interested just in the numbers,
building Epoxy with Meson on my Kaby Lake four Core i7 and NMVe SSD takes about
45% less time than building it with autotools.
A fairly good fraction of the autotools time is spent going through the
autogen and configure phases, because they both aren’t parallelised, and
create a ton of shell invocations.
Conversely, Meson’s configuration phase is incredibly fast; the whole
Meson build of Epoxy fits in the same time the autogen.sh and configure
scripts complete their run.
Epoxy is a simple library, which means it does not need a hugely complicated
build system set up; it does have some interesting deviations, though, which
made the porting an interesting challenge.
For instance, on Linux and similar operating systems Epoxy uses
to find things like the EGL availability and the X11 headers and libraries;
on Windows, though, it relies on finding the
opengl32 shared or static
library object itself. This means that we get something straightforward in
the former case, like:
# Optional dependencies
gl_dep = dependency('gl', required: false)
egl_dep = dependency('egl', required: false)
and something slightly less straightforward in the latter case:
if host_system == 'windows'
# Required dependencies on Windows
opengl32_dep = cc.find_library('opengl32', required: true)
gdi32_dep = cc.find_library('gdi32', required: true)
And, still, this is miles better than what you have to deal with when using autotools.
Let’s take a messy thing in autotools, like checking whether or not the
compiler supports a set of arguments; usually, this involves some m4 macro
that’s either part of autoconf-archive
or some additional repository, like the xorg macros.
Meson handles this in a much better way, out of the box:
# Use different flags depending on the compiler
if cc.get_id() == 'msvc'
test_cflags = [
elif cc.get_id() == 'gcc'
test_cflags = [
test_cflags = [ ]
common_cflags = 
foreach cflag: test_cflags
common_cflags += [ cflag ]
In terms of speed, the configuration step could be made even faster by
parallelising the compiler argument checks; right now, Meson has to do them
all in a series, but nothing except some additional parsing effort would
prevent Meson from running the whole set of checks in parallel, and gather
the results at the end.
In order to use the GL entry points without linking against
libGLES* Epoxy takes the XML description of the API from the Khronos
repository and generates the code that ends up being compiled by using a
Python script to parse the XML and generating header and source files.
Additionally, and unlike most libraries in the G* stack, Epoxy stores its
public headers inside a separate directory from its sources:
│ └── epoxy
The autotools build has the
src/gen_dispatch.py script create both the
source and the header file for each XML at the same time using a rule
processed when recursing inside the
src directory, and proceeds to put the
generated header under
$(top_builddir)/include/epoxy, and the generated
$(top_builddir)/src. Each code generation rule in the
Makefile manually creates the
include/epoxy directory under the build
root to make up for parallel dispatch of each rule.
Meson makes is harder to do this kind of spooky-action-at-a-distance build,
so we need to generate the headers in one pass, and the source in another.
This is a bit of a let down, to be honest, and yet a build that invokes the
generator script twice for each API description file is still faster under
Ninja than a build with the single invocation under Make.
There are sill issues in this step that are being addressed by the Meson
developers; for instance, right now we have to use a custom target for each
generated header and source separately instead of declaring a generator and
calling it multiple times. Hopefully, this will be fixed fairly soon.
Epoxy has a very small footprint, in terms of API, but it still benefits
from having some documentation on its use. I decided to generate the API
reference using Doxygen, as it’s
not a G* library and does not need the additional features of gtk-doc.
Sadly, Doxygen’s default style is absolutely terrible; it would be great if
somebody could fix it to make it look half as good as the look gtk-doc gets
out of the box.
Cross-compilation and native builds
Now we get into “interesting” territory.
Epoxy is portable; it works on Linux and *BSD systems; on macOS; and on
Windows. Epoxy also works on both Intel Architecture and on ARM.
Making it run on Unix-like systems is not at all complicated. When it comes
to Windows, though, things get weird fast.
Meson uses cross files
to determine the environment and toolchain of the host machine, i.e. the
machine where the result of the build will eventually run. These are simple
text files with key/value pairs that you can either keep in a separate
repository, in case you want to share among projects; or you can keep them
in your own project’s repository, especially if you want to easily set up
continuous integration of cross-compilation builds.
Each toolchain has its own; for instance, this is the description of a cross
compilation done on Fedora with MingW:
c = '/usr/bin/x86_64-w64-mingw32-gcc'
cpp = '/usr/bin/x86_64-w64-mingw32-cpp'
ar = '/usr/bin/x86_64-w64-mingw32-ar'
strip = '/usr/bin/x86_64-w64-mingw32-strip'
pkgconfig = '/usr/bin/x86_64-w64-mingw32-pkg-config'
exe_wrapper = 'wine'
This section tells Meson where the binaries of the MingW toolchain are; the
exe_wrapper key is useful to run the tests under Wine, in this case.
The cross file also has an additional section for things like special
compiler and linker flags:
root = '/usr/x86_64-w64-mingw32/sys-root/mingw'
c_args = [ '-pipe', '-Wp,-D_FORTIFY_SOURCE=2', '-fexceptions', '--param=ssp-buffer-size=4', '-I/usr/x86_64-w64-mingw32/sys-root/mingw/include' ]
c_link_args = [ '-L/usr/x86_64-w64-mingw32/sys-root/mingw/lib' ]
These values are taken from the equivalent bits that Fedora provides in
their MingW RPMs.
Luckily, the tool that generates the headers and source files is written in
Python, so we don’t need an additional layer of complexity, with a tool
built and run on a different platform and architecture in order to generate
files to be built and run on a different platform.
Of course, any decent process of porting, these days, should deal with
continuous integration. CI gives us confidence as to whether or not any
change whatsoever we make actually works — and not just on our own computer,
and our own environment.
Since Epoxy is hosted on GitHub, the quickest way to deal with continuous
integration is to use TravisCI, for Linux and
macOS; and Appveyor for Windows.
The requirements for Meson are just Python3 and Ninja; Epoxy also requires
Python 2.7, for the dispatch generation script, and the shared libraries for
GL and the native API needed to create a GL context (GLX, EGL, or WGL); it
also optionally needs the X11 libraries and headers and Xvfb for running the
Since Travis offers an older version of Ubuntu LTS as its base system, we
cannot build Epoxy with Meson; additionally, running the test suite is a
crapshoot because the Mesa version if hopelessly out of date and will either
cause most of the tests to be skipped or, worse, make them segfault. To
sidestep this particular issue, I’ve prepared a Docker image
with its own harness, and I use it as the containerised environment for Travis.
On Appveyor, thanks to the contribution of Thomas Marrinan
we just need to download Python3, Python2, and Ninja, and build everything
inside its own root; as an added bonus, Appveyor allows us to take the build
artefacts when building from a tag, and shoving them into a zip file that
gets deployed to the release page on GitHub.
Most of this work has been done off and on over a couple of months; the
rough Meson build conversion was done last December, with the
cross-compilation and native builds taking up the last bit of work.
Since Eric does not have any more spare
time to devote to Epoxy, he was kind enough to give me access to the
original repository, and I’ve tried to reduce the amount of open pull
requests and issues there.
I’ve also released version 1.4.0
and I plan to do a 1.4.1 release soon-ish, now that I’m positive Epoxy works
I’d like to thank:
- Eric Anholt, for writing Epoxy and helping out when I needed a hand
- Jussi Pakkanen and
Nirbheek Chauhan, for writing
Meson and for helping me out with my dumb questions on
- Thomas Marrinan, for working on the Appveyor integration and testing
Epoxy builds on Windows
- Yaron Cohen-Tal, for maintaining Epoxy in the interim