给 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...