Clang Profiles: Composable compiler flag sets for Ubuntu
Getting compiler flags right is a project-by-project effort. Typical build configurations like security hardening, optimization, debug info, and static analysis all have reasonable sets of flags available in Clang, but there is no standard place that set can be defined. It often ends up scattered across build files, CI configs, or worse. The clang-profiles package is an effort to explore how this can be done more systematically, by setting up useful wrappers to invoke clang in useful ways with minimal effort.
The goal of sharing this is to gather feedback, whether positive or negative. In particular, the sets of flags chosen for various profiles could use input from folks engaged in that type of work.
The Mechanism: Clang Config Files
Clang has supported configuration files via --config since version 10. You can put a set of flags in a .cfg file and load it at compile time:
clang --config=/usr/lib/clang-profiles/release.cfg main.c -o main
Profiles compose. Later flags override earlier ones, and anything on the command line wins over the config:
clang --config=release.cfg --config=lto.cfg main.c -o main
clang --config=debug.cfg --config=asan.cfg test.c -o test
This is a useful primitive that doesn’t seem to get much use in practice. The missing piece is an agreed-upon set of profiles that capture what these flag sets should actually contain.
What’s in the Profiles
The package ships profiles for common scenarios. For example:
ubuntu-default is a baseline meant to replace bare clang invocations: -O2, standard security hardening flags, frame pointers preserved, and warnings enabled.
release adds aggressive optimization (-O3, NDEBUG) on top of the hardening flags.
debug goes the other direction: -O0 -g3, standalone debug info, macro definitions included, tail-call optimization disabled.
hardened enables CFI (-fsanitize=cfi), -D_FORTIFY_SOURCE=3, hidden visibility, and full RELRO. It requires LTO and has performance costs.
size is for size-optimized binaries.
The sanitizer profiles each bundle the flags needed for that tool to be useful, including -g and -fno-omit-frame-pointer so stack traces in error reports are readable:
asan- AddressSanitizer for heap/stack/global buffer overflows and use-after-freeubsan- UndefinedBehaviorSanitizer for null dereference, integer overflow, and bounds violationstsan- ThreadSanitizer for data races in multithreaded codemsan- MemorySanitizer for reads of uninitialized memory
Two profiles cover a PGO workflow. Build with pgo-generate, run against a representative workload, merge the collected data with llvm-profdata, then rebuild with pgo-use. The wrapper script reads a CLANG_PGO_PROFDATA environment variable to locate the profile data (this is a convention specific to these scripts, not a Clang built-in):
pgo-generate- instruments the binary at-O2to collect execution countspgo-use- uses the merged profile data to build at-O3with ThinLTO
The Wrapper Scripts
The Debian package installs wrapper scripts (clang-release, clang-debug, clang++-release, etc.) that embed the profile path so build systems can use them directly:
# autotools
CC=clang-release ./configure
# cmake
cmake -DCMAKE_C_COMPILER=clang-release -DCMAKE_CXX_COMPILER=clang++-release .
The idea is that the definition of “release” lives in one place, and updating the package updates every build that uses the wrapper. Whether that’s the right tradeoff is part of what I’d like feedback on.
Code and Feedback
Profiles: code.launchpad.net/~karljs/+git/clang-profiles
Packaging: code.launchpad.net/~karljs/+git/clang-profiles-deb
The package is also available in a PPA for testing directly: launchpad.net/~karljs/+archive/ubuntu/clang-profiles
The flag choices are the most important thing to get right and also the most subjective. If you have opinions about what should or shouldn’t be in these profiles, or about the approach in general, your input is welcome. Comments are open, or you can reach out to me directly.