[C++][CMake]複数のコンポーネントから成るパッケージを作りたい時。

動機

多数のライブラリを含む自作のパッケージを、find_packageで他のプロジェクトからコンポーネント単位で利用できるようにしたかった。
つまり例えば次のように、ある自作のパッケージ(mylib)が複数のライブラリ(lib1、lib2、lib3)を持っており、その中からlib1とlib3だけ利用したい、というような状況だ。

find_package(mylib COMPONENTS lib1 lib3 REQUIRED)
target_link_library(foo mylib::lib1 mylib::lib3)

mylibConfig.cmakeのようなファイルを自動生成する方法は、単一のライブラリの場合は随所に日本語の解説があるので何とかなったが、しかし上のようにコンポーネント毎にfind_packageを機能させる方法がわからなかった。数日間あちこち調べ回って、先程ようやく動くようになったところだ。プロフェッショナルな方々からすれば常識なのかも知れないが、初心者にとっては非常に難解だった。日本語解説が見当たらなかったので、折角だし纏めておく。

CMakeLists.txtの書き方

まずmylibが次のようなディレクトリ構造になっているものとする。

mylib/
    lib1/
        lib1.h
        lib1.cpp
        CMakeLists.txt
    lib2/
        lib2.h
        lib2.cpp
        CMakeLists.txt
    lib3/
        lib3.h
        lib3.cpp
        CMakeLists.txt
    CMakeLists.txt
    Config.cmake.in #このファイルの役目は後ほど解説する。

これをインストールしたとき、次のようなディレクトリ構造になっていてほしい。

include/
    mylib/
        lib1/
            lib1.h
        lib2/
            lib2.h
        lib3/
            lib3.h
lib/
    {BUILD_TYPE}/ #BUILD_TYPEはDebug、Releaseなど。
        lib1.lib
        lib2.lib
        lib3.lib
cmake/
    mylibConfig.cmake 他

以上を前提として、まずはそれぞれのディレクトリにあるCMakeLists.txtについて詳しく見ていく。

mylib/lib1/CMakeLists.txt
#mylib/lib1/CMakeLists.txt lib2やlib3でもほぼ同様。

project(lib1)

add_library(lib1 lib1.cpp)
add_library(mylib::lib1 ALIAS lib1)

set_target_properties(
    lib1
    PROPERTIES
        PUBLIC_HEADER "lib1.h")

install(
    TARGETS lib1
    EXPORT mylibTargets
    INCLUDES DESTINATION include
    PUBLIC_HEADER DESTINATION include/mylib/lib1
    ARCHIVE DESTINATION lib/${CMAKE_BUILD_TYPE})

installコマンド中でEXPORT mylibTargetsと指定している。lib1~lib3までの各インクルードディレクトリ、ライブラリディレクトリなどの情報は、このmylibTargetsという対象にエクスポートされるらしい。このmylibTargetsは最終的にmylib/CMakeLists.txt中のコマンドによりmylibTargets.cmakeというファイルへと出力される。
なおset_target_propertiesでは、installによってコピーされるべきヘッダファイルを指定している。これがないとinstall(TARGETS)コマンドでPUBLIC_HEADER DESTINATIONを指定しても何もコピーされない。
install(FILE)というコマンドでヘッダファイルを個別にコピーさせても良いのだが、上の書き方のほうが私は好みだ。

mylib/CMakeLists.txt
#mylib/CMakeLists.txt
cmake_minimum_required(VERSION 3.8) #本当に3.8が必要なのかは知らない。

project(mylib)

add_subdirectory(lib1)
add_subdirectory(lib2)
add_subdirectory(lib3)

install(
    EXPORT mylibTargets
    DESTINATION cmake
    NAMESPACE mylib::
    FILE mylibTargets.cmake)

include(CMakePackageConfigHelpers)
configure_package_config_file( 
    "Config.cmake.in" 
    "mylibConfig.cmake"
    INSTALL_DESTINATION cmake)

install(
    FILES "${CMAKE_CURRENT_BINARY_DIR}/mylibConfig.cmake"
    DESTINATION "cmake")

10行目からのinstall(EXPORT mylibTargets...)の部分をまず見てみる。先のmylib/lib1/CMakeLists.txtでエクスポート先をmylibTargetsに指定していたが、これを実際にmylibTargets.cmakeというファイルに出力しているのがこのコマンドのようだ。

16~20行がmylibConfig.cmakeを生成する部分である。まずConfig.cmake.inというファイルを事前に用意しておき、これに基づいてmylibConfig.cmakeを作っているのが、configure_package_config_fileというコマンドだ。Config.cmake.iniの中身は以下の通り。

@PACKAGE_INIT@
include("${CMAKE_CURRENT_LIST_DIR}/mylibTargets.cmake")

22行目のinstall(FILES...)により、mylibConfig.cmakeのコピーを指示している。これで必要な全てのファイルはCMAKE_INSTALL_PREFIXに指定した場所へコピーされる。

最後に

installとfind_package意味わからん。
最近ちょっとRustで遊んでいたのだが、あちらのCargoはすこぶる使いやすかった。C++の場合はどのパッケージを使ってどのソースファイルからビルドして、と一々指定しなければならないのだが、Cargoはそれらの指定が何ら必要ないのだ。単に「このパッケージを使いたい」と一行書くだけで、そのパッケージのダウンロード、インストール、リンクなど全てを自動で行ってくれるのだ。感動的である。
その代わり、クレートやモジュールなどの切り分け方、それらのフォルダ構造などが厳密に指定されており、プログラマの自由を許さない形になっていた。最初はRustの縛りの強さに戸惑ったが、あれはあれでビルド、配布しやすさに全振りしたのだと思えば納得できる仕様だ。

C++の場合、言語自体の仕様が自由すぎるのだ。Rustと違ってプログラマ側が自由に決定できる部分があまりに多いので、CMake側でも指定しなければならないパラメータがとんでもなく多くなる。決してCMakeが悪いわけではない。よく頑張っている。
だが正直、CMakeを他人に勧められないなぁとは思った。ある程度使えるようになったら研究室内でCMakeの布教をしようかと考えていたけれども、やめておくことにした。環境がWindowsだけならVisual Studioのプロジェクトの方が初心者には遥かにとっつきやすい。vcpkgと組み合わせればややこしい設定も殆どいらない。幸い研究室内ではWindowsがデフォルトで、Macは個人が使っているだけ、Linuxはほぼ必要ないので、クロスプラットフォームを視野にいれた大規模開発でもしない限り、敢えてこんな意味不明なビルドツールを理解するコストは支払わなくていいだろう。

Rustが可変長引数ジェネリクス、可変長引数関数、placement newなどを実装してくれたら私も移行できるのだが、まだ当面は実現しそうにない雰囲気だったので、今後数年以上はC++に頼ることになりそうである。Rustが覇権を握る日は来るのだろうか。結局、何でも自由にできるC/C++が生き残ったりするんじゃなかろうか。