[모던 CMake] 생성기 표현식과 커스텀 명령어 활용

모던 CMake를 활용하여 효율적인 C++ 프로젝트 빌드 시스템을 구축하는 방법을 계속해서 알아보겠습니다. 이번 글에서는 생성기 표현식(generator expressions)커스텀 명령어(custom commands)를 활용하여 빌드 설정을 더욱 유연하고 강력하게 만드는 방법을 다루겠습니다. 또한, 대규모 프로젝트에서 모던 CMake를 효과적으로 사용하는 방법에 대해서도 살펴보겠습니다.

생성기 표현식 (Generator Expressions)

생성기 표현식은 빌드 시점에 조건부로 값을 평가하여 빌드 설정을 동적으로 제어할 수 있도록 하는 CMake의 기능입니다. <...> 형태로 표현되며, 타겟의 프로퍼티나 빌드 구성 등에 따라 다른 값을 사용할 수 있습니다.

기본 문법

생성기 표현식은 $<...> 형태로 사용되며, 내부에 조건과 결과를 포함합니다.

예시:

$<CONDITION:VALUE_IF_TRUE>

주요 생성기 표현식 예제

빌드 타입에 따른 설정

target_compile_definitions(MyApp PRIVATE
    $<$<CONFIG:Debug>:DEBUG_MODE>
)
  • 빌드 타입이 Debug일 경우에만 DEBUG_MODE 매크로를 정의합니다.

타겟 속성에 따른 설정

target_compile_options(MyLib PRIVATE
    $<$<CXX_COMPILER_ID:GNU>:-Wall>
)
  • 컴파일러가 GCC일 경우에만 -Wall 옵션을 추가합니다.

플랫폼에 따른 설정

target_link_libraries(MyApp PRIVATE
    $<$<PLATFORM_ID:Windows>:Ws2_32>
)
  • Windows 플랫폼에서만 Ws2_32 라이브러리를 링크합니다.

고급 생성기 표현식

AND, OR, NOT 연산자

$<$<AND:$<BOOL:${VAR1}>,$<BOOL:${VAR2}>>:VALUE_IF_TRUE>
  • VAR1과 VAR2가 모두 참일 경우에만 VALUE_IF_TRUE를 적용합니다.

비교 연산자

$<$<VERSION_GREATER_EQUAL:${CMAKE_CXX_COMPILER_VERSION},10.0>:NEW_FEATURE_ENABLED>
  • 컴파일러 버전이 10.0 이상일 경우 NEW_FEATURE_ENABLED를 정의합니다.

커스텀 명령어와 타겟

CMake에서 커스텀 명령어(custom command)커스텀 타겟(custom target)을 사용하여 빌드 과정에 사용자 정의 작업을 추가할 수 있습니다.

add_custom_command()

add_custom_command()는 특정 파일을 생성하거나 명령을 실행하는 데 사용됩니다.

예제: 파일 생성

add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/generated.h
    COMMAND python generate_header.py > ${CMAKE_BINARY_DIR}/generated.h
    DEPENDS generate_header.py
)
  • generate_header.py 스크립트를 실행하여 generated.h 파일을 생성합니다.

add_custom_target()

add_custom_target()은 빌드 시스템에 새로운 타겟을 추가합니다.

예제: 문서 생성 타겟

add_custom_target(
    docs
    COMMAND doxygen Doxyfile
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMENT "Generating API documentation with Doxygen"
)
  • make docs 명령으로 Doxygen 문서를 생성할 수 있습니다.

빌드 의존성 설정

커스텀 명령어로 생성된 파일을 타겟의 의존성으로 설정하여 빌드 순서를 제어할 수 있습니다.

add_executable(MyApp src/main.cpp ${CMAKE_BINARY_DIR}/generated.h)

add_custom_command(
    OUTPUT ${CMAKE_BINARY_DIR}/generated.h
    COMMAND python generate_header.py > ${CMAKE_BINARY_DIR}/generated.h
    DEPENDS generate_header.py
)

add_dependencies(MyApp generated_header)
  • generated_header 커스텀 타겟을 생성하고 MyApp이 해당 타겟에 의존하도록 합니다.

대규모 프로젝트에서의 모던 CMake 활용

프로젝트 규모가 커짐에 따라 CMake 설정도 복잡해질 수 있습니다. 모던 CMake의 기능을 활용하여 구조화된 빌드 시스템을 구축할 수 있습니다.

디렉토리 구조의 모듈화

프로젝트를 여러 개의 서브 디렉토리로 나누고, 각 디렉토리에 CMakeLists.txt 파일을 배치합니다.

my_project/
├── CMakeLists.txt
├── app/
│   ├── CMakeLists.txt
│   └── main.cpp
├── libs/
│   ├── lib1/
│   │   ├── CMakeLists.txt
│   │   ├── lib1.cpp
│   │   └── lib1.h
│   └── lib2/
│       ├── CMakeLists.txt
│       ├── lib2.cpp
│       └── lib2.h
└── tests/
    ├── CMakeLists.txt
    └── test.cpp

최상위 CMakeLists.txt

cmake_minimum_required(VERSION 3.15)
project(MyProject)

add_subdirectory(libs/lib1)
add_subdirectory(libs/lib2)
add_subdirectory(app)
add_subdirectory(tests)

라이브러리 CMakeLists.txt

각 라이브러리 디렉토리에서 타겟을 정의하고 필요한 설정을 추가합니다.

libs/lib1/CMakeLists.txt

add_library(Lib1 lib1.cpp)

target_include_directories(Lib1 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

# 필요한 컴파일 옵션이나 링크 라이브러리 설정

libs/lib2/CMakeLists.txt

add_library(Lib2 lib2.cpp)

target_include_directories(Lib2 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})

target_link_libraries(Lib2 PUBLIC Lib1)

# Lib2는 Lib1에 의존함

애플리케이션 CMakeLists.txt

add_executable(MyApp main.cpp)

target_link_libraries(MyApp PRIVATE Lib2)

# Lib2를 링크하면 Lib1도 자동으로 링크됨 (의존성 전파)

전역 설정 적용

프로젝트 전체에 적용할 설정은 최상위 CMakeLists.txt에서 지정합니다.

# 전역 컴파일 옵션 설정
add_compile_options(-Wall -Wextra -Wpedantic)

# C++ 표준 설정
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

함수와 매크로 활용

중복되는 CMake 코드를 줄이기 위해 함수와 매크로를 정의하여 재사용할 수 있습니다.

예제: 라이브러리 생성 함수

function(create_library TARGET_NAME)
    add_library(${TARGET_NAME} ${ARGN})
    target_include_directories(${TARGET_NAME} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
endfunction()
  • 라이브러리 생성과 인클루드 디렉토리 설정을 한 번에 처리합니다.

사용법

create_library(Lib1 lib1.cpp)
create_library(Lib2 lib2.cpp)

프로젝트 구성 설정 분리

프로젝트 설정을 모듈화하여 관리하기 위해 별도의 CMake 모듈 파일을 사용할 수 있습니다.

cmake/ 디렉토리 활용

프로젝트 루트에 cmake/ 디렉토리를 생성하고, 공통 설정이나 함수, 매크로를 정의하는 파일을 배치합니다.

my_project/
├── cmake/
│   ├── CompilerWarnings.cmake
│   └── Utilities.cmake
├── CMakeLists.txt
├── app/
├── libs/
└── tests/

CompilerWarnings.cmake 예제

function(enable_strict_compiler_warnings TARGET)
    if(MSVC)
        target_compile_options(${TARGET} PRIVATE /W4 /WX)
    else()
        target_compile_options(${TARGET} PRIVATE -Wall -Wextra -Wpedantic -Werror)
    endif()
endfunction()

CMakeLists.txt에서 사용

include(cmake/CompilerWarnings.cmake)

add_executable(MyApp main.cpp)
enable_strict_compiler_warnings(MyApp)

결론

이번 글에서는 모던 CMake의 고급 기능인 생성기 표현식커스텀 명령어를 활용하여 빌드 시스템을 더욱 유연하게 만드는 방법을 살펴보았습니다. 또한, 대규모 프로젝트에서 모던 CMake를 효과적으로 사용하는 방법에 대해 알아보았습니다.

반응형