
보통 패키지라고 하면 RPM, Brew 처럼 관리 소프트웨어를 통해 다운로드/설치/업데이트해서 사용하는 프로그램들을 말하는데, C++ 개발자들에게 패키지란 개발에 필요한 Library + Manifest에 가까운 것 같다.
일반적인 패키지프로그래밍 패키지 <일반 패키지 및 개발에 필요한 요소들>C++에서는 미리 빌드된 서브 프로그램 뿐만 아니라 소스 코드가 포함된다는 점(include)이 특이하다고 볼 수 있다.
비단 템플릿 프로그래밍의 비중이 늘어난 것 뿐만 아니라 크로스 컴파일과 링킹에 손이 많이 가기 때문이기도 할 것이다.
지금은 많은 C++ 프로젝트들이 UNIX FileSystem에서 표준 C 라이브러리를 배치할 때 사용하던 파일트리 구조를 적용하고 있다.
굳이 이런 배치에 어떤 의미가 부여되어있다기 보다는, CMake의 초창기부터 UNIX 시스템에 빌드 된 라이브러리를 설치하면서 관례를 따르던 것이 현재까지 이어지고 있다 정도로 생각하면 될 것 같다.
binlibincludesharedocs이미 설치된 패키지를 찾는 기능으로 CMake는 find_package를 제공하고 있다.
✔️ 참고
사용하는 라이브러리가
CMake를 지원하는데find_package()가 매끄럽게 사용되지 않는다?이럴 때는
add_subdirectory()를 사용하는 것이 정확한 해결책이 될 수 있다.
Package Export에 문제가 있는 경우, Import하는 쪽에서 수정하기가 어렵기 때문이다.
find_package() 함수는 일반적으로 아래와 같이 이름과 버전을 인자로 사용한다. 탐색에 성공하면 name_FOUND 변수가 생성된다. 아래의 예처럼 이름으로 OpenCV를 사용했다면, 성공여부는 OpenCV_FOUND로 확인할 수 있다.
find_package(OpenCV 3.3)
if(OpenCV_FOUND)
// ...
// target_source: Add OpenCV related source codes...
// target_compile_option: Enable RTTI for OpenCV...
// ...
endif()
find_package(OpenCV 3.3 REQUIRED)
조금 더 상세하게 패키지 탐색을 위한 정보를 제공하는 경우, CONFig를 사용해 Config Mode로 호출하게 된다.
PATHS를 수정하여도 제대로 찾지 못한다면, CMake Cache의 문제일 가능성이 높다. 그런 경우 CMakeCache.txt를 제거하고 다시 CMake를 실행해보자.
find_package(fmt 5.3
CONFIG
REQUIRED
PATHS /home/user/vcpkg/installed/x64-linux
C:/vcpkg/installed/x64-windows
)
수 많은 컴포넌트를 가진 Boost에서 필요한 모듈만 가져다 쓴다면, 아래처럼 작성하면 될 것이다. 분명히 설치 되었음에도 CMake에서 찾지 못한다면 CONFIG를 지우고 다시 시도해보면 찾을 수도 있
다.
find_package(Boost 1.59
// 만약 cmake가 실패하면, CONFIG 제거 후 다시 시도
CONFIG
REQUIRED
COMPONENTS system thread timer
)
CMake에서 find_package()를 호출하면, 해당 함수는 Package를 찾고, 그 안에 있는 Target들을 가져온다(add_library(IMPORTED)).
물론 executable과 링킹을 하지는 않기 때문에, 가져온 Target들을 add_library(INTERFACE) 혹은 add_library(SHARED)로 만들어진 결과물들이다. 따라서 이들을 소비하는 함수는 target_link_libraries()이다.
find_package(gRPC CONFIG REQUIRED)
// ...
target_link_libraries(main
PRIVATE
gRPC::gpr gRPC::grpc gRPC::grpc++ gRPC::grpc_cronet
)
여기에는 하나의 전제가 있다. 해당 라이브러리가 CMake에서 Import할 수 있도록 적절하게 config 파일을 작성해 놓았거나, CMake의 export 함수를 사용해 CMake를 위한 config 파일을 생성해 놓은 것이다.
어떤 파일을 제공해야 하는지 알아보기 위해 아래와 같이 CMakeLists.txt를 작성해 실행해보자.
cmake_minimum_required(VERSION 3.30)
find_package(TBB REQUIRED)
Intel TBB가 설치되지 않은 환경에서 find_package()가 실패하면서 아래와 같이 오류를 출력할 것이다.

이를 통해 확인할 수 있는 것은 find_package()에서 TBB라는 이름으로 대소문자가 혼합된 경우(TBBConfig.cmake)와 소문자만 사용된 경우(tbb-config.cmake)를 고려하여 config 파일을 찾으려 했다는 것을 알 수 있다.
TBB를 설치하면, TBBConfig.cmake가 생성된 것을 확인할 수 있다. 다행히 TBB의 xxx-config.cmake 파일은 비교적 짧은 편에 속한다.
/*
# Copyright (c) 2017-2019 Intel Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# TBB_FOUND should not be set explicitly. It is defined automatically by CMake.
# Handling of TBB_VERSION is in TBBConfigVersion.cmake.
*/
if (NOT TBB_FIND_COMPONENTS)
set(TBB_FIND_COMPONENTS "tbb;tbbmalloc;tbbmalloc_proxy")
foreach (_tbb_component ${TBB_FIND_COMPONENTS})
set(TBB_FIND_REQUIRED_${_tbb_component} 1)
endforeach()
endif()
// Add components with internal dependencies: tbbmalloc_proxy -> tbbmalloc
list(FIND TBB_FIND_COMPONENTS tbbmalloc_proxy _tbbmalloc_proxy_ix)
if (NOT _tbbmalloc_proxy_ix EQUAL -1)
list(FIND TBB_FIND_COMPONENTS tbbmalloc _tbbmalloc_ix)
if (_tbbmalloc_ix EQUAL -1)
list(APPEND TBB_FIND_COMPONENTS tbbmalloc)
set(TBB_FIND_REQUIRED_tbbmalloc ${TBB_FIND_REQUIRED_tbbmalloc_proxy})
endif()
endif()
set(TBB_INTERFACE_VERSION 11007)
get_filename_component(_tbb_root "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_tbb_root "${_tbb_root}" PATH)
get_filename_component(_tbb_root "${_tbb_root}" PATH)
foreach (_tbb_component ${TBB_FIND_COMPONENTS})
set(_tbb_release_lib "${_tbb_root}/lib/${_tbb_component}.lib")
set(_tbb_debug_lib "${_tbb_root}/debug/lib/${_tbb_component}_debug.lib")
if (EXISTS "${_tbb_release_lib}" OR EXISTS "${_tbb_debug_lib}")
add_library(TBB::${_tbb_component} UNKNOWN IMPORTED)
set_target_properties(TBB::${_tbb_component} PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_tbb_root}/include")
if (EXISTS "${_tbb_release_lib}")
set_target_properties(TBB::${_tbb_component} PROPERTIES
IMPORTED_LOCATION_RELEASE "${_tbb_release_lib}")
set_property(TARGET TBB::${_tbb_component} APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
endif()
if (EXISTS "${_tbb_debug_lib}")
set_target_properties(TBB::${_tbb_component} PROPERTIES
IMPORTED_LOCATION_DEBUG "${_tbb_debug_lib}")
set_property(TARGET TBB::${_tbb_component} APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
endif()
// Add internal dependencies for imported targets: TBB::tbbmalloc_proxy -> TBB::tbbmalloc
if (_tbb_component STREQUAL tbbmalloc_proxy)
set_target_properties(TBB::tbbmalloc_proxy PROPERTIES INTERFACE_LINK_LIBRARIES TBB::tbbmalloc)
endif()
list(APPEND TBB_IMPORTED_TARGETS TBB::${_tbb_component})
set(TBB_${_tbb_component}_FOUND 1)
elseif (TBB_FIND_REQUIRED AND TBB_FIND_REQUIRED_${_tbb_component})
message(STATUS "Missed required Intel TBB component: ${_tbb_component}")
set(TBB_FOUND FALSE)
set(TBB_${_tbb_component}_FOUND 0)
endif()
endforeach()
unset(_tbbmalloc_proxy_ix)
unset(_tbbmalloc_ix)
unset(_tbb_lib_path)
unset(_tbb_release_lib)
unset(_tbb_debug_lib)
크게 3가지 정도 눈여결 볼 수 있다.
add_library(IMPORTED)를 사용해서 CMake Target을 생성한다. 이름으로는 TBB::${_tbb_component}를 사용해서 이것이 CMake Target이라는 점을 분명히 드러내고 있다.set_property() 함수를 사용해서 DEBUG/RELEASE 설정으로 빌드되었다는 정보를 추가하는 것을 볼 수 있다.set_target_properties() 함수에서 IMPORTED_LOCATION을 사용해 .lib 파일의 위치를 지정하거나, INTERFACE_LINK_LIBRARIES를 사용해 TBB::tbbmalloc_proxy에서 TBB::tbbmalloc을 링킹하도록(의존하도록) 만들고 있다.요약하면, find_package()가 하는 일은 target_link_libraries()에서 적합한 정보(property)를 받아서 실제 Build System에서 필요로 하는 Linking 정보를 생성할 수 있도록 하는 Target Builder라고 할 수 있다.
CMake에서는 굉장히 많은 Property를 정의하고 있다. 특히 이들을 사용하기 어렵게 만드는 것은, Target의 타입에 따라 사용할 수 있는 property가 달라진다는 것이다.
define_property,set_property,get_property를 사용하는 경우는,xxx-config.cmake를 제외하고 많이 사용되고 있지는 않는 듯 하다.
cmake_minimum_required(VERSION 3.20)
add_library(xyz UNKNOWN IMPORTED)
set_property(TARGET xyz APPEND PROPERTY
IMPORTED_CONFIGURATIONS RELEASE
)
get_property(xyz_import_config TARGET xyz PROPERTY
IMPORTED_CONFIGURATIONS
)
message(STATUS ${xyz_import_config})
3.x 버전의 CMake에서 export된 xxx-config.cmake 파일들을 대부분 아래와 같은 Property들을 설정한다.
INTERFACE_INCLUDE_DIRECTORIES/usr/local/include;/usr/include 형태로 ;을 사용해서 여러 디렉토리를 지정할 수 있다.INTERFACE_LINK_LIBRARIESTarget의 의존성을 보여주는 부분이다.target_link_libraries()에서 필요로 하는 인자, 즉 다른 CMake Target들의 이름을 ;로 구분되는 목록을 사용해서 지정한다.IMPORTED_LOCATION절대 경로로 지정한다.상대 경로로 해결할 수 있으나, 여기서는 절대 경로만을 허용하는 이유가 있다. 그것은 find_package()하는 대상이 이미 설치되었기 때문일 것이다.IMPORTED_IMPLIBWindows의 경우, 링킹을 위해 .lib파일이 필요하다. (다른 플랫폼에서는 잘 사용되지는 않는 듯)실제 사용하는 모습은 다음과 같다.
add_library(xyz UNKNOWN IMPORTED)
set_target_properties(xyz
PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${INTERFACE_DIR}
INTERFACE_LINK_LIBRARIES "OpenMP::OpenMP_CXX"
)
set_target_properties(xyz
PROPERTIES
IMPORTED_LOCATION ${LIBS_DIR}/iphone/libxyz.a
)
set_target_properties(xyz
PROPERTIES
IMPORTED_IMPLIB ${LIBS_DIR}/windows/xyz.lib
IMPORTED_LOCATION ${LIBS_DIR}/windows/xyz.dll
)
덧붙여, Build Target을 작성할 때 개발자는 언제나 CXX_STANDARD를 명시한다. 이는 target_compile_options() 함수로 /std:c++latest 혹은 gnu++2a를 추가하지 않아도 자동으로 추가하도록 해준다. 이 Property의 최대 값은 CMake 버전의 따라 결정된다.
cmake_minimum_required(VERSION 3.14)
add_library(my_modern_cpp_lib
src/libmain.cpp
)
set_target_properties(my_modern_cpp_lib
PROPERTIES
CXX_STANDARD 20
)
🔆 참고
CMake 3.14부터C++20을 명시할 수 있다.
set_target_properties(foo PROPERTIES CXX_STANDARD 20)
절대 경로를 지정해야 하는 경우, /usr/local과 같이 잘 알려진 경로면 좋겠지만 그렇지 못한 경우 해당 xxx-config.cmake를 기준으로 탐색을 해야 할 수도 있다. 여기에는 보통 CMAKE_CURRENT_LIST_FILE 변수가 사용된다.
이 변수는 include 되는 .cmake 파일의 위치를 저장하고 있다. 물론 CMakeLists.txt도 예외가 아니다.
아래와 같이 파일이 배치되었다고 가정해보자.
$ tree $(pwd)
/path/to
CMakeLists.txt
cmake/
print-current-path.cmake
print-parent-path.cmake
1 directory, 3 files
각각의 내용이 아래와 같다면:
// cmake/print-current-path.cmake
message(STATUS "cmake : ${CMAKE_CURRENT_LIST_FILE}")
// CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
include(cmake/print-current-path.cmake)
message(STATUS "cmakelist: ${CMAKE_CURRENT_LIST_FILE}")
이런 결과가 출력될 것이다.
...
-- cmake : /path/to/cmake/print-filepath.cmake
-- cmakelist: /path/to/CMakeLists.txt
...
-- Configuring done
-- Generating done
보통 특정 경로 하나만으로는 문제를 해결할 수 없기 때문에 여기서는 경로를 다루는 방법 중 두 가지를 짚고 넘어가자.
기본적으로 CMake에서 파일의 경로를 생성할 때는 get_filename_component()를 사용합니다. 앞서 TBBConfig.cmake에서도 이 함수가 사용되었는데, 코드를 보면 의도를 파악하기가 어렵다.
get_filename_component(_tbb_root "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_tbb_root "${_tbb_root}" PATH)
get_filename_component(_tbb_root "${_tbb_root}" PATH)
CMake 문서에 따르면, 가장 마지막에 사용된 인자 PATH는 2.8 버전들의 하위 호환을 위한 것으로, 그 의미는 DIRECTORY를 사용하는 것과 동일하다.
DIRECTORY = Directory without file name
PATH = Legacy alias for DIRECTORY (use for CMake <= 2.8.11)
따라서 현재 기술하고 있는 3.8 이후 버전을 기준으로 작성한다면 아래와 같을 것이다.
get_filename_component(_tbb_root "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
get_filename_component(_tbb_root "${_tbb_root}" DIRECTORY)
get_filename_component(_tbb_root "${_tbb_root}" DIRECTORY)
이미 설계된 TBB 빌드 결과물의 배치를 고려해서 부모 디렉토리를 여러번 타고 올라가는 코드라는 것을 쉽게 알 수 있다. 이를 CMAKE_CURRENT_LIST_FILE에 적용해보면 어떻게 될까?
// cmake/print-parent-path.cmake
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_LIST_FILE} DIRECTORY)
message(STATUS "parent : ${PARENT_DIR}")
조금 전에 CMAKE_CURRENT_LIST_FILE에서 사용한 CMakeLists.txt를 아래처럼 수정해 실행하면,
// CMakeLists.txt
cmake_minimum_required(VERSION 3.20)
include(cmake/print-current-path.cmake)
include(cmake/print-parent-path.cmake) // <-- new
message(STATUS "cmakelist: ${CMAKE_CURRENT_LIST_FILE}")
출력 결과는 아래와 같을 것이다.
$ cmake .
-- cmake : /path/to/cmake/print-current-path.cmake
-- parent : /path/to/cmake
-- cmakelist: /path/to/CMakeLists.txt
...
-- Configuring done
-- Generaing done
경로를 처리할 때 집합(concat)을 수행하는 코드를 흔히 볼 수 있다. 이런 코드들은 절대 경로(Absolute Path)와 상대 경로(Relative Path)가 고르게 사용되는 반면, CMake에서 파일 경로는 특별한 처리가 필요하지 않는 한 절대 경로를 사용한다.
이미 존재하는 디렉토리 경로에 새로운 이름을 붙이는 것은 보통의 문자열 생성 방법과 같다. Windows에서는 Command Prompt를 실행하는 경우라면 \\를 구분자로 사용해야 하지만, 단순히 CMake 내에서 경로만 처리한다면 /를 사용해도 별다른 문제가 없다.
// Ok for windows and the others
get_filename_component(CURRENT_MODULE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/cmake ABSOLUTE)
message(STATUS "modules : ${CURRENT_MODULE_DIR}")
여기서 get_filename_component()의 역할은 CURRENT_MODULE_DIR 변수의 타입을 파일 경로로 설정하는 것 뿐이다.
Windows, PowerShell 환경에서 이를 실행해보면, CMake에서 구분자로 /를 사용하는 것을 확인할 수 있다.
PS > cmake .
...
-- modules : D:/path/to/cmake
...
WSL Bash에서는 아래와 같다.
$ cmake .
...
-- modules : /mnt/drive/test-proj/path/cmake
다음의 명령을 자세히 풀어쓰면:
get_file_component(CURRENT_MODULE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/cmake ABSOLUTE)
get_filename_component()는 CMake에서 파일 경로의 일부를 추출하거나 조작하는데 사용되는 명령어이다. 제공된 명령어를 설명하면 다음과 같다.
get_filename_componentCURRENT_MODULE_DIR${CMAKE_CURRENT_SOURCE_DIR}/cmake${CMAKE_CURRENT_SOURCE_DIR}CMakeLists.txt 파일이 위치한 디렉토리를 나타내는 변수/cmake를 추가했으므로, 이는 소스 디렉토리 내의 cmake 하위 디렉토리를 가리킨다.ABSOLUTE따라서 위의 명령은 ${CMAKE_CURRENT_SOURCE_DIR}을 기준으로 cmake 디렉토리의 절대 경로를 계산하여 CURRENT_MODULE_DIR 변수에 저장한다.
예를 들어, ${CMAKE_CURRENT_SOURCE_DIR}의 값이 /path/to/project라면, CURRENT_MODULE_DIR에는 다음과 같은 값이 저장된다.
/path/to/project/cmake
set_target_properties가 여러 Property를 한번에 설정할 수 있는데 반해, get_target_property는 한번에 하나의 변수를 생성한다. 사용법 또한 단순하다.
cmake_minimum_required(VERSION 3.20)
add_library(my_modern_cpp_lib
libmain.cpp
)
set_target_properties(my_modern_cpp_lib
PROPERTIES
CXX_STANDARD 17
)
get_target_property(specified_cxx_version
my_modern_cpp_lib CXX_STANDARD
)
// -- cxx_version: 17
message(STATUS "cxx_version: ${specified_cxx_version}")
CMake에서 Export하는 방법은 튜토리얼마다 설명이 조금씩 다른데, 근본적인 차이점은 CMake를 위한 템플릿 파일을 사용하는지에 달려 있다. 어떤 프로젝트에서는 CMake 모듈들이 배치된 디렉토리에 package-targets.cmake.in과 같이 .in으로 끝나는 파일들이 있는 것을 볼 수 있는데, 이런 인라인 파일들은 어디선가 CMake에서 제공하는 configure file 혹은 configure_package_config_file 함수를 사용하기 때문일 가능성이 높다.
이 함수는 CMake 파일 생성 뿐만 아니라 사용자 환경에 맞는 헤더 파일(.h)을 만들거나, 리눅스 플랫폼에서 pkg-config를 위한 파일을 만드는데 사용된다.
CMake 문서의 설명에 따르면, 플랫포마다 탐색 경로가 다르지만 공통되는 경로가 있다는 것을 알 수 있다.
install을 사용할 때, CMAKE_INSTALL_PREFIX를 기준으로 설치경로를 지정하는 것을 권하지만, 아래처럼 경로에 프로젝트 이름이 들어가는 것이 다른 프로젝트와의 충돌의 가능성을 낮춰줄 것이다.
cmake_minimum_required(VERSION 3.20)
project(my_modern_cpp_lib LANGUAGES CXX)
// ...
install(FILES ${VERSION_FILE_PATH}
${LICENSE_FILE_PATH}
DESTINATION ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}
)
버전 정보를 추가하는 것은 이미 CMake에서 제공하는 모듈을 사용하면 쉽게 작성할 수 있다.
include(CMakPackageConfigHelpers)
set(VERSION_FILE_PATH ${CMAKE_BINaRY_DIR}/cmake/${PROJECT_NAME}-config-version.cmake)
write_basic_package_version_file(${VERSION_FILE_PATH}
VERSION ${PROJECT_VERSION} # x.y.z
COMPATIBILITY $ameMajorVersion
)
// ...
install(FILES ${VERSION_FILE_PATH}
DESTINATION ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}
)
file 혹은 directory의 설치는 단순히 복사/갱신으로 끝날 수 있지만, 결정적으로 xxx-config.cmake에는 프로젝트에서 빌드할 Target에 대한 정보가 들어가야 한다. 여기에는 install(TARGETS)와 install(EXPORT)가 함께 사용된다.
간단한 예로, 아래와 같은 구조의 프로젝트를 만들어보자.

우선 Root CMakeLists.txt 파일에서는 EXPORT_NAME 변수를 만들고, add_subdirectory()로 하위 모듈들을 빌드하도록 한다. 최종적으로 install(EXPORT)를 사용해 설치까지 수행한다.
cmake_minimum_required(VERSION 3.20)
project(test-proj LANGUAGES CXX)
set(EXPORT_NAME ${PROJECT_NAME}-config) // or ${PROJECT_NAME}Config
add_subdirectory(src) // <-- use EXPORT_NAME
install(EXPORT ${EXPORT_NAME}
NAMESPACE test::
DESTINATION ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}
)
src의 CMakeLists.txt 파일은 add_library()로 CMake Target을 생성하고, install(TARGETS)에서 EXPORT 인자를 사용해 해당 라이브러리를 일종의 Export Group에 추가한다. 단순히 추가하기만 할 뿐, install(EXPORT)를 사용하기 전까지 실제 설치는 이루어지지 않는다.
특이하게도
EXPORT는 반드시 다른 인자보다 먼저 사용되어야 한다고 명시하고 있다 (must appear before).
// src/CMakeLists.txt
add_library(test-lib SHARED
libmain.cpp
)
set_target_properties(test-lib
PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS OFF
)
target_include_directories(test-lib
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include>
)
install(TARGETS test-lib
EXPORT ${EXPORT_NAME} # <-- new
RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}/bin
LIBRARY DESTINATION ${CMAKE_INSTALL_PREFIX}/lib
ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}/lib
)
마지막으로 libmain.cpp는 간단히 함수 하나를 동적 링킹(Dynamic Linking)이 가능하도록 정의한다.
#ifndef __LIBMAIN_H__
#define __LIBMAIN_H__
// clang-format off
#if defined(_MSC_VER) // MSVC or clang-cl
#define _HIDDEN_
#ifdef _WINDLL
#define _INTERFACE_ __declspec(dllexport)
#else
#define _INTERFACE_ __declspec(dllimport)
#endif
#elif defined(__GNUC__) || defined(__clang__) // GCC or clang
#define _INTERFACE_ __attribute__((visibility("default")))
#define _HIDDEN_ __attribute__((visibility("hidden")))
#else
error "unexpected linking configuration"
#endif
// clang format on
#include <cstdint>
constexpr auto version_code = 0x0102;
_INTERFACE_ uint32_t get_version() noexcept;
uint32_t get_version() noexcept {
return version_code;
}
#endif
CMake를 수행하면 다음과 같이 ``파일이 설치되는 것을 볼 수 있다.
$ cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="<설치 경로>"
$ cmake --build build --config Debug --target install

여기서 설치된 파일의 이름인 test-proj-config는 앞서 EXPORT_NAME 변수의 값을 따른 것이다.
set(EXPORT_NAME ${PROJECT_NAME}-config) # or ${PROJECT_NAME}Config
불필요한 부분을 제외하고 해당 파일의 내용을 살펴보면, test::test-lib 와 같이 Target을 가져오는 내용이라는 것을 알 수 있다. 이런 파일들은 test-proj-targets.cmake로 따로 만들고 xxx-config.cmake는 configure_package_config_file을 사용해서 만드는 방법을 사용하기도 한다.
하지만 이 예시에서는 Import 측에 전달할 정보가 없기에 CMake 템플릿 파일을 작성하지 않았고, 따라서 바로 xxx-config.cmake를 생성해도 무방하다.
// test-proj-config.cmake
// The installation prefix configured by this project.
set(_IMPORT_PREFIX "/Users/workspace/cmake/test/mylib")
// Create imported target test::test-lib
add_library(test::test-lib SHARED IMPORTED)
set_target_properties(test::test-lib PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "/Users/workspace/cmake/test/mylib/include"
)
// Load information for each installed configuration.
file(GLOB _cmake_config_files "${CMAKE_CURRENT_LIST_DIR}/test-proj-config-*.cmake")
foreach(_cmake_config_file IN LISTS _cmake_config_files)
include("${_cmake_config_file}")
endforeach()
특히 패턴 매칭(test-proj-*.cmake)을 사용해 xxx-config-debug.cmake 혹은 xxx-config-release.cmake를 include 할 수 있도록 되어있는 점에 주목하자. 앞서 write_basic_package_version_file에서 Version 파일의 설치 위치를 비롯해 이름을 ${PROJECT_NAME}-config-version.cmake로 만들도록 한 것은 이를 고려한 것이다.
cmake_minimum_required(VERSION 3.20)
project(test-proj LANGUAGES CXX)
set(EXPORT_NAME ${PROJECT_NAME}-config) # or ${PROJECT_NAME}Config
add_subdirectory(src) # <-- use EXPORT_NAME
install(EXPORT ${EXPORT_NAME}
NAMESPACE test::
DESTINATION ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}
)
include(CMakePackageConfigHelpers)
set(VERSION_FILE_PATH ${CMAKE_BINARY_DIR}/cmake/${PROJECT_NAME}-config-version.cmake)
write_basic_package_version_file(${VERSION_FILE_PATH}
VERSION ${PROJECT_VERSION} # x.y.z
COMPATIBILITY SameMajorVersion
)
install(
FILES ${VERSION_FILE_PATH}
DESTINATION ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}
)
위의 예시에서 BUILD_INTERFACE와 INSTALL_INTERFACE의 사용을 한마디로 정리하자면, 빌드할 때 사용하는 include 디렉토리와 설치 후 사용하는 include 디렉토리가 다르다라는 것이다.
target_include_directories(test-lib
PUBLIC
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include>
)
위와 같이 작성하는 것은 CMake에서는 Generator Expression이라고 하는데, 보통 플랫폼에 따라 IF/ELSE/AND/OR가 뒤섞여 가독성을 심하게 해치는 경향이 있다.
라이브러리가 설치된 이후에는 Build에 사용한 디렉토리가 삭제될 가능성이 높기에, Import할 때 소스코드가 배치된 디렉토리를 사용하도록 한다면 파일을 못찾는 문제가 발생할 것이다.
이를 막기 위해 빌드시에는 PROJECT_SOURCE_DIR 기준으로 include를 수행하지만, 설치 이후에는 CMAKE_INSTALL_PREFIX를 기준으로 include를 수행한다.
인터페이스 파일들은 이미 CMAKE_INSTALL_PREFIX/include로 install(FILES) 혹은 install(DIRECTORIES)를 통해서 복사되었을 것이기에 설치가 완료된 시점부터 해당 디렉토리는 사용가능한 경로가 될 것이다.