Beginner’s Guide to CMake

(Press S to open the speaker’s notes)

https://mugg.io/how-to-cmake/beginner-cmake-slides

Trivial Program


					// hello.c
					#include <stdio.h>

					const char* getGreeting();

					int main() {
						printf("%s\n", getGreeting());
					}
				

					// greeting.c
					const char* getGreeting() {
						return "Hello, compiler!";
					}
				

One-Shot Manual Build


						$ gcc -o hello hello.c greeting.c
						$ ./hello
						Hello, compiler!
					
  • Compile and link all our sources in one go

Partial Builds

If we only update the greeting, do we really need to recompile hello.c as well?


						$ gcc -o hello.o -c hello.c
						$ gcc -o greeting.o -c greeting.c
						$ gcc -o hello hello.o greeting.o
						$ ./hello
						Hello, compiler!
						$ vim greeting.c
						$ gcc -o greeting.o -c greeting.c
						$ gcc -o hello hello.o greeting.o
						$ ./hello
						Hello, object files!
					
  • Compile each source into an object file
  • Link object files together into hello
  • Edit a single source file
  • Only recompile the modified file
  • Relink object files together into hello

Easy? Yes, but tedious, and difficult to scale.

We haven’t touched on compiler/linker flags, libraries, installers, …

Basic Build Systems

make and similar tools help orchestrate builds.


						# Makefile
						hello.o: hello.c
							gcc -o hello.o -c hello.c
						greeting.o: greeting.c
							gcc -o greeting.o -c greeting.c
						hello: hello.o greeting.o
							gcc -o hello hello.o greeting.o
					
  • Makefiles describe how to build targets
  • Targets have dependencies (both of which are usually files)
  • Commands are provided to convert dependencies into target outputs

						$ make hello
						gcc -o hello.o -c hello.c
						gcc -o greeting.o -c greeting.c
						gcc -o hello hello.o greeting.o
						$ ./hello
						Hello, object files!
						$ vim greeting.c
						$ make hello
						gcc -o greeting.o -c greeting.c
						gcc -o hello hello.o greeting.o
						$ ./hello
						Hello, make!
					
  • Run make to build object files and link them together
  • Edit a single source file
  • Run make again to rebuild only what’s necessary

Complications

  • Not every platform provides make
  • Makefiles are difficult to get right, a bit inflexible and aren’t always portable
  • Different compilers spell things differently (-flto vs. /LTCG)

And Now, in CMake!

The same program, but let’s use CMake to generate our Makefile for us.


					# CMakeLists.txt
					cmake_minimum_required(VERSION 3.0)
					project(hello)
					add_executable(
						hello
						hello.c greeting.c
					)
				

					$ mkdir build
					$ cd build
					$ cmake ..
					...
					$ make hello
					...
					$ ./hello
					Hello, make!
					$ vim ../greeting.c
					$ make hello
					...
					$ ./hello
					Hello, cmake!
				

And Now, in CMake!

The same CMake works across operating systems and compilers.


					# CMakeLists.txt
					cmake_minimum_required(VERSION 3.0)
					project(hello)
					add_executable(
						hello
						hello.c greeting.c
					)
				

					> mkdir build
					> cd build
					> cmake ..
					...
					> MSBuild.exe hello.sln
					...
					> .\Debug\hello.exe
					Hello, make!
					> notepad.exe ..\greeting.c
					> MSBuild.exe hello.sln
					...
					> .\Debug\hello.exe
					Hello, cmake!
				

Core Concept #0

  • CMake is a scriptable build system generator (a meta-build system)
  • In general…
    • CMakeLists.txt files are the bulk of the scripts
    • *.cmake files provide extra functionality, shared tools, etc.

Typical Project Layout


					$ tree demo
					demo
					├── cmake
					│   ├── dist.cmake
					│   └── tools.cmake
					├── CMakeLists.txt
					├── include
					├── src
					│   └── CMakeLists.txt
					└── test
							└── CMakeLists.txt
				

Core Concept #1

  • Everything is built around objects that have various properties
  • Objects include things like targets, files, directories, etc.
  • Properties modify how an object behaves or interacts with other objects
  • CMake defines many properties, but custom ones can be added

CMake Targets

  • The most common objects are targets
    • Executables
    • Libraries (static and shared)
    • Custom (e.g., code generators, static analysis checks, …)
  • One CMake target corresponds to one or more make/ninja/msbuild/etc. targets

						# CMakeLists.txt
						add_executable(hello)
						target_sources(hello PRIVATE
							hello.c
							greeting.c
						)
					

						# Makefile
						hello.o: hello.c
							gcc -o hello.o -c hello.c
						greeting.o: greeting.c
							gcc -o greeting.o -c greeting.c
						hello: hello.o greeting.o
							gcc -o hello hello.o greeting.o
					

Core Concept #2

  • Day-to-day modern CMake revolves around targets, their properties, and their relationships
  • add_* functions declare new targets
    • add_executable()
    • add_library()
    • add_custom_target()
  • target_* functions modify properties on existing targets
    • target_compile_definitions()
    • target_compile_options()
    • target_include_directories()
    • target_link_libraries()
    • target_sources()

Target Properties

  • Common target properties are modified with target_* functions
  • PRIVATE-scoped properties only affect the named target
  • INTERFACE-scoped properties only affect other targets that link to the named target
    • INTERFACE-scoped properties are applied transitively
  • The PUBLIC scope modifies both the private- and interface-scoped properties

						add_library(translator)
						target_sources(
							translator
							PRIVATE
								Core.cpp Dictionary.cpp Grammar.cpp
						)
					

						add_library(translator)
						target_include_directories(
							translator
							PUBLIC
								"${CMAKE_SOURCE_DIR}/include"
						)
					

						add_library(translator)
						target_compile_definitions(
							translator
							INTERFACE
								-DTRANSLATOR_VERSION=100297
						)
					

A Real Example


						cmake_minimum_required(VERSION 3.25 FATAL_ERROR)
						project(ezra-vm)
						add_library(ezra-vm)
						target_include_directories(ezra-vm PUBLIC include)

						if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
							target_compile_options(ezra-vm PUBLIC -Wall -Wextra -Werror)
						elseif (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
							target_compile_options(ezra-vm PUBLIC /Zc:__cplusplus /utf-8)
						endif()

						target_sources(ezra-vm PRIVATE
							include/ezra/vm/Assembler.hpp
							include/ezra/vm/Instruction.hpp
							...
							src/Vm.cpp
							src/Word.cpp
						)

						add_executable(testEzraVm)
						target_link_libraries(testEzraVm PRIVATE ezra-vm)
						target_sources(testEzraVm PRIVATE
							testAssembler.cpp
							testVm.cpp
							testWord.cpp
						)
					
  • Create a new library target
  • Add an include directory that should be used by this library and all its users
  • Set compiler flags that should be used by this library and all its users
  • Set sources that should only be compiled as part of this library
  • Create a new executable target
  • Link the executable target to our existing library
  • Set sources that should only be compiled as part of the executable

Getting Help

Questions?

Colored pencil sketch of somebody who just sat through a technical talk and remains very confused

https://mugg.io/how-to-cmake/beginner-cmake-slides