CMakeLists.txt란?
CMakeLists.txt는 CMake가 project를 어떻게 build할지 읽는 설정 파일이다.
C++ project에서는 compiler에 직접 긴 명령어를 입력하는 대신, CMakeLists.txt에 executable, library, include path, link library, compile option 등을 작성한다.
예를 들어 다음 명령어를 직접 입력할 수도 있다.
g++ -std=c++17 -Iinclude src/main.cpp src/math.cpp -o app
하지만 project가 커지면 source file과 option을 직접 관리하기 어렵다. CMake를 쓰면 build 설정을 파일로 관리할 수 있다.
cmake -S . -B build
cmake --build build
최소 CMakeLists.txt
가장 단순한 C++ project는 다음처럼 작성할 수 있다.
cmake_minimum_required(VERSION 3.16)
project(hello_cpp)
add_executable(hello
main.cpp
)
파일 구조는 다음과 같다.
hello_cpp/
CMakeLists.txt
main.cpp
main.cpp 예시는 다음과 같다.
#include <iostream>
int main() {
std::cout << "Hello, CMake\n";
return 0;
}
build는 다음처럼 한다.
cmake -S . -B build
cmake --build build
실행 파일은 보통 build directory 안에 생성된다.
./build/hello
기본 구조
일반적인 CMakeLists.txt는 다음 순서로 작성한다.
cmake_minimum_required(VERSION 3.16)
project(my_project
VERSION 0.1.0
LANGUAGES CXX
)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(my_app
src/main.cpp
)
각 줄의 의미는 다음과 같다.
| 명령어 | 의미 |
|---|---|
cmake_minimum_required | 필요한 최소 CMake version |
project | project 이름, version, language 설정 |
set(CMAKE_CXX_STANDARD ...) | C++ 표준 설정 |
add_executable | 실행 파일 target 생성 |
CMake에서는 executable이나 library를 target이라고 부른다.
현대 CMake는 전역 설정보다 target 단위 설정을 권장한다.
target 중심으로 작성하기
예전 CMake에서는 다음처럼 전역 include path를 설정하는 경우가 많았다.
include_directories(include)
하지만 현대 CMake에서는 target에 직접 설정하는 방식이 좋다.
add_executable(my_app
src/main.cpp
)
target_include_directories(my_app
PRIVATE
include
)
target 중심으로 작성하면 어떤 executable이나 library가 어떤 include path, compile option, link library를 사용하는지 명확해진다.
target_include_directories
target_compile_features
target_compile_options
target_link_libraries
target_compile_definitions
source file 추가
source file이 여러 개라면 add_executable에 함께 적는다.
add_executable(my_app
src/main.cpp
src/calculator.cpp
src/logger.cpp
)
파일 구조는 다음과 같다.
my_project/
CMakeLists.txt
include/
calculator.hpp
logger.hpp
src/
main.cpp
calculator.cpp
logger.cpp
include directory를 target에 연결한다.
target_include_directories(my_app
PRIVATE
include
)
PRIVATE는 이 include path가 my_app을 build할 때만 필요하다는 의미이다.
library 만들기
공통 코드는 library target으로 분리할 수 있다.
add_library(my_math
src/math/add.cpp
src/math/sub.cpp
)
target_include_directories(my_math
PUBLIC
include
)
실행 파일은 library를 link한다.
add_executable(my_app
src/main.cpp
)
target_link_libraries(my_app
PRIVATE
my_math
)
전체 예시는 다음과 같다.
cmake_minimum_required(VERSION 3.16)
project(my_project LANGUAGES CXX)
add_library(my_math
src/math/add.cpp
src/math/sub.cpp
)
target_include_directories(my_math
PUBLIC
include
)
add_executable(my_app
src/main.cpp
)
target_link_libraries(my_app
PRIVATE
my_math
)
PUBLIC, PRIVATE, INTERFACE
CMake target 설정에서 자주 나오는 keyword가 있다.
| keyword | 의미 |
|---|---|
PRIVATE | 현재 target을 build할 때만 필요 |
PUBLIC | 현재 target과 이 target을 사용하는 target 모두 필요 |
INTERFACE | 현재 target 자체에는 필요 없고 사용하는 target에만 필요 |
예를 들어 library header가 include directory에 있고, 이 header를 사용하는 쪽에서도 include path가 필요하다면 PUBLIC을 사용한다.
target_include_directories(my_library
PUBLIC
include
)
반대로 library 내부 구현 파일에서만 필요한 include path라면 PRIVATE가 맞다.
target_include_directories(my_library
PRIVATE
src/internal
)
header-only library는 source file 없이 interface만 제공하므로 INTERFACE를 사용할 수 있다.
add_library(my_header_only INTERFACE)
target_include_directories(my_header_only
INTERFACE
include
)
C++ 표준 설정
C++17을 사용하려면 다음처럼 전역으로 설정할 수 있다.
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
하지만 target 단위로 설정하는 방식도 좋다.
target_compile_features(my_app
PRIVATE
cxx_std_17
)
library라면 이 표준 요구사항이 사용자에게도 전파되어야 하는지 생각해야 한다.
target_compile_features(my_library
PUBLIC
cxx_std_17
)
my_library의 public header가 C++17 문법을 사용한다면 PUBLIC이 맞다.
내부 구현만 C++17을 사용한다면 PRIVATE로 충분하다.
compile option
compiler warning option을 추가하려면 target_compile_options를 사용한다.
target_compile_options(my_app
PRIVATE
-Wall
-Wextra
-Wpedantic
)
하지만 compiler마다 option이 다르다. GCC/Clang 기준 option을 MSVC에 그대로 넣으면 문제가 될 수 있다.
간단히 compiler를 나눌 수 있다.
if(MSVC)
target_compile_options(my_app PRIVATE /W4)
else()
target_compile_options(my_app PRIVATE -Wall -Wextra -Wpedantic)
endif()
compile definition
macro를 정의하려면 target_compile_definitions를 사용한다.
target_compile_definitions(my_app
PRIVATE
APP_VERSION="0.1.0"
ENABLE_LOGGING
)
C++ 코드에서는 다음처럼 사용할 수 있다.
#ifdef ENABLE_LOGGING
// logging code
#endif
문자열 macro는 quote 처리가 헷갈릴 수 있으므로 필요한 경우 header file 생성 방식을 고려할 수도 있다.
option 만들기
사용자가 build option을 켜고 끌 수 있게 하려면 option()을 사용한다.
option(ENABLE_TESTS "Build tests" ON)
조건부로 test를 추가한다.
if(ENABLE_TESTS)
add_subdirectory(tests)
endif()
configure할 때 option 값을 바꿀 수 있다.
cmake -S . -B build -DENABLE_TESTS=OFF
subdirectory 나누기
project가 커지면 directory별로 CMakeLists.txt를 나눌 수 있다.
my_project/
CMakeLists.txt
src/
CMakeLists.txt
main.cpp
tests/
CMakeLists.txt
test_main.cpp
root CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(my_project LANGUAGES CXX)
add_subdirectory(src)
add_subdirectory(tests)
src/CMakeLists.txt:
add_executable(my_app
main.cpp
)
이렇게 하면 directory별로 build 설정을 나눌 수 있다.
외부 library 찾기
system에 설치된 library를 찾을 때는 find_package()를 사용한다.
예를 들어 OpenCV를 사용하는 경우:
find_package(OpenCV REQUIRED)
add_executable(my_app
src/main.cpp
)
target_link_libraries(my_app
PRIVATE
${OpenCV_LIBS}
)
package가 modern CMake target을 제공한다면 target 이름으로 link하는 것이 더 좋다.
find_package(fmt REQUIRED)
target_link_libraries(my_app
PRIVATE
fmt::fmt
)
fmt::fmt처럼 namespace가 붙은 target은 include path와 link option을 함께 들고 있으므로 관리가 쉽다.
install 규칙
실행 파일이나 library를 설치하려면 install()을 사용한다.
install(TARGETS my_app
RUNTIME DESTINATION bin
)
library와 header를 설치하는 예시는 다음과 같다.
install(TARGETS my_math
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/
DESTINATION include
)
설치는 다음 명령어로 실행한다.
cmake --install build
설치 prefix를 바꾸려면 configure 단계에서 지정한다.
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local
build type
single-config generator에서는 CMAKE_BUILD_TYPE을 지정할 수 있다.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
자주 쓰는 값은 다음과 같다.
| build type | 의미 |
|---|---|
Debug | debug symbol 포함, 최적화 낮음 |
Release | 최적화 활성화 |
RelWithDebInfo | 최적화 + debug symbol |
MinSizeRel | 크기 최소화 |
Ninja나 Makefile generator에서는 이 방식을 자주 사용한다. Visual Studio 같은 multi-config generator에서는 build 단계에서 config를 지정한다.
cmake --build build --config Release
좋은 CMakeLists.txt 기준
좋은 CMake 설정은 target 중심으로 읽힌다.
피하는 것이 좋은 예시는 다음과 같다.
include_directories(include)
link_directories(lib)
add_definitions(-DDEBUG)
대신 target 단위로 작성한다.
target_include_directories(my_app PRIVATE include)
target_link_libraries(my_app PRIVATE my_library)
target_compile_definitions(my_app PRIVATE DEBUG)
기준은 다음과 같다.
전역 설정보다 target 설정을 사용한다.
source file은 명시적으로 적는다.
include path는 PUBLIC/PRIVATE를 구분한다.
외부 library는 가능하면 imported target으로 link한다.
build directory는 source directory 밖에 둔다.
전체 예시
다음은 작은 C++ project의 CMakeLists.txt 예시이다.
cmake_minimum_required(VERSION 3.16)
project(sample_app
VERSION 0.1.0
LANGUAGES CXX
)
option(ENABLE_WARNINGS "Enable compiler warnings" ON)
add_library(sample_core
src/calculator.cpp
)
target_include_directories(sample_core
PUBLIC
include
)
target_compile_features(sample_core
PUBLIC
cxx_std_17
)
add_executable(sample_app
src/main.cpp
)
target_link_libraries(sample_app
PRIVATE
sample_core
)
if(ENABLE_WARNINGS)
if(MSVC)
target_compile_options(sample_app PRIVATE /W4)
target_compile_options(sample_core PRIVATE /W4)
else()
target_compile_options(sample_app PRIVATE -Wall -Wextra -Wpedantic)
target_compile_options(sample_core PRIVATE -Wall -Wextra -Wpedantic)
endif()
endif()
build:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
정리
CMakeLists.txt를 작성할 때 핵심은 target 중심으로 생각하는 것이다.
cmake_minimum_required()로 최소 CMake version을 정한다.project()로 project 이름과 language를 설정한다.add_executable()또는add_library()로 target을 만든다.target_include_directories()로 include path를 연결한다.target_link_libraries()로 library를 연결한다.target_compile_features()로 C++ 표준을 설정한다.PUBLIC,PRIVATE,INTERFACE를 구분한다.- build는
cmake -S . -B build,cmake --build build흐름을 사용한다.
작은 project에서는 최소 예제로 시작하고, source file, library, option, install 규칙을 필요한 만큼 추가하면 된다.