ククログ

株式会社クリアコード > ククログ > Apache ArrowとCMake - FetchContent

Apache ArrowとCMake - FetchContent

Apache Arrowの開発に参加している須藤です。現時点でapache/arrowのコミット数は1位です。私はRubyでデータ処理できるようになるといいなぁと思ってApache Arrowの開発に参加し始めました。同じような人が増えるといいなぁと思うので、最近の活動を紹介して仲間を増やそうと試みます。

今回はApache Arrow C++で使っているビルドツールCMakeFetchContentという機能を紹介します。

背景

Apache Arrow C++は開発当初からビルドツールとしてCMakeを使っています。最近はMesonでも(一部の機能は)ビルドできるようになっています。世の中にはビルドシステムをいじるのも好き(苦にならない)という人がそんなにいないようで、ビルドシステムをいじるのも好きな私が結構メンテナンスしています。

今回はCMakeの機能の中でも依存ライブラリーの管理に使えるFetchContentに注目します。最近は、似たような機能であるExternalProjectからFetchContentに移行しています。たとえば、私は最近はAWS SDK for C++の管理をExternalProjectからFetchContentに移行しています。

Apache Arrow C++はたくさんのライブラリーに依存しています。すべての依存はオプションになっているので、最小限の機能でビルドするときは気にしなくてよいですが、普通はそれなりに機能を有効をしてビルドするので気にしなければいけません。

普通は依存しているライブラリーを事前にインストールしておかなければいけません。システムのパッケージマネージャー(Debianならaptとか)でインストールできるものなら比較的簡単に準備できますが、そうでないものを準備することは面倒です。ソースをダウンロードしてビルドしてインストールしないといけません。ということで、デフォルトではシステムにある場合はそれを使って、ない場合は自動でソースをダウンロードしてビルドするようになっています。

それを便利にするための機能がFetchContentやExternalProjectです。ちなみに、MesonにもSubprojectsという似たような機能があります。

CMakeで依存ライブラリーをビルドする方法

CMakeで依存ライブラリーをビルドする方法はいくつかあります。

もし、依存ライブラリーもビルドツールとしてCMakeを使っている場合は、依存ライブラリーをサブディレクトリーに入れてadd_subdirectory(library1)とかとすれば一緒にビルドできます。お手軽ですが、ソースを準備するところを自分でがんばらないといけないところと、CMake以外のビルドツールを使っているとき(たとえばGNU Autotoolsなど)には使えないところなどがモヤッとポイントです。

ExternalProjectを使うと、そこらへんのモヤッとポイントを解決できます。ソースのURLを指定すれば自動でダウンロードしてくれます。パッチを当てることもできます。(patchコマンドがポータブルにどこにでもあるわけではないので私はできるだけ使わないようにはしていますけど。。。)CMake以外のビルドツールを使っていてもビルドできます。

そんな便利そうなExternalProjectですが、完璧ではありません。本体(ここではApache Arrow C++)のとは分離してビルドするので、ビルド結果を本体で使うためにたくさんゴニョゴニョしないといけません。簡単なものだと次のようにIMPORTEDライブラリーを作ってinclude用のパスとビルドされるライブラリーのパスを設定するだけです。

externalproject_add(zstd_ep ...)
add_library(zstd::libzstd_static STATIC IMPORTED)
set_target_properties(zstd::libzstd_static
                      PROPERTIES
                      IMPORTED_LOCATION "../lib/libzstd.a")
target_include_directories(zstd::libzstd_static INTERFACE ".../include")
add_dependencies(zstd::libzstd_static zstd_ep)

このくらいならまだそんなもんかなという気持ちになりますが、AbseilAWS SDK for C++をサポートしようとするとそんな気持ちは吹っ飛びます。これらのライブラリーは小さなプロダクトに分かれているため、これと同じようなことを10回以上やらないといけません。しかも、各ライブラリーの依存関係も設定してあげないといけません。

そこでFetchContentです。FetchContentはadd_subdirectory()とExternalProjectの間みたいなポジションです。ExternalProjectのようにソースのダウンロードはやりますが、ビルドはadd_subdirectory()です。そのため、CMakeを使っているプロダクトしか対応していません。が、最近はCMakeが主流になってきているので多くのプロダクトはCMakeに対応しています。よって、最近のプロダクトではこれで困ることは少ないです。

add_subdirectory()スタイルだとなにがうれしいかというと、↑のExternalProjectの例でやっているような、ビルド結果をCMakeが使えるようにまとめる、という作業をする必要がないことです。CMakeは成果物を「ターゲット」というものにまとめて、ターゲット単位でもろもろ便利に処理することができるようになっています。↑の例では「zstd::libzstd_static」がターゲットです。

昔は、「include用のパスやマクロ定義はCFLAGSに文字列として入れて、リンク用のフラグはLDFLAGSに文字列として入れて、それらを組み合わせてビルドする」というような管理をしていました。それだと複数の情報をまとめて管理するのはビルドツールを使う人になります。ターゲットのようにビルドツールがまとめてくれれば、ビルドツールを使う人は「zstd::libzstd_staticにリンクして」と書けます。そう書けば、あとはビルドツールが「zstd::libzstd_staticをリンクするときに必要なincludeパスはこれで、リンク時のフラグはこれで」とかもろもろやってくれます。

なぜExternalProjectを使っていたときのようにターゲットをまとめなくてよいかというと、本体のビルドプロセスに統合されているからです。add_subdirectory()でサブディレクトリー内でされた設定を本体のビルドプロセスでそのまま参照できるのです。

FetchContentのモヤッとポイント

ただ、FetchContentでカンペキかというとそうでもないんですよねぇ。。。

たとえば、option()の挙動がアレです。option()はビルドオプションを設定するための機能で、cmake -DENABLE_XXX=ONのようなことをするために使えます。

CMakeには普通の変数とCACHE変数というのがあって、option()CACHE変数を使っています。CACHE変数はユーザーが設定する値を保持するように設計してあって、いろいろ違いがあります。たとえば、スコープがグローバルです。普通の変数は関数に入ったときとかディレクトリーに入ったときなどでスコープが変わる(つまり、関数やディレクトリー内で変更しても外に出れば元の値に戻る)のですが、CACHE変数はスコープが変わりません。

ビルドプロセスが同じということは、変数も共有しています。依存ライブラリーのビルドオプションを変えるには次のように変数を設定することになります。

fetchcontent_declare(...)
set(BUILD_SHARED_LIBS OFF) # 共有ライブラリーはビルドしない
set(BUILD_STATIC_LIBS ON)  # 静的ライブラリーのみビルドする
fetchcontent_makeavailable(...)

最近の書き方になっているプロダクトではこのように普通の変数で設定できるのですが、そうでないプロダクトではCACHE変数を使わないと設定できません。これは、前はoption()が普通の変数の値を無視するようになっていたからです。CMP0077というポリシーをNEWにすることで普通の変数の値も参照するようにできます。

CACHE変数のスコープはグローバルなので、意図していないところまで変更が波及してしまう可能性があります。普通の変数の場合は、たとえば、次のように関数に入れることで変更を局所化できました。

function(build_xxx)
  fetchcontent_declare(...)
  set(BUILD_SHARED_LIBS OFF) # 共有ライブラリーはビルドしない
  set(BUILD_STATIC_LIBS ON)  # 静的ライブラリーのみビルドする
  fetchcontent_makeavailable(...)
endfunction()
build_function()

しかし、CACHE変数ではそうはなりません。変な影響がでないように慎重に設定しないといけません。

将来的にはNEWの挙動になるのでいずれ解決する話なのですが、最近これで困ったので書いてしまいました。

まとめ

もう少し具体的なことも説明しようと書き始めましたが、概要とモヤッとポイントを書いただけで長くなってしまったので、今日はこのくらいにしておきます。今度、続きを書くかも。。。

それはそうとして、apache/arrowコミット数1位の私にApache Arrow関連のサポートを頼みたいという場合はクリアコードのApache Arrowサービスをどうぞ。