🔨

Bazel から pkg-config を使う

2022/07/19に公開

初稿: 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 (折りたたまれています) をファイルとして保存する
WORKSPACE.bazel
load("//:pkg_config_repository.bzl", "pkg_config_repository")

pkg_config_repository(
  name = "glib",
  packages = ["glib2.0", "gobject-2.0"],
)
BUILD.bazel
cc_library(
  name = "glib_library",
  srcs = ["glib_library.cc"],
  …
  deps = ["@glib//:glib"],
) 
pkg_config_repository.bzl
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 に次のようにルールを記述します。

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 のビルドルールを使用します。

WORKSPACE.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 は次のように書きます。

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 との連携もそのひとつなのですが、解決方法の参考になりましたらうれしいです。

GitHubで編集を提案

Discussion