What's the difference between Make and CMake, and when do you use each?
Make is a build executor. It reads a Makefile that lists rules of the form target: dependencies; recipe, and runs the recipe whenever a dependency is newer than the target. Make is one layer — you write Make rules and Make compiles your code. Strengths: transparent (you see exactly what runs), no abstraction layers, ubiquitous. Weaknesses: header dependency tracking is manual (need -MMD -MP flags); cross-platform support is poor; doesn't generate IDE projects.
CMake is a meta-build system. It reads CMakeLists.txt (a portable, higher-level project description) and generates native build files: Makefiles, Ninja files, Visual Studio solutions, Xcode projects. You then run the generated build. CMake is two layers — you write CMakeLists.txt, CMake generates build files, the generated build executes them. Strengths: cross-platform, generates IDE projects, supports cross-compilation cleanly via toolchain files, target-based modern style scales well. Weaknesses: more abstraction (errors can be confusing); steeper learning curve; the generated files are not human-friendly.
When to use each:
- Pick Make for small projects (< 20 source files), single-platform, when you want maximum transparency, or when the existing build is a working Makefile and migration isn't justified
- Pick CMake for larger projects, multi-platform, when you need IDE integration (CLion, VS Code, Visual Studio), or when greenfielding a project with multiple developers
- Use IDE-generated builds (STM32CubeIDE, MCUXpresso) when working in a vendor-supported flow where the IDE is non-negotiable
For embedded specifically, CMake + Ninja is the modern default. The CMake toolchain file handles cross-compilation cleanly: a small arm-none-eabi.cmake script sets CMAKE_SYSTEM_NAME Generic, the cross-compiler paths, and a few build options. Pass -DCMAKE_TOOLCHAIN_FILE=arm-none-eabi.cmake to switch from native to cross builds without touching CMakeLists.txt.
Source: Build Systems Q&A
