본문으로 건너뛰기
CMake · 9/9

Modern CMake 고급 — BUILD/INSTALL_INTERFACE·Presets·cmake -E

· Hawk · 6분 읽기

#이 장에서 다루는 것

Ch 1~Ch 8을 모두 마쳤다면 실무에 쓸 만한 Modern CMake 프로젝트를 만들 수 있습니다. 이 장은 그 위에 올라가는 고급 도구 모음입니다 — 매번 등장하진 않지만, 결정적인 자리에서 쓰입니다.

  • target_* 패밀리 전체target_sources / target_link_options / target_precompile_headers / set_target_properties / target_compile_features
  • BUILD_INTERFACE vs INSTALL_INTERFACE — 라이브러리의 두 얼굴
  • add_custom_command vs add_custom_target — 코드 생성·후처리 도구
  • CMakePresets.json — 빌드 설정의 표준 외부화 (3.19+)
  • cmake -E — 크로스 플랫폼 셸 유틸리티

이 장은 순차적 학습이 아니라 참조용에 가깝습니다. 필요한 자리만 찾아 읽으면 됩니다.


#커스텀 타겟 — add_custom_command vs add_custom_target

add_executable·add_library컴파일러 기반의 타겟을 만듭니다. 하지만 빌드 중에 컴파일러가 아닌 도구를 호출해야 할 때가 있습니다. 코드 생성기(protoc, yacc, swig), 셰이더 컴파일러, 문서 생성기(doxygen), 파일 변환기 등.

CMake는 이를 위해 두 가지 명령을 제공합니다 — add_custom_commandadd_custom_target. 둘은 비슷해 보이지만 동작이 완전히 다릅니다.

#add_custom_command파일을 만드는 명령

add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/messages.pb.cc
${CMAKE_CURRENT_BINARY_DIR}/messages.pb.h
COMMAND protoc --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
--proto_path=${CMAKE_CURRENT_SOURCE_DIR}/proto
${CMAKE_CURRENT_SOURCE_DIR}/proto/messages.proto
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/proto/messages.proto
COMMENT "Generating messages.pb.{h,cc} from messages.proto"
)

핵심은 OUTPUT입니다. 이 명령이 생성하는 파일을 미리 알려 줍니다. CMake는 이 파일을 그 자체로는 빌드하지 않습니다. 다른 타겟이 이 OUTPUT 파일을 의존성으로 들고 있을 때만 명령이 실행됩니다.

add_executable(myapp
src/main.cpp
${CMAKE_CURRENT_BINARY_DIR}/messages.pb.cc # ← 이걸 소스로 추가
)

myappmessages.pb.cc를 소스로 가지므로, CMake는 이 파일을 누가 만드는지 찾아 add_custom_commandOUTPUT을 발견하고 그 명령을 실행 단계에 추가합니다. messages.proto가 수정되면 DEPENDS 추적으로 재생성도 자동입니다.

요약: add_custom_command(OUTPUT)Make의 일반 규칙과 같은 모델입니다. “이 파일은 이 명령으로 만들어진다”를 선언하고, 누군가 그 파일을 필요로 할 때만 실행됩니다.

#add_custom_target실행 동작을 묶는 명령

add_custom_target(docs
COMMAND doxygen Doxyfile
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMENT "Generating API documentation"
)

이건 파일을 만들지 않습니다. 동작 그 자체에 이름을 붙이는 명령입니다. Make의 .PHONY 타겟과 같은 개념.

특징:

  • 항상 out-of-date 상태로 취급되어 호출할 때마다 실행됩니다.
  • 다른 타겟의 디폴트 빌드에는 포함되지 않습니다(아래에서 다룸).
  • 사용자가 cmake --build build --target docs명시적으로 호출해야 돕니다.

#두 명령의 조합 — 가장 흔한 패턴

add_custom_command(OUTPUT ...)은 파일을 만들고, add_custom_target은 그 파일을 디폴트 빌드에 포함시킵니다.

add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/version.h
COMMAND ${CMAKE_COMMAND} -P generate_version.cmake
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/VERSION
)
# 디폴트 빌드에 포함되도록 phony 타겟으로 묶기
add_custom_target(generate_version ALL
DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/version.h
)

ALL 키워드가 핵심입니다. add_custom_target(... ALL ...)디폴트 빌드 시 자동 호출되어, 의존성으로 묶인 add_custom_command의 OUTPUT을 강제로 만들게 합니다.

또는, 생성된 파일을 직접 다른 타겟의 소스로 넣는 게 더 깔끔할 때도 있습니다.

add_executable(myapp
src/main.cpp
${CMAKE_CURRENT_BINARY_DIR}/version.h # 의존성 자동 연결
)

이 방식은 별도 add_custom_target이 필요 없고, myapp을 빌드할 때 자동으로 version.h가 생성됩니다.

#선택 기준

상황선택
컴파일러가 소비할 파일을 만든다add_custom_command(OUTPUT ...)
빌드 후 정해진 동작(테스트, 문서, 배포)add_custom_target
위 동작을 디폴트 빌드에 포함add_custom_target(... ALL ...)
다른 타겟 빌드 전/후에 끼워 넣고 싶다add_custom_command(TARGET ... POST_BUILD/PRE_BUILD ...)

마지막 TARGET 형태도 짧게 봅시다.

add_custom_command(TARGET myapp POST_BUILD
COMMAND ${CMAKE_STRIP} $<TARGET_FILE:myapp>
COMMENT "Stripping debug symbols"
)

myapp 빌드가 끝난 직후에 자동으로 strip을 실행합니다. CI에서 산물 후처리(코드 사이닝, 압축, 카피)를 자동화할 때 자주 등장합니다.


#모던 타겟 명령 — target_* 패밀리 전체

Ch 3에서 본 target_link_libraries·target_include_directories·target_compile_options·target_compile_definitions·target_compile_features 다섯이 가장 자주 등장합니다. 그 외에도 Modern CMake의 타겟 명령 패밀리에는 몇 가지가 더 있습니다.

#target_sources() — 타겟에 소스 추가

add_library(mylib)
target_sources(mylib PRIVATE
src/main.cpp
src/utils.cpp
)

add_library()add_executable() 시점에 모든 소스를 다 알 필요 없이, 나중에 추가할 수 있습니다. 큰 프로젝트에서 모듈별 add_subdirectory공통 라이브러리에 자기 소스를 더하는 패턴이 자주 등장합니다.

libs/math/CMakeLists.txt
# 최상위
add_library(mylib)
target_sources(mylib PRIVATE math/sin.cpp math/cos.cpp)
# libs/string/CMakeLists.txt
target_sources(mylib PRIVATE string/format.cpp)

CMake 3.23+부터 공개 헤더target_sources로 등록할 수 있습니다.

target_sources(mylib
PUBLIC FILE_SET HEADERS
BASE_DIRS include
FILES include/mylib/api.hpp
)

이 모델의 장점은 설치 시 자동으로 BASE_DIRS 구조가 보존됩니다.

target_link_libraries(myapp PRIVATE pthread) # 라이브러리 (-lpthread)
target_link_options(myapp PRIVATE -Wl,--as-needed) # 링커 플래그

target_link_libraries링크할 라이브러리, target_link_options링커에게 줄 옵션. 자주 헷갈리는 자리입니다. -flto, -static, -fuse-ld=lld, -Wl,-rpath,... 같은 옵션은 *target_link_options*에 속합니다.

#target_precompile_headers() — PCH (3.16+)

target_precompile_headers(myapp PRIVATE
<vector>
<string>
<unordered_map>
"common.hpp"
)

PCH(Precompiled Headers)로 표준 라이브러리·자주 쓰는 헤더의 컴파일을 한 번만 합니다. 큰 프로젝트에서 빌드 시간을 수십 퍼센트 줄일 수 있습니다. PUBLIC으로 설정하면 의존 타겟도 같은 PCH를 공유합니다.

함정: 모든 .cpp가 같은 PCH를 강제 include한다는 점. ABI나 매크로가 다른 라이브러리를 섞기 어려워질 수 있어, 정말 프로젝트에만 권장됩니다.

#set_target_properties() — 일반화된 속성 설정

set_target_properties(mylib PROPERTIES
VERSION 1.0.0
SOVERSION 1
POSITION_INDEPENDENT_CODE ON
INTERPROCEDURAL_OPTIMIZATION TRUE
)

target_* 명령 카테고리에 맞는 게 없는 속성을 설정합니다. 대표적인 자리:

  • VERSION / SOVERSION — 동적 라이브러리 ABI 버전.
  • POSITION_INDEPENDENT_CODE ON-fPIC 강제 (정적 라이브러리에도).
  • INTERPROCEDURAL_OPTIMIZATION TRUE — LTO 켜기.
  • EXPORT_NAME — install(EXPORT)로 내보낼 때 사용할 별칭.

target_* 명령으로 못하는 게 있으면 set_target_properties가 거의 답입니다.

#target_compile_features vs set(CMAKE_CXX_STANDARD)

# 권장
target_compile_features(myapp PUBLIC cxx_std_17)
# 옛 방식 — 가능하면 피하세요
set(CMAKE_CXX_STANDARD 17)

차이는 전이성입니다. target_compile_features(... PUBLIC ...)이 타겟을 사용하는 다른 타겟에도 같은 표준을 요구합니다. CMAKE_CXX_STANDARD그 자리에서만 동작하고 전이되지 않습니다. 라이브러리를 만들 때는 거의 항상 target_compile_features가 답입니다.


#BUILD_INTERFACE vs INSTALL_INTERFACE — 라이브러리의 두 얼굴

Ch 7에서 잠깐 본 이 두 제너레이터 식은 라이브러리가 개발 중인지 설치된 후인지에 따라 다른 경로를 사용하게 합니다.

target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)

같은 라이브러리, 두 상황.

  • 빌드 중mylib바로 사용하는 자매 타겟(같은 빌드의 myapp 등). BUILD_INTERFACE가 활성화되어 /path/to/source/include를 인클루드 경로로 받음.
  • 설치 후mylib설치되고 find_package(MyLib)로 가져온 외부 프로젝트. INSTALL_INTERFACE가 활성화되어 /usr/local/include를 받음.

두 자리에서 헤더 위치가 다르므로 이 분기가 필요합니다. 모든 라이브러리 프로젝트가 이 패턴을 씁니다. 빼면 설치된 라이브러리를 다른 프로젝트에서 못 씁니다.

#헤더 설치와 함께 가는 표준 패턴

add_library(mylib src/mylib.cpp)
target_include_directories(mylib PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
install(TARGETS mylib EXPORT MyLibTargets
INCLUDES DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
)
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})

$<INSTALL_INTERFACE>INCLUDES DESTINATION과 같은 경로를 가리키게 두면, 설치된 라이브러리를 가져오는 쪽에서 자동으로 올바른 헤더 경로가 잡힙니다.


#CMakePresets.json — 빌드 설정의 표준 외부화 (3.19+)

-DCMAKE_BUILD_TYPE=Release -DENABLE_TESTS=OFF -G Ninja -DCMAKE_INSTALL_PREFIX=/opt/myapp 같은 긴 명령어를 매번 치는 대신, 프리셋 파일에 모아 둡니다.

{
"version": 6,
"configurePresets": [
{
"name": "default",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build/${presetName}",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Release",
"CMAKE_INSTALL_PREFIX": "/opt/myapp"
}
},
{
"name": "debug",
"inherits": "default",
"cacheVariables": {
"CMAKE_BUILD_TYPE": "Debug",
"ENABLE_SANITIZERS": "ON"
}
}
],
"buildPresets": [
{ "name": "default", "configurePreset": "default" },
{ "name": "debug", "configurePreset": "debug" }
],
"testPresets": [
{ "name": "default", "configurePreset": "default", "output": { "verbosity": "verbose" } }
]
}
Terminal window
cmake --preset default
cmake --build --preset default
ctest --preset default

장점들:

  • VCS로 공유 가능. 팀 전체가 같은 설정을 쓰게 됨.
  • IDE 1급 지원. CLion·VS Code·Visual Studio 모두 프리셋을 자동 인식.
  • 상속. inherits로 공통 설정을 빼고 차이만 정의.
  • CI 일관성. CI에서도 같은 프리셋을 호출.

CMakeUserPresets.json은 개인용 (gitignore 대상)이고, CMakePresets.json은 공유용입니다. 두 파일이 함께 있으면 user가 덮어씁니다.


#cmake -E — 크로스 플랫폼 셸 유틸리티

CMake는 내부에 셸 명령 대체를 가지고 있습니다. cmake -E <command> 형태로 호출합니다.

# 디렉터리 복사
add_custom_command(TARGET myapp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_SOURCE_DIR}/assets
$<TARGET_FILE_DIR:myapp>/assets
)
# 파일 복사
add_custom_command(...
COMMAND ${CMAKE_COMMAND} -E copy file1.txt file2.txt
)
# 환경 변수와 함께 실행
add_custom_command(...
COMMAND ${CMAKE_COMMAND} -E env LD_LIBRARY_PATH=/opt/lib ./myapp
)

자주 쓰는 서브명령:

명령의미
copy / copy_if_different / copy_directory파일 복사
make_directorymkdir -p
remove / remove_directory삭제
rename이름 변경
chdir <dir> <cmd>디렉터리 이동 후 실행
env [vars] <cmd>환경 변수와 함께 실행
compare_files두 파일 비교
create_symlink심볼릭 링크
tartar 압축/해제
time명령 실행 시간 측정

플랫폼에 무관하므로 Windows에서도 동작합니다. 셸 명령(cp, mv, rm)을 직접 호출하면 윈도우에서 깨지지만, ${CMAKE_COMMAND} -E는 어디서나 동작합니다. 크로스 플랫폼 빌드 스크립트에서 거의 필수입니다.


#정리

이 장에서 본 다섯 가지 영역은 기본 Modern CMake 학습 이후에 자연스럽게 필요해지는 도구들입니다.

  • target_* 패밀리: 5개 핵심 외에 target_sources / target_link_options / target_precompile_headers / set_target_properties까지 알면 거의 모든 타겟 설정이 가능.
  • add_custom_command vs add_custom_target: 파일 생성 vs 동작 호출. 둘의 조합과 TARGET POST_BUILD 패턴.
  • BUILD_INTERFACE vs INSTALL_INTERFACE: 라이브러리가 빌드 중설치 후 두 다른 경로를 가질 때 필수.
  • CMakePresets.json (3.19+): 빌드 설정의 외부화. 팀·CI·IDE에서 같은 설정을 공유.
  • cmake -E: 크로스 플랫폼 셸 유틸리티. Windows·Linux·macOS에서 같은 빌드 스크립트.

#시리즈 마무리

CMake 시리즈는 여기서 마칩니다. Ch 1의 세 줄짜리 CMakeLists.txt에서 출발해 Modern CMake의 거의 모든 핵심 도구를 거쳤습니다.

다시 한 번 핵심을 한 줄로 요약합니다.

Modern CMake는 타겟에 모든 것을 붙이는 것에서 시작해, 그 타겟을 잘 export하는 것에서 끝난다.

이 두 축이 잡히면 — target_*로 타겟에 옵션·include·라이브러리·표준을 붙이고, install(TARGETS ... EXPORT ...)로 그 타겟을 다른 프로젝트가 쓸 수 있게 내보내는 것 — 그 사이의 모든 것이 자연스럽게 따라옵니다.

다음 단계로 권장:

  • 큰 오픈소스 프로젝트의 CMakeLists.txt 읽기. LLVM, Qt, KDE, Krita는 좋은 학습 대상.
  • FetchContent로 의존성 묶어 보기. 한 번 해 보면 시스템 의존성 관리가 한결 단순해짐.
  • CMakePresets.json을 팀에 도입. 처음 한 번 설정해 두면 그 뒤로 모두가 같은 빌드를 함.

#참고 자료

#관련 시리즈

  • GNU Make — CMake가 생성하는 빌드 파일의 기본 모델