Zig のビルドシステムで compile_commands.json を生成する

/ , , ,
簡体中文日本語
要約:
zig build は compile_commands.json を自動生成しないので、既存のモジュールを Zig 0.13 に対応させ、ビルドステップで生成できるようにした。

Zig 標準のビルドシステムは、C のプロジェクトにも使える。ただ、エディタの clangd が各ファイルに必要なヘッダーパスやコンパイルオプションを把握できず、コードのあちこちに赤線が出て、定義へのジャンプも使えなくなる。

clangd が参照するのは、プロジェクト内の compile_commands.json だ。CMake なら直接出力できるが、zig build にはそのための標準ステップがない。

幸い、ビルドグラフから必要な情報を取り出し、compile_commands.json を生成するモジュールを作った人がいた。ただ、古い Zig の API を使っていて、私が使っている 0.13 までの間にビルドシステムの API がかなり変わっていたため、そのままではコンパイルできなかった。そこで、それをベースに対応版を作った。リポジトリは xxxbrian/zig-compile-commands にある。

使い方

まず、build.zig.zon に依存関係として追加する。特定のコミットの tarball URL と、それに対応するハッシュを指定する。以下の <commit> と <hash> は実際の値に置き換える。

ZIG
.dependencies = .{
    .compile_commands = .{
        .url = "https://github.com/xxxbrian/zig-compile-commands/archive/<commit>.tar.gz",
        .hash = "<hash>",
    },
},

次に build.zig で読み込み、出力対象のターゲットをリストにまとめ、最後に createStep を呼ぶ。以下は組み込み方の例で、C ソースファイル、ヘッダーディレクトリ、リンクするライブラリはプロジェクトに合わせて追加する。

ZIG
const std = @import("std");
const zcc = @import("compile_commands");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    var targets = std.ArrayList(*std.Build.Step.Compile).init(b.allocator);

    const exe = b.addExecutable(.{
        .name = "app",
        .target = target,
        .optimize = optimize,
    });
    // ... 添加 C 源文件、头文件目录、链接库
    targets.append(exe) catch @panic("OOM");

    zcc.createStep(b, "zcc", .{
        .targets = targets.toOwnedSlice() catch @panic("OOM"),
    });
}

これで zcc というステップが追加される。zig build zcc を実行するとプロジェクトのルートに compile_commands.json が生成され、clangd が自動で見つけてくれる。

生成の仕組み

このモジュールが行うのは、ビルドグラフ内の C に関する情報を、clang のコマンドライン引数に変換することだ。

渡された各ターゲットについて、その link_objects をたどる。C ソースファイルがあれば、ファイルパスと個別に設定されたコンパイルオプションを記録する。システムライブラリは -l で始まる引数に変換し、libc や libc++ をリンクしていれば、それぞれ -lc、-lc++ も追加する。ヘッダーディレクトリは -I 引数に変換し、別のターゲットがインストールするヘッダーのディレクトリも含める。また、他のターゲットをリンクしていれば、それもリストに追加してたどるので、最上位の実行ファイルのターゲットだけ渡しても、必要な情報を一通り集められる。

各 C ソースファイルは、最終的に compile_commands.json の 1 エントリになる。作業ディレクトリ、ファイルパス、出力パスと、引数の一覧が入る。clangd が理解するのは clang のコマンドラインなので、引数は clang の呼び出しとして記述する。

Important

createStep は、関係するすべてのターゲットの設定を終えてから呼ぶ。モジュールは呼び出し時にビルドグラフを読むため、設定途中のターゲットがあると、そのヘッダーディレクトリやソースファイルを取りこぼしてしまう。

もう一点、生成ステップの実行時には、createStep の呼び出し時に渡された引数を直接参照できない。そのため、モジュールではターゲットのリストをグローバル変数に保存し、後のステップから読めるようにしている。

Zig 0.13 への対応

古い API から 0.13 までの主な変更点は、次のとおり。

  • std.Build.CompileStep が std.Build.Step.Compile になった。
  • ヘッダーディレクトリやリンクオブジェクトなどは、以前はコンパイルステップに直接置かれていたが、今は root_module に移っている。そのため、compile.root_module.include_dirs と compile.root_module.link_objects から読む必要がある。
  • インストール対象のヘッダーは、以前はファイルだけだったが、ファイルとディレクトリの 2 種類に分かれ、それぞれ処理する必要がある。
  • CSourceFiles に root フィールドが追加され、ファイルのルートディレクトリを指定するようになった。
  • ビルドステップの makeFn で、進捗ノードの型がポインタから値に変わった。

最後の変更だけは少し違う方法で対応した。@import("builtin").zig_version をコンパイル時に調べ、バージョンに応じてポインタか値を選んでいる。

バージョンの固定

Zig の依存関係はハッシュで固定するので、README の URL は特定のコミットを指し、そのハッシュも載せる必要がある。新しいコミットに更新するたびに両方を書き換えるのは面倒なので、GitHub Actions のワークフローを作った。master へのプッシュごとに最新のコミットを取得し、zig fetch でハッシュを計算して、README の URL とハッシュを置き換える。これで、README にある依存関係の URL とハッシュをそのままコピーして使える。

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...