CMakeのクロスコンパイルでfind_packageがホストを見に行くときの設定

CMakeでクロスコンパイルしていると、find_package() が思った場所を見てくれないことがあります。

ターゲット用のライブラリを探してほしいのに、ホスト側の /usr/lib/usr/local を見に行く。逆に、ビルド中に実行するホスト側ツールまでsysroot内から探そうとする。どちらもクロスコンパイルではよく出る問題です。

このあたりは CMAKE_FIND_ROOT_PATH_MODE_* で制御します。

公式のtoolchainドキュメントにも、クロスコンパイル時の典型例として設定が載っています。

https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling-for-linux

前提となる toolchain file は CMakeでクロスコンパイルするときのtoolchain fileの書き方 に、sysrootの考え方は CMakeのクロスコンパイルでsysrootを設定する理由 に分けています。

典型的な設定

toolchain fileに以下のように書きます。

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

ざっくり言うと、こういう考え方です。

変数 探すもの よくある設定
CMAKE_FIND_ROOT_PATH_MODE_PROGRAM ビルド中に実行するプログラム NEVER
CMAKE_FIND_ROOT_PATH_MODE_LIBRARY リンクするライブラリ ONLY
CMAKE_FIND_ROOT_PATH_MODE_INCLUDE includeするヘッダー ONLY
CMAKE_FIND_ROOT_PATH_MODE_PACKAGE find_package() のパッケージ ONLY

ビルド中に実行するプログラムはホスト側のものを使います。

一方で、リンクするライブラリやincludeするヘッダーはターゲット側のものを使います。

PROGRAMだけNEVERにする理由

たとえばビルド中に protoc やコード生成ツールを実行する場合、そのツールはホスト上で動く必要があります。

ターゲットがARM Linuxで、ホストがx86_64 Linuxなら、ARM向けの protoc を見つけてもホストでは実行できません。

そのため、プログラム探索はホスト側を見るようにします。

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

この変数の説明は公式にもあります。

https://cmake.org/cmake/help/latest/variable/CMAKE_FIND_ROOT_PATH_MODE_PROGRAM.html

ライブラリとヘッダーはONLYにする

リンクするライブラリとincludeするヘッダーはターゲット側のものが必要です。

set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

ライブラリの探索モードは CMAKE_FIND_ROOT_PATH_MODE_LIBRARY で制御します。

https://cmake.org/cmake/help/latest/variable/CMAKE_FIND_ROOT_PATH_MODE_LIBRARY.html

ここが曖昧だと、ホスト側のライブラリを拾ってしまうことがあります。

コンパイルは進んでもリンクでアーキテクチャ違いのエラーが出る場合は、このあたりを疑います。

find_packageもターゲット側を見る

find_package() は、ライブラリ本体だけでなく xxxConfig.cmake などのパッケージ設定ファイルを探します。

クロスコンパイル時にターゲット用のパッケージ設定を使いたいなら、次のようにします。

set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

公式変数はこちらです。

https://cmake.org/cmake/help/latest/variable/CMAKE_FIND_ROOT_PATH_MODE_PACKAGE.html

たとえばsysroot内に次のようなファイルがある場合、そこを見てほしいということです。

/opt/sysroots/arm-linux/usr/lib/cmake/ZLIB/ZLIBConfig.cmake

ホスト側に入っているZLIBを拾うと、ターゲット向けのビルドとしては崩れます。

toolchain fileの例

まとめると、toolchain fileはこのような形になります。

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_SYSROOT /opt/sysroots/arm-linux)

set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)

この設定で、プログラムはホスト側、ライブラリ・ヘッダー・パッケージはターゲット側を優先する形になります。

うまくいかないときに見る場所

find_package() がどこを見ているか分からない場合は、まずCMakeのログを増やします。

cmake -S . -B build-arm \
  --toolchain toolchains/arm-linux.cmake \
  --debug-find

--debug-find を付けると、find系コマンドがどこを探しているか追いやすくなります。

ホスト側の /usr/lib を見ているのか、sysroot内を見ているのかを確認します。

外部ライブラリをそもそも find_package() で扱うか FetchContent で取得するか迷う場合は、CMakeでFetchContentとfind_packageを使い分ける目安 も近い内容です。

まとめ

クロスコンパイルで find_package() が変な場所を見るときは、探索対象を分けて考えます。

  • ビルド中に実行するプログラムはホスト側
  • リンクするライブラリはターゲット側
  • includeするヘッダーはターゲット側
  • find_package() の設定ファイルもターゲット側

toolchain fileでは、CMAKE_FIND_ROOT_PATH_MODE_* をまとめて設定しておくと、後から原因を追いやすくなります。

参考