bazelで自ライブラリのヘッダをprefixなしで使用する

プログラミングBazel,C/C++

問題設定

以下のような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つの方法が思いついた。

  1. cc_librarycopts-Iinclude を渡す
  2. cc_libraryincludesinclude を指定する
  3. 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 以外のファイルを公開せずに済む。

notes

  1. この謎多きディレクトリ構造はフィクションである。現実の団体や人物にはそれほど関係していない
  2. 2回目のライブラリ化で linkstatic=True を指定しないと warning が発生する

    WARNING: /tmp/BUILD:14:11: in linkstatic attribute of cc_library rule //:mylib: setting 'linkstatic=1' is recommended if there are no object files