Post

C and C++ Build with Make and Autotools

How Make and GNU Autotools build C and C++ projects, which files they need, how to lint, scan, test and cover, and a complete GitHub Actions pipeline.

C and C++ Build with Make and Autotools

Make is the oldest build tool still in daily use: a Makefile lists targets, their prerequisites and the shell commands that produce them. GNU Autotools (Autoconf + Automake) sits on top of Make to generate portable configure scripts and Makefiles. Much of the Linux and open-source world (and many embedded and legacy enterprise codebases) still builds this way, so DevOps engineers meet it regularly.

This post is part of Build Ecosystems by Programming Language. For modern C++ projects see CMake, Ninja, Conan and vcpkg.

At a glance

CategoryCommon tools
Build toolsGNU Make, BSD make, Autoconf, Automake, Libtool, pkg-config; Bear (generates compile_commands.json)
CompilersGCC, Clang
DependenciesSystem packages (apt, dnf), pkg-config, vendored sources
Build filesMakefile, *.mk, configure.ac, Makefile.am, autogen.sh, m4/ macros; generated: configure, Makefile.in, config.h
Lint and formatclang-tidy (via Bear), cppcheck, clang-format, -Wall -Wextra -Werror
SAST (open source)Clang Static Analyzer (scan-build), cppcheck, Flawfinder, CodeQL, Semgrep
SAST (commercial)Coverity, Klocwork, CodeSonar, Polyspace, Fortify, Checkmarx One
Runtime checksAddressSanitizer, UBSan, Valgrind, fuzzing with AFL++
SCABlack Duck (snippet matching), Mend, Syft/Grype on the resulting OS packages or image
LibrariesGLib, OpenSSL, libcurl, zlib (found with pkg-config)
Test and coveragemake check (Automake test harness), Check, CUnit, GoogleTest; gcov + gcovr / lcov
Build outputExecutables, .a / .so libraries, source tarballs (make dist), .deb / .rpm
CI cacheccache directory

How the build works

In CI, each stage of the generic build pipeline maps to a Make / Autotools command, plugin or scanner. Press play, or step through it:

  1. Source + configure.ac: A commit triggers CI. configure.ac and Makefile.am describe the build; autoreconf -fi generates configure and Makefile.in.
  2. Dependencies: System libraries come from the OS package manager (apt-get in CI) and are found by pkg-config during configure.
  3. Configure: ./configure CFLAGS="-O2 -Wall -Wextra -Werror" checks the toolchain and writes Makefile and config.h; warnings become errors.
  4. Build: make builds in parallel; bear -- make records compile_commands.json so clang-tidy can analyse the code.
  5. Test and coverage: make check runs the Automake test harness; a --coverage build plus gcovr --fail-under-line 80 enforces coverage.
  6. Security scans in parallel: scan-build --status-bugs and cppcheck check our code, Syft and Grype scan the resulting packages or image, and Gitleaks scans for secrets.
  7. Quality gate: make check and the analyzers must pass, and releases are gated on make distcheck.
  8. Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
  9. Release tarball (pass): make distcheck builds the release tarball, unpacks it, then builds and tests it out of tree.
  10. SBOM and signing (pass): Syft generates the SBOM for the tarball or packages, and Sigstore cosign signs them.
  11. Store the tarball (pass): CI keeps the tarball with actions/upload-artifact; distributions repackage it as .deb or .rpm.
  • Make rebuilds a target when any prerequisite is newer than the target (timestamp-based). Recipes are shell commands and must be indented with a tab.
  • Autotools is a generator: maintainers run autoreconf to produce configure; users and CI run ./configure && make. Release tarballs ship the generated configure so users do not need Autotools installed.
  • make distcheck builds the tarball, unpacks it, builds and tests it out of tree, and installs it to a temporary prefix; it is the best release gate for Autotools projects.

Build files

FilePurposeCommit?
MakefileTargets, prerequisites and recipes (plain Make projects)Yes
*.mkIncluded Makefile fragmentsYes
configure.acAutoconf input: checks for compilers, headers, librariesYes
Makefile.amAutomake input: programs, sources, testsYes
autogen.shConvenience wrapper for autoreconf -fiYes
m4/Custom Autoconf macrosYes
configure, Makefile.in, aclocal.m4, config.h.inGenerated by AutotoolsNo in git, yes in release tarballs
config.log, config.status, Makefile (generated)Results of ./configureNo

Commands by category

1
2
3
4
5
6
7
8
9
autoreconf -fi && ./configure CFLAGS="-O2 -Wall -Wextra -Werror"   # generate + configure
make -j"$(nproc)"                                # build
bear -- make -j"$(nproc)"                        # build and record compile_commands.json
clang-tidy -p . src/*.c                          # lint (needs compile_commands.json)
scan-build --status-bugs make -j"$(nproc)"       # SAST (Clang Static Analyzer)
cppcheck --enable=warning,portability --error-exitcode=1 src/   # SAST
make check                                       # tests
make CFLAGS="--coverage -O0" check && gcovr -r . --fail-under-line 80   # coverage gate
make distcheck                                   # release tarball + full verification

Minimal project

A plain Makefile with automatic header dependency tracking (recipes use tabs):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
CC      ?= cc
CFLAGS  ?= -O2 -Wall -Wextra -Werror
CFLAGS  += -MMD -MP
SRC     := $(wildcard src/*.c)
OBJ     := $(SRC:src/%.c=build/%.o)

.PHONY: all clean check
all: build/demo

build/demo: $(OBJ)
	$(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)

build/%.o: src/%.c | build
	$(CC) $(CFLAGS) -c -o $@ $<

build:
	mkdir -p build

check: build/demo
	./tests/run.sh

clean:
	rm -rf build

-include $(OBJ:.o=.d)
Same project with Autotools (configure.ac + Makefile.am)
1
2
3
4
5
6
7
# configure.ac
AC_INIT([demo], [1.0.0])
AM_INIT_AUTOMAKE([foreign -Wall -Werror subdir-objects])
AC_PROG_CC
PKG_CHECK_MODULES([CURL], [libcurl])
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
1
2
3
4
5
6
7
# Makefile.am
bin_PROGRAMS = demo
demo_SOURCES = src/main.c
demo_CFLAGS  = $(CURL_CFLAGS)
demo_LDADD   = $(CURL_LIBS)

TESTS = tests/run.sh

Frameworks and libraries

LibraryUse it from Make / Autotools
libcurlPKG_CHECK_MODULES([CURL], [libcurl]) or $(shell pkg-config --cflags --libs libcurl)
OpenSSLPKG_CHECK_MODULES([SSL], [openssl])
GLibPKG_CHECK_MODULES([GLIB], [glib-2.0])
Check (unit tests)PKG_CHECK_MODULES([CHECK], [check]) + TESTS = tests/check_demo

CI with GitHub Actions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: make-ci
on: [push, pull_request]
permissions:
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: sudo apt-get update && sudo apt-get install -y autoconf automake libtool pkg-config libcurl4-openssl-dev cppcheck clang-tools bear gcovr
      - uses: hendrikmuhs/ccache-action@v1.2
        with:
          key: ${{ runner.os }}-make

      - run: autoreconf -fi
      - run: ./configure CC="ccache gcc" CFLAGS="-O2 -Wall -Wextra -Werror"
      - run: scan-build --status-bugs make -j"$(nproc)"
      - run: cppcheck --enable=warning,portability --error-exitcode=1 src/
      - run: make check
      - run: make distcheck

      - uses: actions/upload-artifact@v4
        with:
          name: tarball
          path: '*.tar.gz'

Best practices

  • Declare .PHONY targets and use -MMD -MP so header changes trigger rebuilds.
  • Build in parallel with make -j"$(nproc)", and make sure the dependency graph is complete so parallel builds are deterministic.
  • Do not commit generated Autotools files; generate them in CI with autoreconf -fi and ship them only in release tarballs.
  • Gate releases on make distcheck.
  • Use DESTDIR for staged installs when packaging (make install DESTDIR=$PWD/pkgroot).
  • Generate compile_commands.json with Bear so modern linters and analyzers work on Make projects.
  • Consider migrating large or cross-platform projects to CMake or Meson; keep Make as a thin task runner if the team likes make test / make lint.
This post is licensed under CC BY 4.0 by the author.