Intro to building GCC and some other GNU projects (focus on macOS & Apple Silicon)

January 9, 2023


Once a year or so I need to build GCC. And every year I wish I had notes from the year before. This is my cheatsheet on the GNU Build System, build/host/target platforms, pkg-config, and gcc building.


The goal is to have a gcc executable built from source, and we’ll take a few detours to better understand some pervasive concepts in the “build” world. And just to be clear, I’m no GCC or build expert! If you see anything suspicious please do reach out.

Note

I love Linux but I also love a display that works when I connect it to my laptop. My development machine is an Apple Silicon (M1) MacBook and hence the focus of this article is on macOS, but most of it will apply to Linux and potentially Windows (with WSL) too.


GCC is not just a single compiler, but (similar to clang) it’s a compiler collection — the GNU Compiler Collection. It consists of e.g. gcc the C compiler, but also g++ for C++ and many more.

image

GCC dependencies

Do note that you should only rarely need to build GCC; in most cases you can get a package for your “distribution” (with apt install or brew install) or download prebuilt executables (for instance you can find some prebuilt GCC bundles for arm on the arm website).

Note as well that we’ll be building GCC’s dependencies (see below) separately, although they can also be built directly from GCC’s source (which vendors them). Building them separately allows us to reuse the build artifacts for other projects, and it’s a really good exercise.

We’ll be building GCC version 12 and I’ll share links to the source used for each build. We’ll need to build a few projects, because gcc itself has a few dependencies:

In addition to that we’ll also build pkg-config, which is a nice tool for including dependencies.

We’ll start with gmp, not only because we need it to build gcc and its other dependencies but also because it’s very simple and will allow us to warm up. Then we’ll work up the dependency chain and build pkg-config along the way.

The best doc I’ve found on building GCC is actually in the avr-libc manual, if you ever want to go into details. They also have a great introduction to what the linker does, and more. Another great source of inspiration is the gcc formula for Homebrew.

With that out of the way, let’s make sure we have all the tools required to start building!

Preparation: cc, make

Most builds will look like this (we’ll get to a concrete example in the gmp section):

$src/configure \ --prefix=$out \ --build=$platform make make install

The first line (calling some configure script) will … configure the build, and make will use whatever tools were configured to actually build the code. This is the bedrock of the GNU build system, and if you learn how to use configure scripts you’ll feel at home in a lot of C and C++ codebases.

(For the curious reader: the configure script and the Makefiles used by make are rarely written by hand, but you won’t need the details unless you’re packaging your own projects using autoconf and automake.)

In order to run those two commands successfully (configure and make), you’ll need:

If you’re building on macOS, then run xcode-select --install, which will install the “Command-Line Tools” (CLT) from Xcode (but more lightweight). Then you’ll have /usr/bin/cc and /usr/bin/make.

image

Install some build tools provided by Apple.

And while you might think you’ve installed GCC, it’s a lie:

$ /usr/bin/gcc --version Apple clang version 14.0.0 (clang-1400.0.29.202) Target: arm64-apple-darwin22.1.0 Thread model: posix InstalledDir: /Library/Developer/CommandLineTools/usr/bin

However the make version shipped with CLT is GNU:

$ /usr/bin/make --version GNU Make 3.81 Copyright (C) 2006 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ...

And that’s “all” you need to build GCC!

The platforms: build, host & target

image

The GNU platform

If you have another look at the generic build command shown earlier, you can see the following argument to the configure script: --build=$platform:

$src/configure \ --prefix=$out \ --build=$platform make make install

Here, you can set $platform to the platform you’re building GCC on. This in an important concept, because the platform you’re building the compiler on (build) may not be the same as the platform where it will be used (host) and the platform where the executables produced by said compiler will run (target). Each platform (build, host, target) can be configured using the corresponding flag (--build,--host,--target) but most often can be left out.

Note

Apple Silicon (M1, M2, …) Users: you may want to specify the --build explicitly since the “Apple Silicon” platform (e.g. aarch64-apple-darwin13 for Ventura) is fairly recent and some older configure scripts will struggle to figure it out). I’ll specify --build in all snippets since that’s the build platform I care about.

To clarify the idea of “platforms” further:

Very often the build, host and target platforms will be the same:

my-machine$ ./compile-gcc --output=./dist my-machine$ ./dist/gcc ~/my-project/main.c -o my-program my-machine$ ./my-program hello

However you may want to specify a target different from the host (and build), meaning you want your new gcc to be a cross-compiler that you’ll build on your machine, that you’ll be running on your machine, but which will produce executables for another platform, like an arduino:

my-machine$ ./compile-gcc --output=./dist --target=avr # avr ~= Arduino my-machine$ ./dist/avr-gcc ~/sample-arduino/main.c -o arduino.hex # arduino.hex will run on the Arduino

Finally you can imagine a situation where the build, host and target platforms are all different. You could for instance have a web platform that allows users to compile firmware for keyboards with avr chips. In this case you’d first build GCC on e.g. your Linux laptop (so the build platform would be Linux), with the intent of running GCC in the browser (the host platform would be WebAssembly) and the programs produced by that GCC would be running on your keyboard (the target would be the AVR platform).

image

A compiler built by a computer wizard, used by an app developper, producing an iOS app used by a mere mortal.

Now that you understand the concept of “platform” and before we start building gmp, the first dependency, it’s important to stress this: the libraries we’ll be building here, i.e. GCC dependencies, will be built for the “host” platform because GCC will use them for internal stuff as it runs. In some cases however (which we won’t get into here) you may also need to compile some libraries for the target platform (like a libc used at runtime by GCC-compiled executables).

Building gmp

Okay let’s start building! Here’s how to build gmp:

$gmp_src/configure \ --prefix=$gmp \ --build=$platform \ --with-pic make make install

Pretty easy, heh? For the the source I unpacked the tarball for gmp-6.2.1 in $gmp_src (can be any directory). GNU recommends not building in the same directory where the sources are, so you could for instance set $gmp_src to ~/my-srcs/gmp_src, and then run the commands above in a temporary directory. The build artifacts will be placed in $gmp, which again can be anything — you could set $gmp to ~/my-libs/gmp for instance.

If 6.2.1 is not to your liking, the easiest way I found to browse versions (applies to gmp and others) is to go to the GNU FTP and click around. It’s not very modern but works like a charm.

Finally, you might notice one last argument, --with-pic. This configures the build to produce position independent code. I don’t understand this enough to attempt to explain it, but you’ll definitely want this when building libraries (.dylib,.so, .a, etc).

Relatively painless, right? Let’s move on!

Preparation: pkg-config

We’ll soon need to configure where to find dependencies (e.g. in the next section we’ll be building mpfr which depends on gmp that we built in the last section). For that we’ll use a handy tool: pkg-config.

When we built gmp, a “package config” was generated:

$ cat $gmp/lib/pkgconfig/gmp.pc prefix=/path/to/built/gmp exec_prefix=${prefix} includedir=${prefix}/include libdir=${exec_prefix}/lib Name: GNU MP Description: GNU Multiple Precision Arithmetic Library URL: https://gmplib.org Version: 6.2.1 Cflags: -I${includedir} Libs: -L${libdir} -lgmp

And pkg-config is a tool that interprets those configs and generates C-compiler-friendly flags:

$ PKG_CONFIG_PATH=$gmp/lib/pkgconfig/ pkg-config --cflags gmp -I/path/to/built/gmp/include

it also has more flags that can be fed to the linker. All it needs is a colon-separated list of package configs in PKG_CONFIG_PATH.

Let’s build pkg-config:

$pkg_config_src/configure \ --prefix=$pkg_config \ --with-internal-glib \ --build=$platform make make install

For the source I downloaded the tarball for pkg-config-0.29.2 and unpacked it in $pkg_config_src. We configure the build with --with-internal-glib, meaning that pkg-config will be built against a vendored version of glib so that we don’t have to build glib ourselves (otherwise this article will get too long).

Ok, let’s now use pkg-config to build mpfr and libmpc, the last dependencies needed for the actual GCC build!

Building mpfr and libmpc

From here on there’s pretty much nothing new. In order to build mpfr (which depends on gmp, and which libmpc depends on) we use pkg-config to specify where the C compiler should look for C headers (CFLAGS=-I...) and where the linker should look for built libs (LDFLAGS=-L...):

PATH=$pkg_config/bin:$PATH export PKG_CONFIG_PATH=$gmp/lib/pkgconfig $mpfr_src/configure \ CFLAGS="$(pkg-config --cflags-only-I gmp)" \ LDFLAGS="$(pkg-config --libs-only-L gmp)" \ --prefix=$mpfr \ --build=$platform \ --with-pic make make install

and for libmpc the pretty much only difference is that we ask pkg-config to generate those -I and -L arguments not only for gmp but also mpfr (note the extra mpfr argument to pkg-config and the extra entry in PKG_CONFIG_PATH):

PATH=$pkg_config/bin:$PATH export PKG_CONFIG_PATH=$gmp/lib/pkgconfig:$mpfr/lib/pkgconfig $libmpc_src/configure \ CFLAGS="$(pkg-config --cflags-only-I gmp mpfr)" \ LDFLAGS="$(pkg-config --libs-only-L gmp mpfr)" \ --prefix=$libmpc \ --build=$platform \ --with-pic make make install

As for sources, I unpacked the tarball for mpfr-4.1.0 in $mpfr_src and I unpacked the tarball for mpc-1.3.1 in $libmpc_src.

We built gmp, mpfr and libmpc, which GCC needs. Let’s now build GCC itself!

Building GCC

image

Here’s one way to build GCC, though there are others:

$gcc_src/configure \ AR=ar \ --with-gmp=$gmp \ --with-mpfr=$mpfr \ --with-mpc=$libmpc \ --with-gcc-major-version-only \ --disable-nls \ --build=$platform \ --enable-languages=c \ --with-sysroot=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk \ --prefix=$gcc make make install

For sources I unpacked the tarball for gcc-12.2.0 in $gcc_src. Apple Silicon users may need to use this patch, which adds some support for the aarch64-apple-darwin build platform.

Instead of using pkg-config, we use the ./configure options --with-<lib> to specify the install paths of the dependencies, though I’m sure it could be done with pkg-config too. We also simplify the build a bit by disabling “native language support” (natural languages, i.e. French, Spanish, etc) with --disable-nls, make gcc only report its major version number (7 instead of 7.23.5444-patch-foo) with --with-gcc-major-version-only and instruct it to only enable the C compiler, although you can add more with --enable-languages=c,c++,....

I do not know if it’s an artifact of only installing CLT (Command-Line Tools) instead of a full blown Xcode install, but headers that gcc expect in /usr/include are not there, and hence we need to specify the path to the CLT SDK with --with-sysroot. Your mileage may very though!

For more information about the configuration, you can check out the full list of options with ./configure --help, which you can also find on the GNU website.

This is not a short build, so go grab a coffee or a book!

Go and build more

foo

This is where our journey ends, for now at least! You should now have a working gcc executable in $gcc/bin/gcc.

We’ve seen how to build gcc and its dependencies (gmp, mpfr, libmpc) as well as the basic of using ./configure and make to build projects and pkg-config to specify dependencies. We also looked at the concept of build, host and target platforms.

This should be plenty to get you started! Have fun and go build some compilers now.


Like build systems? Here's more on the topic:

Start a conversation or share your thoughts below!