cross compiling
sysroot
free linux kernel development course
free embedded systems course
PKG_CONFIG_LIBDIR
If you’ve been following along in this free embedded Linux course, you already know how to link a
library by hand with -l and -I flags. That’s fine for one library. It falls apart the moment
a library needs a dozen dependent flags, or when different versions of the same library expect different linker
arguments. This is exactly the problem pkg-config solves, and understanding it properly is essential
for anyone serious about a free linux kernel development course or embedded build pipeline, because
every non-trivial cross-compiled project — from a simple database client to a full Yocto image — leans on it.
What You Will Learn
- Why manually tracking compiler/linker flags breaks down as projects grow
- How pkg-config’s
.pcmetadata files work - Why the default pkg-config search fails for cross toolchains, and how to fix it with
PKG_CONFIG_LIBDIR - How to build a working cross-compile command using pkg-config’s output
- How to write your own
.pcfile for a library you ship
Prerequisites
- A working cross toolchain with a populated sysroot (covered earlier in this course)
- Comfort with basic gcc invocation and environment variables
- Familiarity with the concept of a sysroot as the toolchain’s private “root filesystem” for headers and libraries
The Problem pkg-config Solves
Say a library needs three link flags and two include paths, and depends on two other libraries which each need
their own flags. Hard-coding all of that into every Makefile that uses the library is fragile: the moment the
library is rebuilt with a different configuration, every consumer’s build flags go stale. pkg-config fixes this by
having each library ship a small text file describing its own flags, so consumers just ask pkg-config instead of
hard-coding anything.
Anatomy of a .pc File
Every library that supports pkg-config installs a <name>.pc file, normally under
/usr/lib/pkgconfig or /usr/lib/<arch>/pkgconfig. Here’s a minimal, original example
for a small logging library — not copied from any book, just illustrating the format:
prefix=/usr
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: eplog
Description: Tiny structured logging library for embedded targets
Version: 1.2.0
Libs: -L${libdir} -leplog
Libs.private: -lpthread
Cflags: -I${includedir}
| Field | Meaning |
|---|---|
Libs |
Public link flags every consumer needs |
Libs.private |
Extra flags needed only for static linking |
Cflags |
Include-path and preprocessor flags |
Requires |
Other pkg-config packages this one depends on (pulls their flags in too) |
Querying Flags With pkg-config
Once a .pc file exists, any consumer can ask pkg-config for the flags instead of hard-coding them:
$ pkg-config eplog --cflags --libs
-I/usr/include -L/usr/lib -leplog
That’s the whole point: one command, always up to date, regardless of how the library’s internal layout changes.
Where It Breaks: Cross Compiling
Run that same command while building for an ARM target and it usually fails outright, or worse, silently returns
host flags:
$ pkg-config eplog --cflags --libs
Package eplog was not found in the pkg-config search path.
Perhaps you should add the directory containing `eplog.pc'
to the PKG_CONFIG_PATH environment variable
No package 'eplog' found
pkg-config, by default, only looks inside the host’s pkgconfig directories. Your cross-compiled
copy of eplog and its .pc file live inside the toolchain’s sysroot, in a completely different location
that pkg-config never checks unless told to.
The Fix: PKG_CONFIG_LIBDIR
Point pkg-config directly at the sysroot’s pkgconfig directory using the PKG_CONFIG_LIBDIR
environment variable. Note this is LIBDIR, not PATH — setting PKG_CONFIG_PATH
instead adds to the search, while PKG_CONFIG_LIBDIR replaces it, which is what you want for cross
builds so host libraries never leak in.
$ export SYSROOT=$(arm-none-linux-gnueabihf-gcc -print-sysroot)
$ export PKG_CONFIG_LIBDIR=${SYSROOT}/usr/lib/pkgconfig
$ pkg-config eplog --cflags --libs
-I/usr/include -L/usr/lib -leplog
Now the flags returned are correct for the target, not the host. Chain that straight into your compile command:
$ PKG_CONFIG_LIBDIR=${SYSROOT}/usr/lib/pkgconfig \
arm-none-linux-gnueabihf-gcc $(pkg-config eplog --cflags --libs) \
eplog-demo.c -o eplog-demo
$ file eplog-demo
eplog-demo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)
Common Mistakes
- Using PKG_CONFIG_PATH instead of PKG_CONFIG_LIBDIR for cross builds — this appends to the
default search path rather than replacing it, so host.pcfiles can still be picked up and silently
break the build. - Forgetting to export the variable in every shell/CI step that invokes pkg-config, not just the
one where it was set. - Hand-editing prefix in .pc files after copying them between machines instead of letting the
build system regenerate them — this reintroduces exactly the staleness pkg-config was meant to prevent.
Best Practices
- Set
PKG_CONFIG_LIBDIRonce per cross-build session (a wrapper script or environment setup file
works well) rather than per command. - Never let host and target pkgconfig directories mix in the same search — always use LIBDIR, not PATH, for
cross work. - If you ship your own library, ship a
.pcfile with it — it costs a few lines and saves every
downstream consumer from guessing your flags.
Summary / Key Takeaways
- pkg-config replaces hand-maintained compiler/linker flags with a queryable, versioned metadata file per library.
- Cross builds must redirect pkg-config into the sysroot using
PKG_CONFIG_LIBDIR, not
PKG_CONFIG_PATH. - Once redirected, pkg-config’s output can be substituted directly into the cross gcc invocation.
Conclusion
pkg-config looks like a small utility, but skipping it — and hard-coding flags instead — is one of the fastest
ways to make a cross-compiled project unmaintainable. Once PKG_CONFIG_LIBDIR is set correctly for your
sysroot, every library that ships a .pc file becomes trivial to link against, on host or target alike.
FAQ
Why does pkg-config default to host paths even when I’m cross compiling?
pkg-config has no built-in concept of “target” versus “host” — it just searches a fixed default location unless
told otherwise. Redirecting it is the build system’s job, via PKG_CONFIG_LIBDIR.
What’s the difference between PKG_CONFIG_PATH and PKG_CONFIG_LIBDIR?
PKG_CONFIG_PATH is additive — it extends the default search. PKG_CONFIG_LIBDIR
replaces the default search entirely, which is what you want for cross builds so host packages can’t leak in.
Do I need a .pc file for every library I cross compile?
No — only libraries that ship pkg-config support have one. Plenty of smaller libraries still expect manual
-l/-I flags, and that’s fine.
Can build systems like Buildroot and Yocto set this up for me?
Yes — both configure PKG_CONFIG_LIBDIR (or an equivalent wrapper) automatically as part of their
cross environment, which is one reason they’re recommended over fully manual cross compiling for larger projects.
What happens if I forget to set PKG_CONFIG_LIBDIR?
Best case, pkg-config reports the package as not found. Worst case, it silently finds a same-named host package
and hands you host flags, producing a binary that fails to link or run on the target.
Keep Building Your Toolchain Skills
Next up: the classic pain points of cross compiling real-world packages — and when to stop doing it by hand.
