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 的命令行。ImportantcreateStep 要放在所有相关目标都配置完成之后调用。模块会在调用时读取构建图;如果还有目标没配置完,相应的头文件目录和源文件就不会被读到。还有一点,生成的步骤在运行的时候,拿不到调用 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 里的依赖地址和哈希就可以直接复制使用。
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> 需要换成实际值。
然后在 build.zig 里引入,把需要生成的目标收集到一个列表里,最后调用 createStep。下面是接入方式的示意,C 源文件、头文件目录和链接库按项目实际情况添加。
这样就加了一个叫 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 的命令行。
createStep 要放在所有相关目标都配置完成之后调用。模块会在调用时读取构建图;如果还有目标没配置完,相应的头文件目录和源文件就不会被读到。
还有一点,生成的步骤在运行的时候,拿不到调用 createStep 时传进来的参数,所以模块里用了一个全局变量,把目标列表存了起来,让后面的步骤可以读到。
适配 Zig 0.13
旧版接口到 0.13 的变化主要是下面几处。
最后一处不太一样,我是用 @import("builtin").zig_version 在编译期判断的,根据版本选择用指针还是用值。
固定版本
Zig 的依赖是靠哈希来固定的,所以 README 里的地址要指向某一个具体的提交,并且带上它的哈希。升级到新提交时,地址和哈希都要更新,手动去维护太麻烦,所以我写了一个 GitHub Actions:每次推送到 master 的时候,自动取最新的提交,用 zig fetch 算出哈希,再把 README 里的地址和哈希替换掉。这样 README 里的依赖地址和哈希就可以直接复制使用。