Bazel から pkg-config を使う
初稿: 2022-07-18
改訂: 2024-08-15 (Bzlmod へ対応)
小松弘幸 (@komatsuh:bsky, @komatsuh:twitter)
記事の内容
ビルドシステムの Bazel で、pkg-config コマンドから取得できるビルドフラグ等を使えるようにする方法を紹介します。
用語
- Bazel: ビルドシステム。Make や CMake などと同じ役割のもの。この記事は Bazel についてはすでに知っている方むけです。
- pkg-config: ビルドフラグを取得するためのコマンドライン。C++ のヘッダーファイル用インクルードパスやリンクオプション等、開発環境に依存する情報を取得できます。
方法
-
WORKSPACE.bazel
に pkg-config 経由で使用したいパッケージの設定を記述する -
BUILD.bazel
のルールに設定したパッケージを追加する - 下記の
pkg_config_repository.bzl
(折りたたまれています) をファイルとして保存する
load("//:pkg_config_repository.bzl", "pkg_config_repository")
pkg_config_repository(
name = "glib",
packages = ["glib2.0", "gobject-2.0"],
)
cc_library(
name = "glib_library",
srcs = ["glib_library.cc"],
…
deps = ["@glib//:glib"],
)
pkg_config_repository.bzl
BUILD_TEMPLATE = """
package(
default_visibility = ["//visibility:public"],
)
cc_library(
name = "{name}",
hdrs = glob([
{hdrs}
]),
copts = [
{copts}
],
includes = [
{includes}
],
linkopts = [
{linkopts}
],
)
"""
EXPORTS_FILES_TEMPLATE = """
exports_files(glob(["bin/*"]))
"""
def _exec_pkg_config(repo_ctx, flag):
binary = repo_ctx.which("pkg-config")
result = repo_ctx.execute([binary, flag] + repo_ctx.attr.packages)
items = result.stdout.strip().split(" ")
uniq_items = sorted({key: None for key in items}.keys())
return uniq_items
def _make_strlist(list):
return "\"" + "\",\n \"".join(list) + "\""
def _symlinks(repo_ctx, paths):
for path in paths:
if repo_ctx.path(path).exists:
continue
repo_ctx.symlink("/" + path, path)
def _pkg_config_repository_impl(repo_ctx):
includes = _exec_pkg_config(repo_ctx, "--cflags-only-I")
includes = [item[len("-I/"):] for item in includes]
_symlinks(repo_ctx, includes)
data = {
# In bzlmod, repo_ctx.attr.name has a prefix like "_main~_repo_rules~glib".
"name": repo_ctx.attr.name.split("~")[-1],
"hdrs": _make_strlist([item + "/**" for item in includes]),
"copts": _make_strlist(_exec_pkg_config(repo_ctx, "--cflags-only-other")),
"includes": _make_strlist(includes),
"linkopts": _make_strlist(_exec_pkg_config(repo_ctx, "--libs-only-l")),
}
build_file_data = BUILD_TEMPLATE.format(**data)
# host_bins
host_bins = _exec_pkg_config(repo_ctx, "--variable=host_bins")
if len(host_bins) == 1:
repo_ctx.symlink(host_bins[0], "bin")
build_file_data += EXPORTS_FILES_TEMPLATE
repo_ctx.file("BUILD.bazel", build_file_data)
pkg_config_repository = repository_rule(
implementation = _pkg_config_repository_impl,
attrs = {
"packages": attr.string_list(),
},
)
Bzlmod への対応
Bzlmod は Bazel 7 以降から標準で採用された、依存関係を記述する仕組みです。従来の WORKSPACE.bazel ファイルを使う代わりに MODULE.bazel を使用します。
pkg-config を Bzlmod で使用する場合は WORKSPACE.bazel の代わりに、MODULE.bazel に次のようにルールを記述します。
pkg_config_repository = use_repo_rule("@//bazel:pkg_config_repository.bzl", "pkg_config_repository")
pkg_config_repository(
name = "glib",
packages = ["glib2.0", "gobject-2.0"],
)
実際の使用例
動作の説明
動作を理解しやすくするために、まずは pkg-config を使わない方法を説明します。こまかい内容ですので必要なければ飛ばしても大丈夫です。
pkg-config を直接使わないで Bazel から外部ライブラリを使う方法
Bazel では、使用する外部ライブラリのヘッダーファイルやソースファイルを『レポジトリ』という概念で管理します。これは変更されうるすべてのファイルを管理することで、ビルドの一貫性を確保するためです。
開発マシンのローカルな環境にインストールされたライブラリの場合、new_local_repository
という Bazel のビルドルールを使用します。
new_local_repository(
name = "qt",
build_file = "BUILD.qt.bazel",
path = "/usr/include/x86_64-linux-gnu/qt5",
)
- new_local_repository の name に従って、
@qt//:
がこのレポジトリを示す名前になります。 - new_local_repository の path が Bazel から管理されるようになります。具体的には path へのシンボリックリンクが Bazel の作業用ディレクトリ内 (bazel-src/external/) に作成されます。
- new_local_repository の build_file がこのレポジトリ用のビルドファイルです。
BUILD.qt.bazel
は次のように書きます。
cc_library(
name = "qt",
hdrs = glob([
"QtCore/**",
"QtGui/**",
"QtWidgets/**",
]),
includes = [
".",
"QtCore",
"QtGui",
"QtWidgets",
],
linkopts = [
"-lQt5Core",
"-lQt5Gui",
"-lQt5Widgets",
],
)
hdrs および includes に書かれてるパスは new_local_repository.path (つまり "/usr/include/x86_64-linux-gnu/qt5") からの相対パスです。
cc_library の詳細についてはリファレンスを参照してください。
上記のファイルパスやコンパイルオプションは、pkg-config を事前に実行して取得しています。
% pkg-config --cflags-only-I Qt5Core Qt5Gui Qt5Widgets
-I/usr/include/x86_64-linux-gnu/qt5/QtWidgets -I/usr/include/x86_64-linux-gnu/qt5 -I/usr/include/x86_64-linux-gnu/qt5/QtGui -I/usr/include/x86_64-linux-gnu/qt5 -I/usr/include/x86_64-linux-gnu/qt5/QtCore -I/usr/include/x86_64-linux-gnu/qt5
% pkg-config --libs-only-l Qt5Core Qt5Gui Qt5Widgets
-lQt5Widgets -lQt5Gui -lQt5Core
pkg-config を Bazel から使う方法
上記の new_local_repository.path と BUILD.qt.bazel の内容を含んだ pkg-config から取得し、その内容を収めた Bazel のレポジトリを動的に作成します。
Bazel のレポジトリを動的に作成するには、repository_rule という仕組みを拡張して独自ルールを作成します。この文書の先頭の pkg_config_repository.bzl
のことです。
pkg_config_repository.bzl
ではおもに以下のことをしています。
- インクルードパスへのシンボリックリンクを作成する。
- pkg-config から得たビルドオプションをビルドルールに反映させる。
インクルードパスへのシンボリックリンクの作成
pkg-coinfig に --cflags-only-I
オプションをつけると、インクルードパスへの情報が得られます。
そして、得られたパスのルートからのシンボリックリンクを Bazel の内部に作成します。
たとえば、つぎの 2 つのパスの場合を考えます。
-I/usr/include/x86_64-linux-gnu/qt5/QtCore
-I/usr/include/x86_64-linux-gnu/qt5/QtGui
この場合は、内部ディレクトリから下記のコマンドと同様の操作を行います。
ln -s /usr/include/x86_64-linux-gnu/qt5/QtCore usr/include/x86_64-linux-gnu/qt5/QtCore
ln -s /usr/include/x86_64-linux-gnu/qt5/QtGui usr/include/x86_64-linux-gnu/qt5/QtGui
そして、インクルードパスから先頭の /
を除いた、下記のものを Bazel 内で使用します。
-Iusr/include/x86_64-linux-gnu/qt5/QtCore
-Iusr/include/x86_64-linux-gnu/qt5/QtGui
pkg-config からのビルドオプションをビルドルールへ反映
pkg-config から必要な情報のみを得られるように、コマンドラインオプションを指定して、Bazel のルールの値に反映させます。
pkg-config のオプション | Bazel のルールの値 |
---|---|
--cflags-only-I | hdrs, includes |
--cflags-only-other | copts |
--libs-only-l | linkopts |
ライブラリ用バイナリの反映
ライブラリによっては、ツール用バイナリを含んでいるものもあります。たとえば Qt の場合、uic や rcc などのバイナリがあります。
このバイナリ用のパスは pkg-config に --variable=host_bins
を指定すると得られます。
このパスへのシンボリックリンクを Bazel 内部の bin/ (つまり bazel-src/external/qt/bin) として作成することで、//@qt:bin/uic
として参照できるようにします。
Bzlmod への対応
pkg_config_repository.bzl を WORKSPACE から呼ぶ場合と比べて、repo_ctx.attr.name
の内容が異なります。
WORKSPACE の場合は、WORKSPACE 内で記述した name
の値がそのまま入ります。上記の例の場合では glib
がその値です。対して Bzlmode の場合は、階層構造を表す値が先頭に追加されます。上記の例では _main~_repo_rules~glib
となります。
WORKSPACE との互換性のために、最後の `~' 以降の文字列のみを値として扱うようにしています。
注意点
- 筆者が pkg-config で実際に必要になった機能のみに対応しています。
-
--cflags-only-I
,--cflags-only-other
,--libs-only-l
,--variable=host_bins
以外のオプションには対応していません。 -
--variable=host_bins
が複数のパスを返す場合や、bin/
ディレクトリが他のオプションと競合する場合には対応していません。
まとめ
ビルドシステムの Bazel で pkg-config コマンドを組み込む方法を紹介しました。
Bazel は比較的大掛かりなビルドシステムで、かゆいところに手が届きにくい面があると思います。
pkg-config との連携もそのひとつなのですが、解決方法の参考になりましたらうれしいです。
Discussion