為 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 的介面寫的,而我用的是 0.13,這之間建置系統的介面改了不少,直接用會編譯不過。於是我在它的基礎上改了一版,儲存庫在 xxxbrian/zig-compile-commands。

用法

先在 build.zig.zon 裡把它加為相依套件,網址指向某個提交的 tarball,再填上對應的雜湊值。下面的 <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 裡的一筆記錄,包含工作目錄、檔案路徑、輸出路徑和一整組參數。這些參數會寫成 clang 呼叫的形式,因為 clangd 認得的是 clang 的命令列。

Important

createStep 要等所有相關目標都設定完成後再呼叫。模組會在呼叫時讀取建置圖;如果還有目標沒設定完,就讀不到相應的標頭檔目錄和原始檔。

另外,產生編譯資訊的步驟執行時,無法取得呼叫 createStep 時傳入的參數,所以模組用了一個全域變數,把目標列表存起來,供後續步驟讀取。

適配 Zig 0.13

從舊版介面到 0.13,主要有以下幾處變動。

  • std.Build.CompileStep 改成了 std.Build.Step.Compile。
  • 標頭檔目錄、連結物件等資訊,原本直接放在編譯步驟上,現在都移到了 root_module,所以要從 compile.root_module.include_dirs 和 compile.root_module.link_objects 讀取。
  • 安裝的標頭檔原本只有檔案一種,現在分成檔案和目錄兩種,需要分別處理。
  • CSourceFiles 多了一個 root 欄位,用來表示這些檔案的根目錄。
  • 建置步驟的 makeFn 中,進度節點的型別從指標改成了值。

最後一項的處理稍微不同,我用 @import("builtin").zig_version 在編譯期判斷版本,再決定使用指標還是值。

固定版本

Zig 的相依套件靠雜湊值固定,因此 README 中的網址要指向特定提交,並附上對應的雜湊值。升級到新的提交時,網址和雜湊值都要更新,手動維護太麻煩,所以我寫了一個 GitHub Actions 工作流程:每次推送到 master 時,自動取得最新提交,用 zig fetch 算出雜湊值,再替換 README 中的網址和雜湊值。這樣 README 裡的相依套件網址和雜湊值就能直接複製使用。

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