CMake Package, Property, Export

Seongcheol Jeon·2024년 10월 10일
0

CMake

목록 보기
17/17
post-thumbnail

Package

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로 확인할 수 있다.

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 파일을 작성해 놓았거나, CMakeexport 함수를 사용해 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 파일을 찾으려 했다는 것을 알 수 있다.

xxx-config.cmake

TBB를 설치하면, TBBConfig.cmake가 생성된 것을 확인할 수 있다. 다행히 TBBxxx-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라고 할 수 있다.

Property

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

set_property() / get_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})

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파일이 필요하다. (다른 플랫폼에서는 잘 사용되지는 않는 듯)

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

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)

CMAKE_CURRENT_LIST_FILE

절대 경로를 지정해야 하는 경우, /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

get_filename_component()

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

기본적으로 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)

Parent Path (Removing Base Name)

CMake 문서에 따르면, 가장 마지막에 사용된 인자 PATH2.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

Path Join

경로를 처리할 때 집합(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_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에는 다음과 같은 값이 저장된다.

/path/to/project/cmake

get_target_property()

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에서 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를 기준으로 설치경로를 지정하는 것을 권하지만, 아래처럼 경로에 프로젝트 이름이 들어가는 것이 다른 프로젝트와의 충돌의 가능성을 낮춰줄 것이다.

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에서 제공하는 모듈을 사용하면 쉽게 작성할 수 있다.

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)가 함께 사용된다.

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

우선 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}
)

srcCMakeLists.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.cmakeconfigure_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.cmakeinclude 할 수 있도록 되어있는 점에 주목하자. 앞서 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}
)

target_include_directories : Build >> Install

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

0개의 댓글