Project idea: pkg-config support in compilers

I’ve got a lot of ideas in my head of things I’d like to see, but don’t have the time to see out myself. Perhaps this could be a series. However, unlike many “idea guys”, I’m aiming to explain how you could implement this. And please do – I’m sharing because I think it’d be beneficial, or at least interesting to think about.

Anyways, what I have wanted for a while is pkg-config support in compilers. Perhaps it could simplify smaller build systems for programs without needing to involve something like CMake. Or maybe it’s a bad idea, but we can at least learn along the way.

What’s pkg-config anyways?

As the free software/open source ecosystem grew rapidly in the 2000s, there was a proliferation of libraries. Because this was mostly all in C and C++, there were barely any conventions, and the few there were weren’t enforced. You had to think about where a library and its headers could be installed, especially if alternative roots or subdirectories (i.e. /usr/include vs. having to include /usr/include/libfoo on the search path) were involved. Good luck if you needed to check for versions! Probing for libraries led to a lot of hand-rolled and mostly bad autoconf code.

pkg-config was intended to solve this. It provides a standard mechanism for discovering packages, their dependencies, where they live, and how to use them. While build configuration systems like CMake, meson, and autoconf can include their own custom library finding code, they usually all can defer to pkg-config. Library authors, or failing that, your software distributor usually provides pkg-config files. A typical pkg-config file looks like this (note the dependencies):

prefix=/opt/local
includedir=${prefix}/include
libdir=${prefix}/lib

Name: Pango
Description: Internationalized text handling
Version: 1.55.0
Requires: gobject-2.0 >=  2.62, harfbuzz >=  2.6.0
Requires.private: glib-2.0 >=  2.62, gio-2.0 >=  2.62, fribidi >=  1.0.6, fontconfig >=  2.15.0, freetype2, xrender, xft >=  2.0.0, cairo >=  1.12.10
Libs: -L${libdir} -lpango-1.0
Libs.private: -lm -framework CoreFoundation -framework ApplicationServices
Cflags: -I${includedir}/pango-1.0

How is it used?

pkg-config emits compiler arguments you can usually just paste directly into your compiler invocation. You can specify the version needed too – it’ll error out if it’s not present. It’ll also paste in other libraries that need to be referenced (for example, for static libraries, you need to mention other dependencies since the dynamic linker won’t be there to find them):

gcc $(pkg-config --cflags "pango >= 1.50.0") -c -o foo.o foo.c
gcc $(pkg-config --libs pango) -o foo foo.o

And of course, you can use it from a makefile as well. The following example shows how you can include everyone’s favourite C hash table library when you have multiple objects:

JSONC_CFLAGS := $(shell pkg-config --cflags json-c)
JSONC_LDFLAGS := $(shell pkg-config --libs json-c)

program: $(OBJECTS)
        $(LD) $(JSONC_LDFLAGS) $(LDFLAGS) -o $@ $^ 

%.o: %.c
        $(CC) $(JSONC_CFLAGS) $(CFLAGS) -c -o $@ $^

Note that the pkg-config call is done in a variable to avoid multiple executions, and allow users to override it (while keeping flags people may want to override in a separate ariable) in case they’re not using pkg-config. However, this is still a little clumsy. It does involve shelling out, the error handling if the package isn’t found can result in confusing outputs for users (especially if the Makefile continues on anyways, pushing the error above), and it’s a lot of boilerplate. What if we could just specify the package instead and let the compiler deal with it, like below? And even have an equivalent of -I and -L?

gcc -P/opt/local/lib/pkgconfig -pbzip2 -c -o foo.o foo.c
gcc -P/opt/local/lib/pkgconfig -pbzip2 -o foo foo.o

I think this is pretty easy to use, and for the average program, simplifies build steps a lot. Note that -p and -P might already be used by your compiler for something unrelated. Treat this as a mockup.

How does it fit into a compiler?

If you thought about the role of the compiler binary (cc/c++, usually GCC or clang alias this), you’ll notice it does a lot of kind of unrelated things. Yes, it can compile an object, but it can also assemble assembly files, link objects, and a few other things. Often, it can do multiple of these in one invocation. It’s not all in one binary. The cc binary is a frontend for multiple programs behind the scenes, often called a “driver”. It has to normalize flags and files passed and give them to the appropriate stages of compilation. Things like optimization passes might be easy to handle since it’s part of the same compiler. However, the assembler and linker are usually not, and often aren’t from the same vendor. The driver still needs to handle passing the right flags and adapting as needed.

The driver needs to normalize flags, so it seems like a good spot to put absorbing the pkg-config output. It can munge them as if the user passed them manually. In the case a package couldn’t be used, it can error out without having to think of the best way to handle it in a shell script or makefile. Perhaps the simplest way would be for the compiler to shell out to pkg-config. Forking can be a little expensive though, so a library can be used.

pkg-config doesn’t have a library, but it turns out pkg-config also hasn’t been maintained and updated in years. However, a compatible replacement called pkgconf, which does provide a library for things like this. It’s also more efficient than the old pkg-config as well.

What are the problems?

Of course, host compilers are easy. There are other cases too, from cross-compiling to alternative sysroots. It’s possible that pkg-config integration could make these easier, but integrating this into the compiler infrastructure requires thinking about these cases. And if it does happen, there’s going to be a long tail of compilers that don’t support such an option, so the current ways will persist for a while in build systems. There’s also the fact that the C/C++ ecosystem is slowly receding in favour of newer languages, and those tend to have their own package managers, that often can handle C/C++ code themselves. Perhaps that makes this idea not worth it.

Leave a Reply

Your email address will not be published. Required fields are marked *