概要
CMakeを用いて、C/C++製のライブラリをインストール可能にし、別のCMakeプロジェクトやCMake管理外のC/C++コードから容易に利用できるようにするような方法の紹介です。皆のライブラリにmake install
ターゲットを追加しましょう。
背景だったものポエム
ポエムになった。ここは読み飛ばして構いません。C++製のプロジェクトのビルドツールとしてはCMakeがデファクトスタンダードの一つとしての位置を占めているのではないかと思います。しかし、その独特で自由度の高い表現からややとっつきにくく、また、パッケージマネージャ的な要素の薄さ1から、C++でライブラリを製作しても、それを他のプロジェクトに向け公開する事は少しハードルが上がるように思われます。せっかく製作された素晴らしいライブラリが、他のプロジェクトから利用しづらく、日の目を見る事ができないのは良い事ではないです。また、ライブラリの充実度は言語の特徴を決める重要なファクターになるでしょう。この投稿が多少でもライブラリの製作者及びユーザの助けになれれば幸いです。
やっていきましょう
例として以下のようなヘッダオンリーのプロジェクトを扱います。
mylib/
├── CMakeLists.txt
└── include
└── mylib
└── hello_world.hpp
簡単な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
内で定義されたターゲットを利用できるようになります。
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
のパラメータを変更します。
# --
# 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 ...)
を用いる事で行なえます。
# ++
# エクスポートしたいターゲットの名前
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
によって探索されるディレクトリに配置しなければなりませんので、以下のようにします。
# ++
# インストールするconfigの指定、前項で生成したもの
install(EXPORT mylib-config
# 名前空間の指定
NAMESPACE mylib::
# インストール先
DESTINATION lib/cmake/mylib)
これでmake install
によって、${CMAKE_INSTALL_PREFIX}/lib/cmake/mylib
にmylib-config.cmake
が配置されるようになります。4
NAMESPACE
は以下のように、他のパッケージからmylib内のターゲットを参照する際のプレフィックスになります。<package>::
とするのがメジャーなようです。
target_link_libraries(your-exe mylib::mylib)
(4)ヘッダファイルのインストール
インクルードパスに${CMAKE_INSTALL_PREFIX}/include
を指定しましたが、実際にそこにヘッダファイルを配置するのは別の設定で行わないといけません。5以下のようにしてディレクトリの中身をコピーします。
# ++
# コピー元、末尾の`/`は要注意
install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/include/
# コピー先
DESTINATION include)
注意が必要なのはDIRECTORY
の指定の末尾の/
で、この有無によって結果が変わるようです。DIRECTORY
にdir/
と指定した場合はdir/
の中身のファイルやディレクトリが全てDESTINATION
の下にコピーされますが、一方dir
と指定した場合は、中身ではなくdir
自体がDESTINATION
の下にコピーされます。
尚、install(TARGETS ...)
で指定したARCHIVE DESTINATION
やLIBRARY DESTINATION
はこのような事をせずとも配置まで自動的に行われます。
(5)NAMESPACE
に関するイディオム
大方の作業は以上で完了ですが、使い勝手の為にもうひと手間かける事ができます。
さて、ここで定義したmylib
ターゲットを外部のCMakeプロジェクトから利用する際、
add_subdirectory(mylib)
を使用した場合はもちろんmylib
として、find_package(mylib)
を使用した場合は名前空間のプレフィックスが付いてmylib::mylib
として参照できる事になります。
これから紹介するのは、どちらの場合でもmylib::mylib
の名前で統一的に参照するためのイディオムです。
方法は至って単純で、mylib
のエイリアスとしてmylib::mylib
を作成します。
add_library(mylib INTERFACE)
# ++
add_library(mylib::mylib ALIAS mylib)
これでadd_subdirectory
を用いた場合にも、mylib::mylib
の名前でmylib
を参照できるようになりました。
バージョンの互換性の設定
大方の作業は以上で完了ですが、使い勝手の為に(ry
find_package
では第2パラメータで、要求するパッケージのバージョンを指定する事ができます。
find_package(mylib 0.1.0)
パッケージのバージョンと要求されたバージョンとの互換性を判定するファイル6、<package-name>-config-version.cmake
あるいは<PackageName>ConfigVersion.cmake
をインストールに含める事で、この指定に意味を持たせられます。7
典型的なPackage Version Fileは、CMakePackageConfigHelpers
のwrite_basic_package_version_file
というユーティリティを用いて簡単に生成できます。
# ++
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
で指定できるCOMPATIBILITY
はSameMajorVersion
、AnyNewerVersion
、ExactVersion
の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
に記録されます。
参考
- Installing - Modern CMake - https://cliutils.gitlab.io/modern-cmake/chapters/install/installing.html
- CMakeスクリプトを作成する際のガイドライン - Qiita - https://qiita.com/shohirose/items/5b406f060cd5557814e9
- hana/CMakeLists.txt - boostorg/hana - https://github.com/boostorg/hana/blob/master/CMakeLists.txt
- Modern CMake for Library Developers - unclejimbo's site - https://unclejimbo.github.io/2018/06/08/Modern-CMake-for-Library-Developers/
- Effective CMake - C++Now 2017 - https://github.com/boostcon/cppnow_presentations_2017/blob/master/05-19-2017_friday/effective_cmake__daniel_pfeifer__cppnow_05-19-2017.pdf
- It's Time To Do CMake Right - Pablo Arias - https://pabloariasal.github.io/2018/02/19/its-time-to-do-cmake-right/
- find_packageの動作 - Qiita - https://qiita.com/osamu0329/items/bd3d1e50edf37c277fa9
-
めっちゃ余談ですがpoacというrustのcargoっぽいのが開発中らしいです。普及したらいいな。poacpm/poac ↩
-
Generator Expressionsと呼ばれる機構です。CMake: Generator Expressions - Qiita ↩
-
デフォルトでは
/usr/local/...
に配置される事になります。find_package
の$PATH
に含まれるディレクトリの末尾の/bin
,/sbin
を取り除いたディレクトリを探索するというルールに従って見つかります。 ↩ -
install(TARGETS ...)
にはPUBLIC_HEADER DESTINATION
なるパラメータがありますが、これはtarget_include_directories
の指定とは無関係です。cmake-properties(7) » PUBLIC_HEADER ↩ -
Package Version Fileと呼ぶ。cmake-packages(7) ↩
-
Package Version Fileが無い状態でバージョンが要求されると
find_package
は失敗します。 ↩ -
1桁目が0の時だけ特殊な処理が行われるというような事は無いようです。 ↩