CMake is a cross-platform build system generator that simplifies the compilation of complex software projects written in multiple languages. It uses configuration files named CMakeLists.txt to generate native build environments such as Makefiles or Visual Studio projects.
A minimal example starts with a source file main.cpp:
#include <iostream>
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}
Accompanied by a CMakeLists.txt:
project(HelloProject)
set(SOURCES main.cpp)
add_executable(hello ${SOURCES})
To build using internal (in-source) compilation:
cmake .
make
./hello
However, out-of-source builds are strongly recommended to keep generated files separate from source code:
mkdir build
cd build
cmake ..
make
Core CMake Concepts
project(): Declares the project name and supported languages. It implicitly defines variables like<PROJECT_NAME>_SOURCE_DIRand<PROJECT_NAME>_BINARY_DIR. UsePROJECT_SOURCE_DIRandPROJECT_BINARY_DIRfor name-agnostic references.set(): Assigns values to variables. Example:set(SRC_FILES main.cpp helper.cpp).message(): Outputs messages during configuration. Types includeSTATUS,SEND_ERROR, andFATAL_ERROR.add_executable(): Creates an executable target from source files.
CMake syntax rules:
- Variables are referenced with
${VAR_NAME}. - Commands are case-insensitive, but uppercase is conventional.
- Arguments are space- or semicolon-separated.
Multi-directory Projects
For structrued projects:
project_root/
├── CMakeLists.txt
├── src/
│ ├── CMakeLists.txt
│ └── main.cpp
└── build/
Top-level CMakeLists.txt:
project(HelloApp)
add_subdirectory(src bin)
src/CMakeLists.txt:
add_executable(hello main.cpp)
The add_subdirectory(source_dir [binary_dir]) command includes subdirectories and optionally specifies where to place compiled outputs.
To control output locations globally:
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/bin)
set(LIBRARY_OUTPUT_PATH ${PROJECT_BINARY_DIR}/lib)
Installation Rules
Use install() to define what gets deployed:
# Install documentation
install(FILES COPYRIGHT README DESTINATION share/doc/hello)
# Install scripts
install(PROGRAMS runhello.sh DESTINATION bin)
# Install entire directory
install(DIRECTORY doc/ DESTINATION share/doc/hello)
Run installation after building:
make install # installs to default prefix (/usr/local)
make install DESTDIR=/tmp/test # installs to /tmp/test/usr/local
The install prefix is controlled by CMAKE_INSTALL_PREFIX.
Building Libraries
Create both static and shared libraries from the same source:
Directory structure:
lib/
├── hello.h
└── hello.cpp
lib/CMakeLists.txt:
set(LIB_SOURCES hello.cpp)
add_library(hello_static STATIC ${LIB_SOURCES})
set_target_properties(hello_static PROPERTIES
OUTPUT_NAME "hello"
CLEAN_DIRECT_OUTPUT 1
)
add_library(hello_shared SHARED ${LIB_SOURCES})
set_target_properties(hello_shared PROPERTIES
OUTPUT_NAME "hello"
VERSION 1.0
SOVERSION 1
CLEAN_DIRECT_OUTPUT 1
)
Install headers and libraries:
install(FILES hello.h DESTINATION include/hello)
install(TARGETS hello_static hello_shared
ARCHIVE DESTINATION lib # static libs (.a)
LIBRARY DESTINATION lib # shared libs (.so)
)
Using External Libraries
When consuming installed libraries:
// main.cpp
#include <hello.h>
int main() {
HelloFunc();
return 0;
}
In CMakeLists.txt:
include_directories(/usr/include/hello)
add_executable(app main.cpp)
target_link_libraries(app hello)
Alternatively, use environment variables like CMAKE_INCLUDE_PATH and CMAKE_LIBRARY_PATH to hint CMake about non-standard search paths.