cmake는 크로스 플랫폼 빌드 시스템 생성기(cross-platform build system generator)이며, 1999년부터 현재까지 개발되어 오래된 코드 작성법부터 최신의 코드 작성법까지 다양한 사용 방법이 공존하고 있음
실제로 github을 보면 cmake로 코드를 관리하는 각각의 프로젝트에서 여러 스타일의 코드들을 볼 수 있음
본 문서에서는 CMake에 대한 Modern 스타일의 소스코드 관리 방법을 정리함
대부분의 자료는 CMake Documentation과 Meeting C++ 2018, 2019에서 발표된 More Modern CMake와 Oh No! More Modern CMake의 내용을 참조함
CMake 언어의 세대는 Traiditional, Moden, More Modern으로 나뉨(비공식?)
- Traiditional : 1999/2000년 초기 CMake부터 Modern CMake 전
- Modern : CMake 3.0 ~ / 2014년경 타겟 개념 추가
- More Modern : CMake 3.12 / 2018년경 대규모 구현 완료
-
target
- 빌드 결과물인 실행파일(executable)과 라이브러리(library)
add_library(my_lib STATIC) # my_lib 이름을 가지는 target으로 libmy_lib.a를 생성(리눅스 환경에서의 static library 빌드 결과로 접두어 lib가 붙음) add_executable(main) # main 이름을 가지는 target으로 main 출력물을 생성
- 빌드 관련 여러 속성(property)들을 포함하는 컨테이너
- 속성으로는 타입, 소스파일, include 탐색 경로, 전처리 매크로, 링크 의존성, 컴파일러 옵션 등이 있음. CMake 타겟에 대한 속성들
SOURCES LINK_LIBRARIES INCLUDE_DIRECTORIES COMPILE_DEFINITIONs COMPILE_FLAGS ... - 위의 속성들은 ', '를 구분자로 하는 리스트 구조임
- 속성 필드들로 구성된 c/c++의 구조체라 보면 이해하기 쉬울 듯
// cpp style의 구조체 정의 struct TARGET { string type; // 실행파일 또는 라이브러리 list<string> sources; // 소스코드 파일 이름들 list<string> link_libraries; // 링크 의존성 list<string> include_directories; // 헤더파일 탐색 경로들 list<string> compile_definitions; // 전처리 매크로 정의들 list<string> compile_flags; // 지정한 컴파일 플래그들 ... }; // target 변수 선언(초기화 과정은 생략) TARGET my_lib; TARGET main;
- 타겟에 속성을 추가하는 방법
add_library또는add_executable함수는 TYPE 속성을 설정함- 속성별
target_*함수 사용target_sources,target_link_libraries,target_include_directories등이 있음# target에 소스를 추가 target_sources(main PRIVATE main.cpp) # target에 링크 의존성(link dependencies)를 추가. "main은 my_lib 타겟(또는 라이브러리)가 필요하다"라는 의미 target_link_libraries(main PRIVATE my_lib)
set_target_properties(<target> PROPERTIES <property> <value>)함수 사용target_*함수들은 이 함수의 레퍼 형태# target에 소스를 추가 set_target_properties(main PROPERTIES SOURCES main.cpp) # target에 링크 의존성(link dependencies)를 추가. "main은 my_lib 타겟(또는 라이브러리)가 필요하다"라는 의미 set_target_properties(main PROPERTIES LINK_LIBRARIES my_lib)
- 빌드 결과물인 실행파일(executable)과 라이브러리(library)
-
target 간 의존성
target_link_libraries함수(또는set_target_properties)를 사용하여 target 간 의존성을 생성할 수 있음- CMake는 target에 대한 속성을 의존성에 따라 전파될 수 있게 설계됨
- CMake 코드 작성자는 속성이 다른 타겟으로 전파(propagation) 될지를 선택할 수 있음
target_link_libraries(A PRIVATE B)는 "A 타겟은 B 타겟에 의존한다"를 의미하며, B 타겟에서 전파되도록 설정한 속성들이 A 타겟으로 전파되어 A 타겟의 관련 속성으로 추가됨
-
target 속성 중 전파되는 속성들
- 앞서 target 속성으로
SOURCES,LINK_LIBRARIES등이 있음을 설명함 - 추가적으로, target 속성 중 소스파일, include 탐색 경로, 전처리 매크로, 링크 의존성, 컴파일러 옵션 등에 대해 전파되는 속성들이 있음
- 전파되는 속성들 앞에는
INTERFACE_접두어가 붙음. CMake 타겟에 대한 속성들 - 다음 위쪽은 전파되지 않는 속성들이고,
INTERFACE_가 붙은 아래쪽은 전파되는 속성들임SOURCES LINK_LIBRARIES INCLUDE_DIRECTORIES COMPILE_DEFINITIONs COMPILE_FLAGS ... INTERFACE_SOURCES INTERFACE_LINK_LIBRARIES INTERFACE_INCLUDE_DIRECTORIES INTERFACE_COMPILE_DEFINITIONS INTERFACE_COMPILE_OPTIONS ... - 타겟의
INTERFACE_*속성은 이 타겟을 의존하는 모든 타겟에 전파됨 - 예를 들어,
target_link_libraries(A PRIVATE B)에서 B 타겟의 INTERFACE_INCLUDE_DIRECTORIES 속성은 A 타겟의 INCLUDE_DIRECTORIES 속성에 추가됨
- 앞서 target 속성으로
-
target 속성 중 전파되는 속성 설정
set_target_properties로 설정 가능, 그러나target_*커맨드를 활용하는 더 간편한 방법이 있음- 앞선 예제 코드들에서
target_*커맨드에서PRIVATE키워드를 사용함 PRIVATE키워드는 "해당 속성을 전파하지 않는다"를 의미함- 속성을 의존성 관계에 따라 전파하려면
INTERFACE키워드를 사용해야함 - 예, 라이브러리 코드들을 특정 디렉터리에 넣어서 관리할 때, 라이브러리 타겟에 대한 include 디렉터리 경로를
INTERFACE로 설정하여, 이 타겟에 의존하는 타겟들이 자동으로 include 디렉터리를 전파받을 수 있게 할 수 있음# root/subdirectory/CMakeLists.txt add_library(sub_lib) target_sources(sub_lib PRIVATE sub_lib.h sub_lib.cpp) # CMAKE_CURRENT_SOURCE_DIR 변수는 현재 소스코드가 있는 디렉터리 경로를 의미하며, 여기선 "root/subdirectory"로 변환됨 target_include_directories(sub_lib INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
# root/CMakeLists.txt add_subdirectory(subdirectory) # root/subdirectory/CMakeLists.txt를 읽는다. add_executable(main main.cpp) # main 타겟은 sub_lib 타겟에 의존함으로써 INCLUDE_DIRECTORY를 전파받아 "root/subdirectory"를 include 탐색 경로로 사용한다. target_link_libraries(main PRIVATE sub_lib)
// root/main.cpp // main.cpp는 "./subdirectory/sublib.h"로 안해도 됨 #include "sublib.h" ...
-
build-requirements
- build-requirements란 target을 **빌드(build)**하기 위해 필요한 것들을 의미함
- 공식 문서에서는 build-specification으로 표현함
target_*함수에PRIVATE키워드를 지정하면 해당 속성은 build-requirements가 됨# build-requirements 지정 예시 target_sources( <target> PRIVATE <source-file>... ) target_link_libraries( <target> PRIVATE <dependency>... ) target_include_directories( <target> PRIVATE <include-search-dir>... )
- 소스파일, include 탐색 경로, 전처리 매크로, 링크 의존성, 컴파일러 옵션 등이 build-requirements에 해당함
-
usage-requirements
- 다른 target에서 이 target을 **사용(use)**하기 위해 필요한 것들
- target 속성 중 전파되는 속성들이 이에 해당함
- 접두어 `INTERFACE_'가 붙은 속성들
- 소스 파일, include 탐색 경로, 전처리 매크로, 링크 의존성, 컴파일러 옵션 등
- 이 라이브러리를 사용하려면 어떤 전처리 매크로를 넣어줘야 하는지, 어떤 디렉터리를 include 해야하는지, 어떤 컴파일 옵션 등을 지정해야 하는지를 빌드 시스템 측면에서 기술한 것
target_*함수에INTERFACE로 지정한 것들# usage-requirements 지정 target_sources( <target> INTERFACE <source-file>... ) target_link_libraries( <target> INTERFACE <dependency>... ) target_include_directories( <target> INTERFACE <include-search-dir>... )
-
target_*커맨드의PRIVATE,INTERFACE,PUBLIC키워드- PRIVATE, INETERFACE는 위에서 설명했지만 한번 더 간단하게 설명하고, PUBLIC 키워드를 소개함
- PRIVATE
- 지정한 타겟에서만 사용됨(전파되지 않음)
- 지정한 타겟을 빌드하기 위해 사용됨(build-requirements)
- INTERFACE
- 지정한 타겟의 속성을 지정한 타겟에 의존하는 타겟에 전파(propagation)
- 지정한 타겟을 사용하기 위해 필요한 속성으로 지정(usage-requirements)
- 빌드하는데 사용되지 않음
- PUBLIC
- 해당 속성을 build-requirements와 usage-requirements으로 둘다 설정
- 이 키워드를 사용하면 PRIVATE으로도 설정하고 INTERFACE로도 설정
- 예를 들어, target_include_directories에 PUBLIC 키워드를 사용하면
INCLUDE_DIRECTORIES속성과INTERFACE_INCLUDE_DIRECTORIES속성을 같은 값으로 설정함 - 즉 빌드하는데도 사용하고, 사용하기 위해 필요하므로 전파되도록 함
-
transitive usage-requirements
- Transitive는 국어로 추이적이라함
추이적 : x가 y와 관계가 있고 y가 z와 관계가 있으면, x가 z와 관계가 있는. 또는 그런 것.
- A 타겟이 B 타겟에 의존하고, B 타겟이 C 타겟에 의존하면 C 타겟에 대한 속성이 B 타겟을 거쳐 A 타겟까지 전파되는 것을 말함
target_link_libraries함수에서INTERFACE또는PUBLIC키워드를 지정하여 transitive usage-requirements 가능- 아래 코드를 예로 들어, lib_C -> lib_B -> main_A 순으로 usage-requirements 전파가 일어남
add_library(lib_C STATIC) target_include_directories(lib_C "some/header/directory") add_library(lib_B STATIC) target_link_libraries(lib_B PUBLIC lib_C) # lib_B 타겟은 "some/header/directory"를 자동으로 include 디렉터리로 설정 add_executable(main_A) target_link_libraries(main PRIVATE lib_B) # main 타겟 또한 "some/header/directory"를 자동으로 include 디렉터리로 설정
- transitive usage-requirements 참조
- Transitive는 국어로 추이적이라함
-
헤더 파일의 usage-requirements 전파
- 소스 파일 중 헤더 파일(.h)은 일반적으로 usage-requirements에 사용되지 않음. 그러나 Visual Studio 등의 IDE에서 프로젝트 구조를 읽는데 사용될 수 있음
# IDE를 고려한 소스코드 설정 add_library(my_lib STATIC) target_sources(my_lib PUBLIC my_lib.h PRIVATE my_lib.cpp ) # cpp는 전파되므로 PUBLIC 또는 INTERFACE로 지정 시 의도치 않은 동작 가능
- 소스 파일 중 헤더 파일(.h)은 일반적으로 usage-requirements에 사용되지 않음. 그러나 Visual Studio 등의 IDE에서 프로젝트 구조를 읽는데 사용될 수 있음
-
오브젝트 라이브러리
add_library함수에SHARED또는STATIC대신OBJECT를 사용하여 오브젝트 라이브러리 타겟을 정의- object-libraries를 보면 Normal Libraries와 오브젝트 라이브러리가 구분되어 있음
- 오브젝트 라이브러리를
SHARED또는STATIC라이브러리로 이해하면 힘듬. 특수한 형태의 라이브러리임. - 오브젝트 라이브러리는 결과물이 오브젝트 파일(
*.o)임. - 오브젝트 라이브러리 또한
target_link_libraries를 사용하여 타겟간 의존성 관계를 설정할 수 있음 - usage-requirements는 오브젝트 라이브러리를 포함한 의존성 관계에서 동일하게 전파됨
- 문제는 오브젝트 파일(
*.o)의 전파임 - 다음과 같이 1번의 전파만 이뤄지는 상황에서는 문제 없음
add_library(objLib OBJECT) add_executable(main_A) target_link_libraries(main PRIVATE objLib) # main.o와 objLib.o를 링크하여 main 실행파일 생성
- 문제는 오브젝트 라이브러리가 포함된 transitive usage-requirements 상황에서 발생함
- 규칙1) 오브젝트 라이브러리 타겟간 의존성에서 오브젝트 파일(
*.o)은 전파되지 않음add_library( obj OBJECT ) ... add_library( obj2 OBJECT ) target_sources( obj2 PRIVATE src.cpp ) target_link_libraries( obj2 PRIVATE obj ) # PRIVATE, PUBLIC, INTERFACE 여부와 관계 없이 오브젝트 파일(`*.o`)은 전파되지 않음 add_executable( exe ) target_sources( exe PRIVATE main.cpp ) target_link_libraries( exe PRIVATE obj2 )
- 규칙2) 오브젝트 라이브러리가 아닌 타겟에 대한 의존성은 build-requirements만 적용됨
add_library( obj OBJECT ) ... add_library( lib SHARED ) target_sources( lib PRIVATE src.cpp ) target_link_libraries( lib PRIVATE obj ) # lib.o와 obj.o를 링킹 # INTERFACE 키워드를 사용하면 오브젝트 파일(`*.o`)이 exe 타겟으로 전파되지 않음(의도한 빌드 과정 실패) # PUBLIC은 PRIVATE과 동일 add_executable( exe ) target_sources( exe PRIVATE main.cpp ) target_link_libraries( exe PRIVATE lib )
- 규칙3) 오브젝트 라이브러리 타겟에 의존하는 오브젝트 라이브러리가 아닌 타겟에 대한 링크 의존성 전파(
INTERFACE_LINK_LIBRARIES속성)는 usage-requirements만 적용됨add_library( obj OBJECT ) ... add_library( lib SHARED ) target_sources( lib PRIVATE src.cpp ) target_link_libraries( obj PRIVATE lib ) # obj는 lib와 링크되지 않고, exe 타겟으로 전파되지 않음 # INTERFACE 키워드는 lib를 exe로 전파함(exe가 obj를 거쳐 lib를 링크) # PUBLIC은 INTERFACE랑 동일함 add_executable( exe ) target_sources( exe PRIVATE main.cpp ) target_link_libraries( exe PRIVATE obj )
- Oh No! More Modern CMake을 보면 자세한 이해 가능
-
오브젝트 파일 전파 방법
- 오브젝트 라이브러리는 generator-expressions인
$<TARGET_OBJECTS:objlib>으로 오브젝트 파일(*.o)을 소스코드와 같이 지정 가능 objlib는 오브젝트 라이브러리 타겟 이름이며,$<TARGET_OBJECTS:objlib>은 빌드 시스템 생성 시간(Generation Time)에objlib.o로 평가됨# 공식 문서에서의 $<TARGET_OBJECTS:objlib> 사용 방법 안내 add_library(... $<TARGET_OBJECTS:objlib> ...) add_executable(... $<TARGET_OBJECTS:objlib> ...)
- 공식 문서에서의 안내는 컨테이너에서 필요 속성들을 관리하는 개념에 위배됨
- 오브젝트 라이브러리의 자기 자신의 오브젝트 파일을 소스코드에 포함함으로써 컨테이너 및 전파 개념 사용 가능하며, 규칙 1도 극복하여 transitive usage-requirements 가능
add_library(a OBJECT) target_sources(a PRIVATE a.cpp INTERFACE $<TARGET_OBJECTS:a> # obj 타겟을 의존하는 모든 타겟의 소스코드(SOURCE 속성)에 obj.o를 전파하게 한다. ) add_library(b OBJECT) target_sources(b PRIVATE b.cpp INTERFACE $<TARGET_OBJECTS:b> ) add_executable(main main.cpp) target_link_libraries(main PRIVATE a b)
- issues/18682 참조
- 오브젝트 라이브러리는 generator-expressions인
- traditional
set(BASIC_SOURCES "src/BasicMath.cpp" "src/HeavyMath.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/Math.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/MathAPI.h" ) add_library( basicmath_ObjLib OBJECT ${BASIC_SOURCES})
- modern
# add_library에 반드시 소스파일 하나를 지정해야 했음 add_library(basicmath_ObjLib OBJECT "src/dummy.cpp") target_sources(basicmath_ObjLib PRIVATE "src/BasicMath.cpp" "src/HeavyMath.cpp" PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/Math.h" INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include/MathAPI.h" )
- more modern
# 3.11 버전부터 소스파일 지정 불필요 add_library(basicmath_ObjLib OBJECT) target_sources(basicmath_ObjLib PRIVATE "src/BasicMath.cpp" "src/HeavyMath.cpp" PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include/Math.h" INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/include/MathAPI.h" )
- more modern (3.13 버전 이후)
# 3.13 버전부터는 소스코드 경로가 상대 경로일 때 자동으로 ${CMAKE_CURRENT_SOURCE_DIR} 접두어 추가 add_library(basicmath_ObjLib OBJECT) target_sources(basicmath_ObjLib PRIVATE "src/BasicMath.cpp "src/HeavyMath.cpp" PUBLIC "include/Math.h" INTERFACE "include/MathAPI.h" )
-
traditional
add_library( basicmath_ObjLib OBJECT ${BASIC_SOURCES} ) # include_directories는 CMakeLists.txt에 있는 모든 타겟에 적용되며, build-requirements만 적용 include_directories( "${CMAKE_CURRENT_SOURCE_DIR}/include" ) # 컴파일 플래그 설정은 전역으로 적용됨 set( CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11" CACHE STRING "C++ compile-flags" FORCE )
-
modern
add_library( basicmath_ObjLib OBJECT "src/dummy.cpp" ) # 타겟에만 적용되는 include 경로 target_include_directories( basicmath_ObjLib PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/include" ) # 타겟에만 적용되는 컴파일 플래그 target_compile_features( basicmath_ObjLib PUBLIC cxx_constexpr )
-
more modern
- 변경 사항 없음
-
target_*함수 목록target_include_directories( <target> PRIVATE <include-search-dir>... ) target_compile_definitions( <target> PRIVATE <macro-definitions>... ) target_compile_options( <target> PRIVATE <compiler-option>... ) target_compile_features( <target> PRIVATE <feature>... ) target_sources( <target> PRIVATE <source-file>... ) target_precompile_headers( <target> PRIVATE <header-file>... ) target_link_libraries( <target> PRIVATE <dependency>... ) target_link_options( <target> PRIVATE <linker-option>... ) target_link_directories( <target> PRIVATE <linker-search-dir>... ) -
target_link_libraries에서 외부 라이브러리와 내부 타겟 구분을 위한 코드 스타일
- 프로젝트 내부 라이브러리 :
target이름 그대로 사용 - 외부 라이브러리 : gcc의 링크 옵션과 같이
-l[외부 라이브러리]로 사용. 예)-ljpeg-lpng
- 프로젝트 내부 라이브러리 :
-
Cmake는 최소 3.16 버전부터 사용하는게 좋은 듯 싶다.