51
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【モダンCMake】つくったC++ライブラリを簡単にインストールできるようにする

Last updated at Posted at 2019-12-30

概要

CMakeを用いて、C/C++製のライブラリをインストール可能にし、別のCMakeプロジェクトやCMake管理外のC/C++コードから容易に利用できるようにするような方法の紹介です。皆のライブラリにmake installターゲットを追加しましょう。

背景だったものポエム

ポエムになった。ここは読み飛ばして構いません。C++製のプロジェクトのビルドツールとしてはCMakeがデファクトスタンダードの一つとしての位置を占めているのではないかと思います。しかし、その独特で自由度の高い表現からややとっつきにくく、また、パッケージマネージャ的な要素の薄さ1から、C++でライブラリを製作しても、それを他のプロジェクトに向け公開する事は少しハードルが上がるように思われます。せっかく製作された素晴らしいライブラリが、他のプロジェクトから利用しづらく、日の目を見る事ができないのは良い事ではないです。また、ライブラリの充実度は言語の特徴を決める重要なファクターになるでしょう。この投稿が多少でもライブラリの製作者及びユーザの助けになれれば幸いです。

やっていきましょう

例として以下のようなヘッダオンリーのプロジェクトを扱います。

mylib/
├── CMakeLists.txt
└── include
    └── mylib
        └── hello_world.hpp

簡単なCMakeLists.txtは以下のような形になると思われます。(このあたりについては他の情報を参照してください)

mylib/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)

project(mylib VERSION 0.1.0 LANGUAGE CXX)

# INTERFACE、コンパイルを要しない(ヘッダオンリー)
add_library(mylib INTERFACE)

# c++11以上を要求
target_compile_features(mylib INTERFACE cxx_std_11)

# インクルードパスに以下を追加する
target_include_directories(mylib INTERFACE
	${CMAKE_CURRENT_SOURCE_DIR}/include)

パッケージをエクスポートする

CMakeでライブラリを他のプロジェクトに公開するには大きく二つの手法が考えられます。一つはadd_subdirectoryを使う方法です。これは上に書いたようなCMakeLists.txtのみですぐに利用できます。mylib/を配下に持った別のプロジェクトからadd_subdirectory(mylib)とする事で、mylib/CMakeLists.txt内で定義されたターゲットを利用できるようになります。

another/CMakeLists.txt
add_subdirectory(mylib)

add_executable(your-exe main.cpp)
target_link_libraries(your-exe mylib)

もう一つが、グローバルな環境にパッケージをインストールする方法です。これによって、子ディレクトリにmylib/を持っていないプロジェクトからもfind_package(mylib)のようにしてmylibが利用可能になる他、CMakeを利用していないコードからライブラリを使う事も容易になります。

パッケージのインストールは、パッケージの利用に必要な情報が記録されたファイル、<package-name>-config.cmakeあるいは<PackageName>Config.cmakeと、必要なリソース(ヘッダファイルやビルド済ライブラリ等)を適切なディレクトリ2に配置する事で達成されます。

このために上のmylib/CMakeLists.txtを編集していきましょう。

(1)インクルードパスの変更

ひとたびパッケージがグローバルにインストールされれば、もはや${CMAKE_CURRENT_SOURCE_DIR}/includeはプロジェクトとは無関係です。(${CMAKE_CURRENT_SOURCE_DIR}は依然としてローカルのmylib/を参照する為。)通常のビルドとインストール後のビルドで異なるインクルードパスを指定するように、target_include_directoriesのパラメータを変更します。

mylib/CMakeLists.txt
# --
# target_include_directories(mylib INTERFACE
# 	${CMAKE_CURRENT_SOURCE_DIR}/include)
# ++
target_include_directories(mylib INTERFACE
	# 通常のビルドで使われるインクルードパス
	$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
	# インストールされたパッケージを利用するとき使われるインクルードパス
	$<INSTALL_INTERFACE:include>)

$<INSTALL_INTERFACE:...>の指定はインストール時にのみ有効になり3${CMAKE_INSTALL_PREFIX}/がプレフィックスとして付与されます。${CMAKE_INSTALL_PREFIX}のデフォルト値は/usr/local(WindowsではC:/Program Files)です。

(2)configの生成

前述した*-config.cmakeファイルを生成します。install(TARGETS ... EXPORT ...)を用いる事で行なえます。 

mylib/CMakeLists.txt
# ++
	# エクスポートしたいターゲットの名前
install(TARGETS mylib
	# 生成するconfigファイルの名前
	EXPORT mylib-config)
	# インクルードパスの指定、前項で指定済なので不要
# 	INCLUDES DESTINATION include)

前項でインストール時のインクルードパスを設定したため、生成されるmylib-config.cmakeには${CMAKE_INSTALL_PREFIX}/includeをインクルードパスに加える設定が記されているでしょう。

(非ヘッダオンリーライブラリの場合)

このとき、もしライブラリがヘッダオンリーで無い場合は、以下のように必要に応じて生成されたライブラリのインストール先等も指定します。(パスには${CMAKE_INSTALL_PREFIX}/が付与されます。)

install(TARGETS mylib2
	EXPORT mylib2-config
	# 静的ライブラリのインストール先
	ARCHIVE DESTINATION lib
	# 共有ライブラリのインストール先
	LIBRARY DESTINATION lib)

(3)configのインストール

前項までのコードからconfigファイルが生成されました。これをfind_packageによって探索されるディレクトリに配置しなければなりませんので、以下のようにします。

mylib/CMakeLists.txt
# ++
	# インストールするconfigの指定、前項で生成したもの
install(EXPORT mylib-config
	# 名前空間の指定
	NAMESPACE mylib::
	# インストール先
	DESTINATION lib/cmake/mylib)

これでmake installによって、${CMAKE_INSTALL_PREFIX}/lib/cmake/mylibmylib-config.cmakeが配置されるようになります。4

NAMESPACEは以下のように、他のパッケージからmylib内のターゲットを参照する際のプレフィックスになります。<package>::とするのがメジャーなようです。

another/CMakeLists.txt
target_link_libraries(your-exe mylib::mylib)

(4)ヘッダファイルのインストール

インクルードパスに${CMAKE_INSTALL_PREFIX}/includeを指定しましたが、実際にそこにヘッダファイルを配置するのは別の設定で行わないといけません。5以下のようにしてディレクトリの中身をコピーします。

mylib/CMakeLists.txt
# ++
	# コピー元、末尾の`/`は要注意
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
	# コピー先
	DESTINATION include)

注意が必要なのはDIRECTORYの指定の末尾の/で、この有無によって結果が変わるようです。DIRECTORYdir/と指定した場合はdir/の中身のファイルやディレクトリが全てDESTINATIONの下にコピーされますが、一方dirと指定した場合は、中身ではなくdir自体がDESTINATIONの下にコピーされます。

尚、install(TARGETS ...)で指定したARCHIVE DESTINATIONLIBRARY DESTINATIONはこのような事をせずとも配置まで自動的に行われます。

(5)NAMESPACEに関するイディオム

大方の作業は以上で完了ですが、使い勝手の為にもうひと手間かける事ができます。
さて、ここで定義したmylibターゲットを外部のCMakeプロジェクトから利用する際、
add_subdirectory(mylib)を使用した場合はもちろんmylibとして、find_package(mylib)を使用した場合は名前空間のプレフィックスが付いてmylib::mylibとして参照できる事になります。
これから紹介するのは、どちらの場合でもmylib::mylibの名前で統一的に参照するためのイディオムです。

方法は至って単純で、mylibのエイリアスとしてmylib::mylibを作成します。

mylib/CMakeLists.txt
add_library(mylib INTERFACE)

# ++
add_library(mylib::mylib ALIAS mylib)

これでadd_subdirectoryを用いた場合にも、mylib::mylibの名前でmylibを参照できるようになりました。

バージョンの互換性の設定

大方の作業は以上で完了ですが、使い勝手の為に(ry
find_packageでは第2パラメータで、要求するパッケージのバージョンを指定する事ができます。

another/CMakeLists.txt
find_package(mylib 0.1.0)

パッケージのバージョンと要求されたバージョンとの互換性を判定するファイル6<package-name>-config-version.cmakeあるいは<PackageName>ConfigVersion.cmakeをインストールに含める事で、この指定に意味を持たせられます。7

典型的なPackage Version Fileは、CMakePackageConfigHelperswrite_basic_package_version_fileというユーティリティを用いて簡単に生成できます。

mylib/CMakeLists.txt
# ++
include(CMakePackageConfigHelpers)

# Package Version Fileの生成
write_basic_package_version_file(
	# 生成するファイルの名前
	${CMAKE_CURRENT_BINARY_DIR}/mylib-config-version.cmake
	# 互換性の判定方法の指定
	COMPATIBILITY SameMajorVersion)
	# バージョンの指定、project(...)で指定しているため省略可能
#	VERSION 0.1)

# Package Version Fileのインストール
	# コピー元
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mylib-config-version.cmake
	# コピー先
	DESTINATION lib/cmake/mylib)

これでmake install時に、${CMAKE_INSTALL_PREFIX}/lib/cmake/mylib/mylib-config-version.cmakeが作られるようになります。write_basic_package_version_fileで指定できるCOMPATIBILITYSameMajorVersionAnyNewerVersionExactVersionの3種類です。

SameMajorVersionでは実在するバージョンが要求するバージョンより新しく(あるいは等しく)、かつSemVerの1桁目が等しい場合に互換性があると判断されます。8雑に例を挙げます。

要求ver 実在ver 互換性
0.1.0 0.1 OK
0.1.0 0.3 OK
0.1 0.1.0 OK
0.1 0.1.5 OK
0.1.5 0.1 NG
1.1 1.3.0 OK
1.1.0 1.3.0 OK
1.5.0 1.3.0 NG
0.3.0 1.3.0 NG

まとめ

今回書いたCMakeLists.txtの全容は以下の通りです。

cmake_minimum_required(VERSION 3.8)

project(mylib VERSION 0.1.0 LANGUAGES CXX)

add_library(mylib INTERFACE)

add_library(mylib::mylib ALIAS mylib)

target_compile_features(mylib INTERFACE cxx_std_11)

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

install(TARGETS mylib
	EXPORT mylib-config)

include(CMakePackageConfigHelpers)

write_basic_package_version_file(
	${CMAKE_CURRENT_BINARY_DIR}/mylib-config-version.cmake
	COMPATIBILITY SameMajorVersion)

install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mylib-config-version.cmake
	DESTINATION lib/cmake/mylib)

install(EXPORT mylib-config
	NAMESPACE mylib::
	DESTINATION lib/cmake/mylib)

install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
	DESTINATION include)

これによって、作成したライブラリをシステムにインストールできるようになりました。

~ $ cd mylib/build
mylib/build $ cmake ..
mylib/build $ sudo make install

mylibは他のプロジェクトのCMakeLists.txtからは以下のように利用できます。

find_package(mylib REQUIRED)

add_executable(your-exe main.cpp)

target_link_libraries(your-exe mylib::mylib)

デフォルトでは/usr/local/...にインストールされるのでCMake管理外のコードからも容易に利用できるはずです。(ライブラリを生成した場合も-lオプション等でリンクできるでしょう。)

~ $ cat main.cpp
#include <mylib/hello_world.hpp>

int main() {
  mylib::hello_world();
}
~ $ g++ main.cpp

インストールされたファイル群のパスは${CMAKE_CURRENT_BINARY_DIR}/install_manifest.txtに記録されます。


参考

  1. めっちゃ余談ですがpoacというrustのcargoっぽいのが開発中らしいです。普及したらいいな。poacpm/poac

  2. find_packageの動作 - Qiita

  3. Generator Expressionsと呼ばれる機構です。CMake: Generator Expressions - Qiita

  4. デフォルトでは/usr/local/...に配置される事になります。find_package$PATHに含まれるディレクトリの末尾の/bin,/sbinを取り除いたディレクトリを探索するというルールに従って見つかります。

  5. install(TARGETS ...)にはPUBLIC_HEADER DESTINATIONなるパラメータがありますが、これはtarget_include_directoriesの指定とは無関係です。cmake-properties(7) » PUBLIC_HEADER

  6. Package Version Fileと呼ぶ。cmake-packages(7)

  7. Package Version Fileが無い状態でバージョンが要求されるとfind_packageは失敗します。

  8. 1桁目が0の時だけ特殊な処理が行われるというような事は無いようです。

51
38
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
38

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?