# CMake 튜토리얼 Lv.3
- [Package](#package)
- [Package의 구성](#package의-구성)
- [CMake의 Package 찾기](#cmake의-package-찾기)
- [Property](#property)
- 관련 CMake 함수들
- [CMake Export](#cmake-export)
- [CMake Manifest 파일의 배치](#cmake-manifest-파일의-배치)
- [CMake Manifest 만들기](#cmake-manifest-만들기)
## Package
### Package의 구성
보통 패키지라고 하면 [Chocolaty](https://chocolatey.org/), [NuGet](https://docs.microsoft.com/ko-kr/nuget/what-is-nuget), [RPM](https://ko.wikipedia.org/wiki/RPM_%ED%8C%A8%ED%82%A4%EC%A7%80_%EB%A7%A4%EB%8B%88%EC%A0%80), [Brew](https://brew.sh/index_ko)처럼 관리 소프트웨어를 통해 다운로드/설치/업데이트해서 사용하는 프로그램들(+ 문서)을 말하는데, C++ 프로그래머들에게 패키지란 개발에 필요한 Library + Manifest에 가까운 것 같습니다.
* 일반적인 패키지:
* 실행 프로그램(executable)
* 문서 파일(license, manual, readme 등)
* 프로그래밍 패키지: 일반 패키지 + 개발에 필요한 요소들
* 서브 프로그램(library)
* 실행 프로그램(test tools, script 등)
* 소스 코드(include, example 등)
C++ 에서는 미리 빌드된 서브 프로그램 뿐만 아니라 소스 코드가 포함된다는 점(include)이 특이하다고 할 수 있습니다. 비단 템플릿 프로그래밍의 비중이 늘어난 것 뿐만 아니라 크로스 컴파일과 링킹에 손이 많이 가기 때문이기도 할 것입니다.
지금은 많은 C++ 프로젝트들이 [Unix Filesystem](https://en.wikipedia.org/wiki/Unix_filesystem)에서 표준 C 라이브러리를 배치할때 사용하던 파일트리 구조를 적용하고 있습니다.
굳이 이런 배치에 어떤 의미가 부여되어있다기 보다는, "CMake의 초창기부터 Unix 시스템에 빌드 된 라이브러리을 설치하면서 관례를 따르던 것이 이어지고 있다"정도로 생각하면 될 것 같습니다.
* bin : 실행 프로그램(executable)
* lib : 미리 빌드된 라이브러리(so, lib 등)
* include : 소스 코드(헤더)
* share : 기타 필요한 파일들. 주로 빌드 지원 파일
* docs : 문서가 (많이) 있는 경우 따로 두기도
### CMake의 Package 찾기
#### [`find_package`](https://cmake.org/cmake/help/latest/command/find_package.html)
이미 설치된 패키지를 찾는 기능으로 CMake는 `find_package`를 제공하고 있습니다.
CMake의 패키지를 어떻게 만드는지에 앞서서, 어떻게 사용하는지부터 짚고 넘어가겠습니다.
잠시 미리 적자면, 여러분이 사용하는 라이브러리가 CMake를 지원하는 경우, `find_package`가 매끄럽게 사용되지 않을 때는 `add_subdirectory`를 사용하는 것이 '정확한' 해결책이 될 수 있습니다. Package export에 문제가 있는 경우 이를 찾아내기에도, **Import하는 쪽에서 수정하기에도 어렵기 때문**입니다.
이 함수는 일반적으로는 아래와 같이 이름과 버전을 인자로 사용합니다. 탐색에 성공하면 `name_FOUND` 변수가 생성됩니다.
아래 예시처럼 이름으로 `OpenCV`를 사용했다면, 성공여부는 `OpenCV_FOUND`로 확인할 수 있습니다.
```cmake
# optional import
find_package(OpenCV 3.3)
if(OpenCV_FOUND)
# ...
# target_source: Add OpenCV related source codes ...
# target_compile_options: Enable RTTI for OpenCV ...
# ...
endif()
# mandatory import
find_package(OpenCV 3.3 REQUIRED)
```
좀더 상세하게 패키지 탐색을 위한 정보를 제공하는 경우, [`CONFIG`를 사용해 Config Mode로 호출하게 됩니다](https://cmake.org/cmake/help/latest/command/find_package.html#full-signature-and-config-mode).
PATHS를 수정하여도 제대로 찾지 못한다면, CMake Cache의 문제일 가능성이 높습니다.
그런경우 CMakeCache.txt 를 제거하고 다시 CMake를 실행시켜보시기 바랍니다
```cmake
# cmake might find multiple packages.
# In the case it will peek the first one
find_package(fmt 5.3
CONFIG
REQUIRED
PATHS C:/vcpkg/installed/x64-windows
/mnt/vcpkg/installed/x64-linux
)
```
수많은 컴포넌트를 가진 Boost에서 필요한 모듈만 가져다 쓴다면 아래처럼 작성하면 될 것입니다.
분명히 설치 되었음에도 CMake에서 찾지 못한다면 CONFIG를 지우고 다시 시도해보시면 찾을수도 있습니다.
>
> 작성자도 아직 `CONFIG`의 유무가 탐색에 미치는 영향을 명확하게 알아내지는 못했습니다.
> 대부분의 패키지들은 CONFIG를 함께 쓰면 별 문제없이 탐색에 성공하는 것을 확인했습니다.
> 아마도 CMake로 export 했는지 여부가 영향을 미치는 것 같다고 짐작할 뿐입니다.
>
```cmake
find_package(Boost 1.59
CONFIG # <--- try without CONFIG if the function fails !
REQUIRED
COMPONENTS system thread timer
)
```
CMake에서 `find_package`를 호출하면, 해당 함수는 Package를 찾고, 그 안에 있는 Target들을 가져옵니다(`add_library(IMPORTED)`).
물론 executable과 링킹을 하지는 않기 때문에, 가져온 Target들을 `add_library(INTERFACE)`혹은 `add_library(SHARED)`로 만들어진 결과물들입니다.
따라서 이들을 소비하는 함수는 `target_link_libraries`입니다.
```cmake
find_package(gRPC CONFIG REQUIRED)
# ...
target_link_libraries(main
PRIVATE
gRPC::gpr gRPC::grpc gRPC::grpc++ gRPC::grpc_cronet
)
```
물론 여기에는 하나의 전제가 있습니다.
해당 라이브러리가 CMake에서 Import할 수 있도록 적절하게 Manifest를 작성해 놓았거나, CMake의 `export`함수를 사용해 CMake를 위한 Manifest를 생성해놓은 것입니다.
>
> Manifest라는 표현은 **이 문서 상에서만** "Package를 내보낸것과 같이 가져오기 위한 목록"이라는 의미로 사용하기에
> 웹에서 CMake관련 검색할 때 사용하면 오히려 방해가 될 수 있습니다.
>
어떤 파일을 제공해야 하는지 알아보기 위해 아래와 같이 CMakeLists.txt를 작성해 실행해보겠습니다.
```cmake
cmake_minimum_required(VERSION 3.8)
find_package(TBB REQUIRED)
```
[Intel TBB](https://github.com/intel/tbb)가 설치되지 않은 환경에서 `find_package`가 실패하면서 아래와 같은 오류를 출력할 것입니다.
```console
$ cmake .
...
-- Detecting CXX compile features - done
CMake Error at CMakeLists.txt:3 (find_package):
By not providing "FindTBB.cmake" in CMAKE_MODULE_PATH this project has
asked CMake to find a package configuration file provided by "TBB", but
CMake did not find one.
Could not find a package configuration file provided by "TBB" with any of
the following names:
TBBConfig.cmake
tbb-config.cmake
Add the installation prefix of "TBB" to CMAKE_PREFIX_PATH or set "TBB_DIR"
to a directory containing one of the above files. If "TBB" provides a
separate development package or SDK, be sure it has been installed.
-- Configuring incomplete, errors occurred!
```
이를 통해 `find_package`에서 TBB라는 이름을 가지고 대소문자가 혼합된 경우(`TBBConfig.cmake`)와 소문자만 사용된 경우(`tbb-config.cmake`)를 고려하여 Manifest파일을 찾으려 했다는 것을 알 수 있습니다.
#### `-config.cmake`
이전까지는 Manifest파일이라고 하였으나, 이후로는 `-config.cmake`파일이라고 하겠습니다.
TBB를 설치하면 TBBConfig.cmake가 생성된 것을 확인할 수 있습니다. 다행히 TBB의 `-config.cmake`파일은 비교적 짧은 편에 속합니다. Details를 열어 한번 읽어보시기 바랍니다.
TBBConfig.cmake <------------------ click me !!!!
```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가지 정도를 눈여겨 볼 수 있습니다.
1. `add_library(IMPORTED)`를 사용해서 CMake Target을 생성합니다. 이름으로는 `TBB::${_tbb_component}`를 사용해서 이것이 CMake Target이라는 점을 분명히 드러내고 있습니다.
2. `set_property`함수를 사용해서 DEBUG/RELEASE 설정으로 빌드되었다는 정보를 추가하는 것을 볼 수 있습니다.
3. `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라고 할 수 있겠습니다.
## [Property](https://cmake.org/cmake/help/latest/manual/cmake-properties.7.html)
[CMake에서는 굉장히 많은 Property를 정의하고 있습니다.](https://cmake.org/cmake/help/v3.14/manual/cmake-properties.7.html)
특히 이들을 사용하기 어렵게 만드는 것은, Target의 타입에 따라서 사용할 수 있는 property가 달라진다는 것입니다.
한때 작성자는 QMake를 볼때처럼 좀 읽다보면 이해하게 되겠거니 하였지만 아주 멍청하고 어리석은 생각이었습니다.
여러분은 직접적으로 Property를 조작하는 일을 멀리하고 참고영상에 나온 용례들을 보시면서 따라하시는게 낫습니다.
#### [`set_property`](https://cmake.org/cmake/help/latest/command/set_property.html)/[`get_property`](https://cmake.org/cmake/help/latest/command/get_property.html)
솔직히 적건대 작성자는 `define_property`, `set_property`, `get_property`를 쓰는 경우는 `-config.cmake`를 제외하고 아직 보지 못했습니다.
여기서는 단순히 함수의 시그니처만 확인할 수 있도록 실행가능한 예시를 적어놓겠습니다.
```cmake
cmake_minimum_required(VERSION 3.8)
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})
```
#### [`set_target_properties`](https://cmake.org/cmake/help/latest/command/set_target_properties.html)
3.x 버전의 CMake에서 export 된 `-config.cmake`파일들은 대부분 아래와 같은 Property들을 설정합니다.
* `INTERFACE_INCLUDE_DIRECTORIES`: 헤더 파일이 위치한 폴더들
`/usr/local/include;/usr/include` 형태로 ';'을 써서 여러 폴더를 지정할 수 있습니다.
* `INTERFACE_LINK_LIBRARIES`: 현재 Target의 의존성을 보여주는 부분입니다.
`target_link_libraries`에서 필요로 하는 인자, 즉 다른 CMake Target들의 이름을 ';'로 구분되는 목록을 사용해서 지정합니다.
(`INTERFACE_INCLUDE_DIRECTORIES`와 동일)
* `IMPORTED_LOCATION`: 서브 프로그램의 위치를 '절대경로'로 지정합니다.
지금까지는 대부분 상대경로로 해결할 수 있었으나, 여기서 절대경로만을 허용하는 이유는 지금 `find_package`하는 대상이 이미 **설치**되었기 때문일 것입니다.
* `IMPORTED_IMPLIB`: Windows의 경우 링킹을 위해 .lib파일이 필요하기도 합니다.
다른 플랫폼에서는 사용되는 것을 보지 못했습니다.
실제 사용하는 모습은 다음과 같습니다.
```cmake
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
cmake_minimum_required(VERSION 3.8)
add_library(my_modern_cpp_lib
src/libmain.cpp
)
set_target_properties(my_modern_cpp_lib
PROPERTIES
CXX_STANDARD 17
)
```
작성자가 지금까지 항상 3.8 버전을 사용한 이유가 여기에 있습니다. CMake 3.14부터는 C++ 20을 명시할 수 있습니다.
```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_CURRENT_LIST_FILE`](https://cmake.org/cmake/help/latest/variable/CMAKE_CURRENT_LIST_FILE.html)
절대 경로를 지정해야 하는 경우, `/usr/local`과 같이 잘 알려진 경로면 좋겠지만 그렇지 못한 경우 해당 `-config.cmake`를 기준으로 탐색을 해야 할수도 있습니다. 여기에는 보통 `CMAKE_CURRENT_LIST_FILE` 변수가 사용됩니다.
이 변수는 `include`되는 `.cmake` 파일의 위치를 저장하고 있습니다.
물론 `CMakeLists.txt`도 예외가 아닙니다.
아래와 같이 파일이 배치되었다고 가정해보겠습니다.
```console
$ tree $(pwd)
/path/to
├── CMakeLists.txt
└── cmake
├── print-current-path.cmake
└── print-parent-path.cmake
1 directory, 3 files
```
각각의 내용이 아래와 같다면...
```cmake
# cmake/print-current-path.cmake
message(STATUS "cmake : ${CMAKE_CURRENT_LIST_FILE}")
# CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
include(cmake/print-current-path.cmake)
message(STATUS "cmakelist: ${CMAKE_CURRENT_LIST_FILE}")
```
이런 결과가 출력될 것입니다.
```console
$ cmake .
...
-- cmake : /path/to/cmake/print-filepath.cmake
-- cmakelist: /path/to/CMakeLists.txt
...
-- Configuring done
-- Generating done
```
#### [`get_filename_component`](https://cmake.org/cmake/help/latest/command/get_filename_component.html)
보통 특정 경로 하나만으로는 문제를 해결할 수 없기 때문에 여기서는 경로를 다루는 방법 중 두가지를 짚고 넘어가겠습니다.
기본적으로 CMake에서 파일의 경로 생성할때는 `get_filename_component`를 사용합니다.
앞서 `TBBConfig.cmake`에서도 이 함수가 사용되었었는데, 코드를 보면 의도를 파악하기가 어렵습니다.
```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)
# ...
```
##### Parent Path (Removing Base Name)
CMake 문서에 따르면 가장 마지막에 사용된 인자 `PATH`는 2.8 버전들의 하위호환을 위한 것으로, 그 의미는 `DIRECTORY`를 사용하는 것과 동일합니다.
```
DIRECTORY = Directory without file name
PATH = Legacy alias for DIRECTORY (use for CMake <= 2.8.11)
```
따라서 현재 기술하고 있는 3.8 이후 버전을 기준으로 작성한다면 아래와 같을 것입니다.
```cmake
# ...
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
# 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를 아래처럼 수정해 실행하면:
```cmake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
include(cmake/print-current-path.cmake)
include(cmake/print-parent-path.cmake) # <--- new !
message(STATUS "cmakelist: ${CMAKE_CURRENT_LIST_FILE}")
```
출력 결과는 아래와 같을 것입니다.
```console
$ cmake .
-- cmake : /path/to/cmake/print-current-path.cmake
-- parent : /path/to/cmake
-- cmakelist: /path/to/CMakeLists.txt
...
-- Configuring done
-- Generating done
```
##### Path Join
경로를 처리할때 접합(concat)을 수행하는 코드를 흔히 볼 수 있습니다.
이런 코드들은 절대 경로(Absolute Path)와 상대 경로(Relative Path)가 고르게 사용되는 반면,
CMake에서 파일 경로는 특별한 처리가 필요하지 않는 한 절대 경로를 사용합니다.
>
> 작성자의 생각으로는, `PROJECT_SOURCE_DIR`, `CMAKE_CURRENT_SOURCE_DIR` 등 파일경로를 만들때 가장 기초가 되는 경로가 모두 절대경로로 반환되기 때문인 것 같습니다.
>
이미 존재하는 **폴더** 경로에 새로운 이름을 붙이는 것은 보통의 문자열 생성 방법과 같습니다. Windows에서는 Command Prompt를 실행하는 경우라면 `\\`를 구분자로 사용해야 하지만, 단순히 CMake 내에서 경로만 처리한다면 `/`를 사용해도 별다른 문제가 없습니다.
```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에서 구분자로 `/`를 사용하는 것을 확인할 수 있습니다.
```console
PS > cmake .
...
-- modules : D:/path/to/cmake
...
```
WSL Bash에서는 아래와 같습니다.
```console
$ cmake .
...
-- modules : /mnt/d/cmake_tutorial/path/cmake
...
```
#### [`get_target_property`](https://cmake.org/cmake/help/latest/command/get_target_property.html)
`set_target_properties`가 여러 Property를 한번에 설정할 수 있는데 반해, `get_target_property`는 한번에 하나의 변수를 생성합니다. 사용법 또한 굉장히 단순합니다.
```cmake
cmake_minimum_required(VERSION 3.8)
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}")
```
이제 `find_package`는 Target을 선언하고 Property들을 설정한다는 것과 Property를 설정하고 확인하는 함수들을 다루었으므로
빌드 이후 Manifest파일을 생성하는 방법에 대해 다뤄보겠습니다.
## [CMake Export](https://cmake.org/cmake/help/latest/command/export.html)
사실 CMake에서 Export하는 방법은 튜토리얼마다 설명이 조금씩 다른데, 근본적인 차이점은 CMake를 위한 템플릿 파일을 사용하는지에 달려 있습니다.
어떤 프로젝트에서는 CMake 모듈들이 배치된 폴더에 `package-targets.cmake.in`과 같에 `.in`으로 끝나는 파일들이 있는 것을 볼 수 있는데,
이런 인라인 파일들은 어디선가 CMake에서 제공하는 [`configure_file`](https://cmake.org/cmake/help/latest/command/configure_file.html) 혹은 [`configure_package_config_file`](https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html#generating-a-package-version-file)함수를 사용하기 때문일 가능성이 높습니다.
이 함수는 CMake파일 생성 뿐만 아니라 사용자 환경에 맞는 헤더 파일(.h)을 만들거나, Linux플랫폼에서 에서 pkg-config를 위한 파일을 만드는데 사용되기도 합니다.
>
> 작성자는 이 기능을 사용하는 것을 추천하지 않습니다.
> 빌드 시스템 파일을 생성하는것이 CMake의 가장 중요한 부분이며, 오직 그 일에 집중해야 한다고 생각하기 때문입니다
>
### CMake Manifest 파일의 배치
지금까지 `find_package`에 어떤 인자를 사용하는지, 해당 함수에서 사용하는 Manifest 파일에 어떤 내용이 들어가는지는 살펴보았으나, **어디에서** 해당 파일을 찾는지는 설명하지 않았습니다.
#### [CMake의 Manifest 탐색 과정](https://cmake.org/cmake/help/latest/command/find_package.html#search-procedure)
[CMake 문서의 설명](https://cmake.org/cmake/help/latest/command/find_package.html#search-procedure)에 따르면 플랫폼마다 탐색 경로가 다르지만, 공통되는 경로가 있다는 것을 알 수 있습니다.
앞서 이 문서에서는 `install`을 사용할 때 `CMAKE_INSTALL_PREFIX`를 기준으로 설치경로를 지정하는 것을 권했었는데,
아마 아래처럼 경로에 프로젝트 이름이 들어가는 것이 다른 프로젝트와의 충돌의 가능성을 낮춰줄 것입니다.
```cmake
cmake_minimum_required(VERSION 3.8)
project(my_modern_cpp_lib LANGUAGES CXX)
# ...
install(FILES ${VERSION_FILE_PATH}
${LICENSE_FILE_PATH}
DESTINATION ${CMAKE_INSTALL_PREFIX}/share/${PROJECT_NAME}
)
```
### CMake Manifest 만들기
#### [`write_basic_package_version_file`](https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html#generating-a-package-version-file)
버전 정보를 추가하는 것은 이미 [CMake에서 제공하는 모듈](https://cmake.org/cmake/help/latest/module/CMakePackageConfigHelpers.html#cmakepackageconfighelpers)을 사용하면 쉽게 작성할 수 있습니다.
```cmake
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}
)
```
#### [`install(EXPORT)`](https://cmake.org/cmake/help/latest/command/install.html#export)
[파일](https://cmake.org/cmake/help/latest/command/install.html#installing-files) 혹은
[폴더](https://cmake.org/cmake/help/latest/command/install.html#installing-directories)의 설치는 단순히 복사/갱신으로 끝날 수 있지만,
결정적으로 `-config.cmake`에는 프로젝트에서 빌드할 [Target](https://cmake.org/cmake/help/latest/command/install.html#targets)에 대한 정보가 들어가야 합니다.
여기에는 `install(TARGETS)`와 `install(EXPORT)`가 함께 사용됩니다.
간단한 예시로, 아래와 같은 구조의 프로젝트를 만들어보겠습니다.
```console
$ tree $(pwd)
/mnt/d/example
├── CMakeLists.txt
└── src
├── CMakeLists.txt
└── libmain.cpp
1 directory, 3 files
```
우선 Root CMakeLists.txt에서는 `EXPORT_NAME`변수를 만들고 `add_subdirectory`로 하위 모듈들을 빌드하도록 합니다. 최종적으로는 `install(EXPORT)`를 사용해 설치까지 수행합니다.
```cmake
# CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(stones LANGUAGES CXX)
set(EXPORT_NAME ${PROJECT_NAME}-config) # or ${PROJECT_NAME}Config
add_subdirectory(src) # <--- uses EXPORT_NAME
install(EXPORT ${EXPORT_NAME}
NAMESPACE stones::
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).**
```cmake
# src/CMakeLists.txt
add_library(stone1 SHARED
libmain.cpp
)
set_target_properties(stone1
PROPERTIES
CXX_STANDARD 17
)
target_include_directories(stone1
PUBLIC
$
$
)
install(TARGETS stone1
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)이 가능하도록 정의합니다.
```c++
#pragma once
// 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__)
# define _INTERFACE_ __attribute__((visibility("default")))
# define _HIDDEN_ __attribute__((visibility("hidden")))
#else
# error "unexpected linking configuration"
#endif
// clang-format on
#include
constexpr auto version_code = 0x0102;
_INTERFACE_ uint32_t get_version() noexcept;
uint32_t get_version() noexcept{
return version_code;
}
```
#### Export 결과 확인
Windows에서 설치를 수행하면 아래와 같이 `stones-config.cmake`파일이 설치되는 것을 볼 수 있습니다.
```console
PS D:\examples\build> cmake --build . --config debug --target install
PS D:\install> Tree /f .
Folder PATH listing for volume keep
Volume serial number is B47E-DE87
D:\INSTALL
├─bin
│ stone1.dll
│
├─lib
│ stone1.lib
│
└─share
└─stones
stones-config-debug.cmake
stones-config.cmake
```
여기서 설치된 파일의 이름인 `stones-config`는 앞서 `EXPORT_NAME` 변수의 값을 따른 것입니다.
```cmake
set(EXPORT_NAME ${PROJECT_NAME}-config) # or ${PROJECT_NAME}Config
```
불필요한 부분을 제외하고 해당 파일의 내용을 살펴보면 `stones::stone1`와 같이 Target을 가져오는 내용이라는 것을 알 수 있습니다.
이런 파일들은 `stones-targets.cmake`로 따로 만들고 `-config.cmake`는 `configure_package_config_file`을 사용해서 만드는 방법을 사용하기도 합니다.
하지만 이 예시에서는 Import측에 전달할 정보가 없기에 CMake 템플릿 파일을 작성하지 않았고, 따라서 바로 `-config.cmake`를 생성해도 무방합니다.
```cmake
# stones-config.cmake
# ...
# The installation prefix configured by this project.
set(_IMPORT_PREFIX "D:/install")
# Create imported target stones::stone1
add_library(stones::stone1 SHARED IMPORTED)
set_target_properties(stones::stone1 PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "D:/install/include"
)
# Load information for each installed configuration.
get_filename_component(_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
file(GLOB CONFIG_FILES "${_DIR}/stones-config-*.cmake")
foreach(f ${CONFIG_FILES})
include(${f})
endforeach()
# ...
```
특히 패턴매칭(`stones-config-*.cmake`)을 사용해 `-config-debug.cmake`혹은 `-config-release.cmake`를 `include`할 수 있도록 되어있는 점에 주목하시길 바랍니다.
앞서 `write_basic_package_version_file`에서 Version 파일의 설치 위치를 비롯해 이름을 `${PROJECT_NAME}-config-version.cmake`로 만들도록 한 것은 이를 고려한 것입니다.
```cmake
set(EXPORT_NAME ${PROJECT_NAME}-config)
add_subdirectory(src) # <--- uses EXPORT_NAME
install(EXPORT ${EXPORT_NAME}
NAMESPACE stones::
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}
)
```
#### `target_include_directories`: Build >> Install
좀 전의 예시에서 처음으로 보인 `BUILD_INTERFACE`와 `INSTALL_INTERFACE`의 사용을 한마디로 정리하자면,
"빌드할 때 사용하는 include 폴더와 설치 후 사용하는 include 폴더가 다르다" 라는 것입니다.
```
target_include_directories(stone1
PUBLIC
$
$
)
```
위와 같이 작성하는 것을 CMake에서는 [Generator Expression](https://cmake.org/cmake/help/latest/manual/cmake-generator-expressions.7.html)이라 하는데, 보통 플랫폼에 따라 IF/ELSE/AND/OR이 뒤섞여 가독성을 심하게 해치는 경향이 있습니다.
라이브러리가 설치된 이후에는 Build에 사용한 폴더가 삭제될 가능성이 높기에, Import할 때 소스코드가 배치된 폴더를 사용하도록 한다면 파일을 못찾는 문제가 발생할 것입니다.
이를 막기 위해 빌드시에는 `PROJECT_SOURCE_DIR`기준으로 include를 수행하지만, 설치 이후에는 `CMAKE_INSTALL_PREFIX`를 기준으로 include를 수행합니다.
아마 인터페이스 파일들은 이미 `CMAKE_INSTALL_PREFIX/include`로 `install(FILES)` 혹은 `install(DIRECTORIES)`를 통해서 복사되었을 것이기에 설치가 왼료된 시점부터 해당 폴더는 사용가능한 경로가 될 것입니다.