bazelで自ライブラリのヘッダをprefixなしで使用する
問題設定
以下のようなBazelを用いて管理されたC++ライブラリを考える。
. ├── include │ ├── public.hpp │ └── internal.hpp ├── src │ ├── public.cpp │ └── internal.cpp └── BUILD.bazel
ヘッダファイルは include
以下、ソースファイルは src
以下にそれぞれ格納され、ディレクトリ全体としては静的ライブラリを提供する。2つのヘッダファイルのうち、ライブラリ使用者に公開するヘッダは include/public.hpp
のみで、他方のヘッダは外からは使わせないものとする。
ライブラリ内部から include
以下のヘッダファイルにアクセスするときに余計なプレフィックスなしでアクセスできるようにしたい。同時に、外部に公開する include/public.hpp
は他ライブラリとの干渉を避けるために mylib/public.hpp
というパスで外部に公開したい状況を考える1。
ライブラリ内部 | ライブラリ外部 | |
---|---|---|
public.hpp | “public.hpp" | “mylib/public.hpp" |
internal.hpp | “internal.hpp" | -(公開しない) |
このような要求を実現する方法として、Bazel初心者の僕は、以下の3つの方法が思いついた。
cc_library
のcopts
で-Iinclude
を渡すcc_library
のincludes
にinclude
を指定するcc_library
を2つに分け、内部向けライブラリでパスのつけ外しを行う
結論を先に言うと、1と2はあまりおすすめしない。面倒でも3.の方法でライブラリを作成するべきである。その理由について1つずつ詳しく見ていく。
1. copts=["-Iinclude"]
1つ目は、コンパイラに直接オプションを渡してインクルードパスを追加する方法である。
cc_library( name = "mylib", srcs = [ "include/internal.hpp", "src/internal.cpp", "src/public.cpp", ], hdrs = [ "include/public.hpp", ], include_prefix = "mylib", strip_include_prefix = "include", copts = ["-Iinclude"], )
こうすることで、ライブラリ内部からは include/
なしで、ライブラリ外部からは mylib/public.hpp
のような形でアクセスできると考えるかもしれない。 ディレクトリ構造によっては偶然うまくいく可能性もあるが、外部リポジトリから参照するときに確実にビルドエラーになる。-Iinclude
はあくまでコンパイラに直接渡されるオプションのため、作業ディレクトリの位置によっては正しくパスを通すことができない。
2. includes=["include"]
Bazelの公式ドキュメントを漁ると、includes
オプションが見つかる。名前の響きからしてこれが使えそうに見えるかもしれない。
cc_library( name = "mylib", srcs = [ "src/public.cpp", "include/internal.hpp", "src/internal.cpp", ], hdrs = [ "include/public.hpp", ], include_prefix = "mylib", strip_include_prefix = "include", includes = ["include"], )
この方法は copts
のときとは異なり、環境依存の方法ではない。Bazelがパスの解決を行ってくれるので、外部リポジトリとして参照された場合でも問題なくビルドが通る。
一見するとこの方法で問題なさそうに見えるかもしれないが、残念ながら致命的な欠陥が2つもある。includes
はかなり癖が強いオプションなので、Bazelに慣れないうちはあまり手を出さない方がよい。
欠点1. 見せたくないヘッダも公開してしまう
まず、上記のような BUILD.bazel を書くと、ユーザーから public.hpp
が(prefixなしで)アクセスできてしまう。それだけでなく、internal.hpp
もまたprefixなしでアクセス可能になってしまう。
// user.cpp #include "mylib/public.hpp" // OK 普通の使い方 #include "public.hpp" // OK(!) prefixなしでアクセスできる #include "internal.hpp" // OK(!) 意図しないヘッダが使える
このように、hdrs
に渡していないファイルも使える状態になってしまう。このように、includes
を使って解決する方法ではBazelの哲学に真っ向から反するライブラリになってしまう。
欠点2. カバレッジ計測から除外されてしまう
公式ドキュメントをよく読むと、includes
に指定したディレクトリは -isystem
オプションによりコンパイラに渡される。つまり、普通のヘッダではなくシステムヘッダとして扱われる。システムヘッダは通常、カバレッジ計測から除外されるので、カバレッジが正しく計測できなくなる。これが2つ目の致命的な欠点である。
3. cc_library
2段重ね
今回のケースでは、内部用の cc_library()
と外部公開用の cc_library()
の2段階に分けてビルドする必要がある。
cc_library( name = "mylib_internal", srcs = [ "src/internal.cpp", "src/public.cpp", ], hdrs = [ "include/internal.hpp", "include/public.hpp", ], strip_include_prefix = "include", ) cc_library( name = "mylib", srcs = [ ":mylib_internal" ], hdrs = [ "include/public.hpp", ], include_prefix = "mylib", strip_include_prefix = "include", linkstatic = True, )
1段階目のライブラリ化で include
のstripと内部ファイルのビルドを行う。その後、できたライブラリを srcs
に渡して外部公開用のライブラリをビルドする2。こうすることで、ライブラリ使用者に include/public.hpp
以外のファイルを公開せずに済む。