A sample C23 project built on samuel-stidham/c-project-skeleton. A small interactive calculator stands in for real domain code and shows the skeleton in use: CMake and Ninja drive the build, a thin Makefile provides the daily verbs, warnings are errors from the first compile, and the Check suite tests the same core library the app links.
It all works on clone: make build, make test, and make lint before you read a line of the CMake.
This repository was moved from dqfan2012/c-project-skeleton
c-project-sample/
├── CMakeLists.txt // The build: targets, flags, tests
├── Makefile // Verb layer: make debug, make test, make asan, ...
├── docs/ // Documentation
│ └── DEVELOPMENT_WORKFLOW.md
├── include/ // Public headers (calculator.h)
├── lib/ // Third-party libraries
├── scripts/ // Helper scripts for setup and maintenance
├── src/ // C sources (core library + main.c)
└── tests/ // Check test suitesThe build writes into build-<compiler>-<type>/ directories (for example build-clang-debug/), one per compiler and configuration, so builds never clobber each other. Sanitizer and analysis builds get their own build-asan/-style directories.
- CMake 3.28 or newer and Ninja
- A C23-capable compiler (recent clang or gcc)
- Check for unit testing (
brew install checkon macOS,apt install checkon Ubuntu) - clang-format (its own Homebrew formula, lands on PATH) and clang-tidy (from
brew install llvm) - A real GCC for
make gcc-analyze(native on Linux,brew install gccon macOS) - Optional: cppcheck, flawfinder, Valgrind (Linux), CBMC, Facebook Infer
Homebrew LLVM is keg-only and stays off PATH on purpose, so it never shadows Apple clang. The Makefile resolves tools in this order: environment variables, then the keg through brew --prefix llvm, then PATH. Apple clang remains the default compiler throughout. To make the tools available everywhere, export them once in ~/fish/env.fish:
if test (uname) = Darwin
set -x LLVM_PREFIX /opt/homebrew/opt/llvm
set -x CLANG_TIDY $LLVM_PREFIX/bin/clang-tidy
set -x SCAN_BUILD $LLVM_PREFIX/bin/scan-build
set -x LLVM_COV $LLVM_PREFIX/bin/llvm-cov
set -x LLVM_PROFDATA $LLVM_PREFIX/bin/llvm-profdata
endOn Linux the LLVM tools install onto PATH normally, so no setup is needed.
- Debug build (default):
make debugor justmake - Release build:
make release - Run the app:
make run(ormake run BUILD_TYPE=release), then type a calculation like3 + 4 - Switch compilers:
make debug CC=gcc
Tests use the Check framework and run through ctest:
make testThe layout is one suite per file: each test_*.c exports a suite creator, tests/suites.h declares it, and tests/runner.c registers it. To add a suite for new code, copy tests/test_calculator.c, rename its creator, and register it in both places. The CMake glob picks new files up at the next build.
One usage rule under our strict flags: for floating-point assertions use Check's *_eq_tol and *_ne_tol variants. The exact ck_assert_double_eq family will not compile because -Wfloat-equal rejects the comparison at the use site.
This project uses clang-format configured for K&R-derived 1TBS: every opening brace attaches to its line, every control statement gets braces, two-space indent, 120-column lines. Names are snake_case, with UPPER_CASE macros and enum constants (enforced by clang-tidy). Run make format to format and make tidy (or make lint) to lint. make compile-db links compile_commands.json to the repo root for clangd.
-
Lint:
make tidy(clang-tidy, includes clang-analyzer checks) -
Deep static analysis:
make analyze(scan-build) andmake gcc-analyze(GCC-fanalyzer, e.g.make gcc-analyze GCC=gcc-16) -
Other checkers:
make cppcheck,make flawfinder,make dependency-check -
All of the quality tools:
make quality -
Sanitizers:
make asan,make ubsan,make tsan,make lsan, or all in turn withmake sanitizers. Each builds into its own directory, runs the test runner under instrumentation, then runs the app on sample input. Apple clang lacks-fsanitize=leak. On macOS, install LLVM withbrew install llvmand runmake lsan CC="$(brew --prefix llvm)/bin/clang".The
lsantarget runs the tests withCK_FORK=no, because Check's fork mode exposes the parent's live allocations to LeakSanitizer in every child. Signal and exit tests require fork mode, so keep them out of suites that must pass underlsan. -
Coverage:
make llvm-coveragewrites an HTML report tocoverage/html/ -
Valgrind (Linux):
make valgrind-memcheck(test runner),make valgrind-cachegrind,make valgrind-callgrind,make valgrind-massif
This project is licensed under the MIT License.