Post

[DevTools/CMake] Package, Property, Export

Package는 find_package()를 통해 외부 라이브러리를 탐색하고 사용하는 개념이다. Property는 타겟이나 디렉토리에 설정되는 메타 정보로, 컴파일 옵션·include 경로 등 동작을 제어한다. Export는 현재 프로젝트의 타겟을 다른 프로젝트에서 find_package()로 사용할 수 있도록 내보내는 기능이다.

[DevTools/CMake] Package, Property, Export

Package의 구성

보통 패키지라고 하면 RPM, Brew 처럼 관리 소프트웨어를 통해 다운로드/설치/업데이트해서 사용하는 프로그램들을 말하는데, C++ 개발자들에게 패키지란 개발에 필요한 Library + Manifest에 가까운 것 같다.

  • 일반적인 패키지
    • 실행 프로그램 (excecutable)
    • 문서 파일 (license, manual, readme 등)
  • 프로그래밍 패키지 <일반 패키지 및 개발에 필요한 요소들>
    • 서브 프로그램 (library)
    • 실행 프로그램 (test tools, script 등)
    • 소스 코드 (include, example 등)

C++에서는 미리 빌드된 서브 프로그램 뿐만 아니라 소스 코드가 포함된다는 점(include)이 특이하다고 볼 수 있다. 비단 템플릿 프로그래밍의 비중이 늘어난 것 뿐만 아니라 크로스 컴파일링킹에 손이 많이 가기 때문이기도 할 것이다.

지금은 많은 C++ 프로젝트들이 UNIX FileSystem에서 표준 C 라이브러리를 배치할 때 사용하던 파일트리 구조를 적용하고 있다. 굳이 이런 배치에 어떤 의미가 부여되어있다기 보다는, CMake의 초창기부터 UNIX 시스템에 빌드 된 라이브러리를 설치하면서 관례를 따르던 것이 현재까지 이어지고 있다 정도로 생각하면 될 것 같다.

  • bin
    • 실행 프로그램 (executable)
  • lib
    • 미리 빌드된 라이브러리 (*.so, *.lib 등)
  • include
    • 소스 코드 (header)
  • share
    • 기타 필요한 파일들 <주로 빌드="" 지원="" 파일="">
    • docs
      • 문서가 있는 경우

CMake의 Package 찾기

find_package

이미 설치된 패키지를 찾는 기능으로 CMakefind_package를 제공하고 있다.

✔️ 참고

사용하는 라이브러리가 CMake를 지원하는데 find_package()가 매끄럽게 사용되지 않는다?

이럴 때는 add_subdirectory()를 사용하는 것이 정확한 해결책이 될 수 있다. Package Export에 문제가 있는 경우, Import하는 쪽에서 수정하기가 어렵기 때문이다.

find_package() 함수는 일반적으로 아래와 같이 이름과 버전을 인자로 사용한다. 탐색에 성공하면 name_FOUND 변수가 생성된다. 아래의 예처럼 이름으로 OpenCV를 사용했다면, 성공여부는 OpenCV_FOUND로 확인할 수 있다.

1
2
3
4
5
6
7
8
9
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를 실행해보자.

1
2
3
4
5
6
find_package(fmt 5.3
CONFIG
	REQUIRED
    PATHS 		/home/user/vcpkg/installed/x64-linux
    			C:/vcpkg/installed/x64-windows
)

수 많은 컴포넌트를 가진 Boost에서 필요한 모듈만 가져다 쓴다면, 아래처럼 작성하면 될 것이다. 분명히 설치 되었음에도 CMake에서 찾지 못한다면 CONFIG를 지우고 다시 시도해보면 찾을 수도 있 다.

1
2
3
4
5
6
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()이다.

1
2
3
4
5
6
find_package(gRPC CONFIG REQUIRED)
// ...
target_link_libraries(main
PRIVATE
	gRPC::gpr gRPC::grpc gRPC::grpc++ gRPC::grpc_cronet
)

여기에는 하나의 전제가 있다. 해당 라이브러리가 CMake에서 Import할 수 있도록 적절하게 config 파일을 작성해 놓았거나, CMakeexport 함수를 사용해 CMake를 위한 config 파일을 생성해 놓은 것이다.

어떤 파일을 제공해야 하는지 알아보기 위해 아래와 같이 CMakeLists.txt를 작성해 실행해보자.

1
2
3
cmake_minimum_required(VERSION 3.30)

find_package(TBB REQUIRED)

Intel TBB가 설치되지 않은 환경에서 find_package()가 실패하면서 아래와 같이 오류를 출력할 것이다.

img

이를 통해 확인할 수 있는 것은 find_package()에서 TBB라는 이름으로 대소문자가 혼합된 경우(TBBConfig.cmake)와 소문자만 사용된 경우(tbb-config.cmake)를 고려하여 config 파일을 찾으려 했다는 것을 알 수 있다.

xxx-config.cmake

TBB를 설치하면, TBBConfig.cmake가 생성된 것을 확인할 수 있다. 다행히 TBBxxx-config.cmake 파일은 비교적 짧은 편에 속한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/*
# 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라고 할 수 있다.

Property

CMake에서는 굉장히 많은 Property를 정의하고 있다. 특히 이들을 사용하기 어렵게 만드는 것은, Target의 타입에 따라 사용할 수 있는 property가 달라진다는 것이다.

set_property() / get_property

define_property, set_property, get_property를 사용하는 경우는, xxx-config.cmake를 제외하고 많이 사용되고 있지는 않는 듯 하다.

1
2
3
4
5
6
7
8
9
10
11
12
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})

set_target_properties

3.x 버전의 CMake에서 exportxxx-config.cmake 파일들을 대부분 아래와 같은 Property들을 설정한다.

  • INTERFACE_INCLUDE_DIRECTORIES
    • 헤더 파일이 위치한 디렉토리들
    • /usr/local/include;/usr/include 형태로 ;을 사용해서 여러 디렉토리를 지정할 수 있다.
  • INTERFACE_LINK_LIBRARIES
    • 현재 Target의 의존성을 보여주는 부분이다.
    • target_link_libraries()에서 필요로 하는 인자, 즉 다른 CMake Target들의 이름을 ;로 구분되는 목록을 사용해서 지정한다.
  • IMPORTED_LOCATION
    • 서브 프로그램의 위치를 절대 경로로 지정한다.
    • 대부분 상대 경로로 해결할 수 있으나, 여기서는 절대 경로만을 허용하는 이유가 있다. 그것은 find_package()하는 대상이 이미 설치되었기 때문일 것이다.
  • IMPORTED_IMPLIB
    • Windows의 경우, 링킹을 위해 .lib파일이 필요하다. (다른 플랫폼에서는 잘 사용되지는 않는 듯)

실제 사용하는 모습은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 버전의 따라 결정된다.

1
2
3
4
5
6
7
8
9
10
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)

CMAKE_CURRENT_LIST_FILE

절대 경로를 지정해야 하는 경우, /usr/local과 같이 잘 알려진 경로면 좋겠지만 그렇지 못한 경우 해당 xxx-config.cmake를 기준으로 탐색을 해야 할 수도 있다. 여기에는 보통 CMAKE_CURRENT_LIST_FILE 변수가 사용된다. 이 변수는 include 되는 .cmake 파일의 위치를 저장하고 있다. 물론 CMakeLists.txt도 예외가 아니다.

아래와 같이 파일이 배치되었다고 가정해보자.

1
2
3
4
5
6
7
8
$ tree $(pwd)
/path/to
  CMakeLists.txt
  cmake/
    print-current-path.cmake
    print-parent-path.cmake

1 directory, 3 files

각각의 내용이 아래와 같다면:

1
2
3
4
5
6
7
8
// 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}")

이런 결과가 출력될 것이다.

1
2
3
4
5
6
...
-- cmake 	: /path/to/cmake/print-filepath.cmake
-- cmakelist: /path/to/CMakeLists.txt
...
-- Configuring done
-- Generating done

get_filename_component()

보통 특정 경로 하나만으로는 문제를 해결할 수 없기 때문에 여기서는 경로를 다루는 방법 중 두 가지를 짚고 넘어가자.

기본적으로 CMake에서 파일의 경로를 생성할 때get_filename_component()를 사용합니다. 앞서 TBBConfig.cmake에서도 이 함수가 사용되었는데, 코드를 보면 의도를 파악하기가 어렵다.

1
2
3
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 문서에 따르면, 가장 마지막에 사용된 인자 PATH2.8 버전들의 하위 호환을 위한 것으로, 그 의미는 DIRECTORY를 사용하는 것과 동일하다.

1
2
DIRECTORY 	= Directory without file name
PATH 		= Legacy alias for DIRECTORY (use for CMake <= 2.8.11)

따라서 현재 기술하고 있는 3.8 이후 버전을 기준으로 작성한다면 아래와 같을 것이다.

1
2
3
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에 적용해보면 어떻게 될까?

1
2
3
4
// 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를 아래처럼 수정해 실행하면,

1
2
3
4
5
6
7
// 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}")

출력 결과는 아래와 같을 것이다.

1
2
3
4
5
6
7
8
$ cmake .

-- cmake 	: /path/to/cmake/print-current-path.cmake
-- parent 	: /path/to/cmake
-- cmakelist: /path/to/CMakeLists.txt
...
-- Configuring done
-- Generaing done

Path Join

경로를 처리할 때 집합(concat)을 수행하는 코드를 흔히 볼 수 있다. 이런 코드들은 절대 경로(Absolute Path)상대 경로(Relative Path)가 고르게 사용되는 반면, CMake에서 파일 경로는 특별한 처리가 필요하지 않는 한 절대 경로를 사용한다.

이미 존재하는 디렉토리 경로에 새로운 이름을 붙이는 것은 보통의 문자열 생성 방법과 같다. Windows에서는 Command Prompt를 실행하는 경우라면 \\를 구분자로 사용해야 하지만, 단순히 CMake 내에서 경로만 처리한다면 /를 사용해도 별다른 문제가 없다.

1
2
3
4
// 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에서 구분자로 /를 사용하는 것을 확인할 수 있다.

1
2
3
4
PS > cmake .
...
-- modules : D:/path/to/cmake
...

WSL terminal에서는 아래와 같다.

1
2
3
$ cmake .
...
-- modules : /mnt/drive/test-proj/path/cmake

다음의 명령을 자세히 풀어쓰면:

1
get_file_component(CURRENT_MODULE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/cmake ABSOLUTE)

get_filename_component()CMake에서 파일 경로의 일부를 추출하거나 조작하는데 사용되는 명령어이다. 제공된 명령어를 설명하면 다음과 같다.

  • get_filename_component
    • 파일 경로의 일부를 추출하거나 수정한다.
  • CURRENT_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에는 다음과 같은 값이 저장된다.

1
/path/to/project/cmake

get_target_property()

set_target_properties가 여러 Property를 한번에 설정할 수 있는데 반해, get_target_property는 한번에 하나의 변수를 생성한다. 사용법 또한 단순하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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에서 Export하는 방법은 튜토리얼마다 설명이 조금씩 다른데, 근본적인 차이점CMake를 위한 템플릿 파일을 사용하는지에 달려 있다. 어떤 프로젝트에서는 CMake 모듈들이 배치된 디렉토리에 package-targets.cmake.in과 같이 .in으로 끝나는 파일들이 있는 것을 볼 수 있는데, 이런 인라인 파일들은 어디선가 CMake에서 제공하는 configure file 혹은 configure_package_config_file 함수를 사용하기 때문일 가능성이 높다.

이 함수는 CMake 파일 생성 뿐만 아니라 사용자 환경에 맞는 헤더 파일(.h)을 만들거나, 리눅스 플랫폼에서 pkg-config를 위한 파일을 만드는데 사용된다.

CMake의 Config 파일 탐색 과정

CMake 문서의 설명에 따르면, 플랫포마다 탐색 경로가 다르지만 공통되는 경로가 있다는 것을 알 수 있다. install을 사용할 때, CMAKE_INSTALL_PREFIX를 기준으로 설치경로를 지정하는 것을 권하지만, 아래처럼 경로에 프로젝트 이름이 들어가는 것이 다른 프로젝트와의 충돌의 가능성을 낮춰줄 것이다.

1
2
3
4
5
6
7
8
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의 Config 파일 만들기 (write_basic_package_version_file)

버전 정보를 추가하는 것은 이미 CMake에서 제공하는 모듈을 사용하면 쉽게 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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}
)

install(EXPORT)

file 혹은 directory의 설치는 단순히 복사/갱신으로 끝날 수 있지만, 결정적으로 xxx-config.cmake에는 프로젝트에서 빌드할 Target에 대한 정보가 들어가야 한다. 여기에는 install(TARGETS)install(EXPORT)가 함께 사용된다.

간단한 예로, 아래와 같은 구조의 프로젝트를 만들어보자.

img

우선 Root CMakeLists.txt 파일에서는 EXPORT_NAME 변수를 만들고, add_subdirectory()로 하위 모듈들을 빌드하도록 한다. 최종적으로 install(EXPORT)를 사용해 설치까지 수행한다.

1
2
3
4
5
6
7
8
9
10
11
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}
)

srcCMakeLists.txt 파일은 add_library()CMake Target을 생성하고, install(TARGETS)에서 EXPORT 인자를 사용해 해당 라이브러리를 일종의 Export Group에 추가한다. 단순히 추가하기만 할 뿐, install(EXPORT)를 사용하기 전까지 실제 설치는 이루어지지 않는다.

특이하게도 EXPORT반드시 다른 인자보다 먼저 사용되어야 한다고 명시하고 있다 (must appear before).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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)이 가능하도록 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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를 수행하면 다음과 같이 ``파일이 설치되는 것을 볼 수 있다.

1
2
3
$ cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="<설치 경로>"

$ cmake --build build --config Debug --target install

img

여기서 설치된 파일의 이름인 test-proj-config는 앞서 EXPORT_NAME 변수의 값을 따른 것이다.

1
set(EXPORT_NAME ${PROJECT_NAME}-config) # or ${PROJECT_NAME}Config

불필요한 부분을 제외하고 해당 파일의 내용을 살펴보면, test::test-lib 와 같이 Target을 가져오는 내용이라는 것을 알 수 있다. 이런 파일들은 test-proj-targets.cmake로 따로 만들고 xxx-config.cmakeconfigure_package_config_file을 사용해서 만드는 방법을 사용하기도 한다. 하지만 이 예시에서는 Import 측에 전달할 정보가 없기에 CMake 템플릿 파일을 작성하지 않았고, 따라서 바로 xxx-config.cmake를 생성해도 무방하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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.cmakeinclude 할 수 있도록 되어있는 점에 주목하자. 앞서 write_basic_package_version_file에서 Version 파일의 설치 위치를 비롯해 이름을 ${PROJECT_NAME}-config-version.cmake로 만들도록 한 것은 이를 고려한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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}
)

target_include_directories : Build » Install

위의 예시에서 BUILD_INTERFACEINSTALL_INTERFACE의 사용을 한마디로 정리하자면, 빌드할 때 사용하는 include 디렉토리와 설치 후 사용하는 include 디렉토리가 다르다라는 것이다.

1
2
3
4
5
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/includeinstall(FILES) 혹은 install(DIRECTORIES)를 통해서 복사되었을 것이기에 설치가 완료된 시점부터 해당 디렉토리는 사용가능한 경로가 될 것이다.

This post is copyrighted by the author. All rights reserved.