Adding compile_commands.json to Zig's Build System
/ , , ,
AI TranslationSimplified ChineseEnglish
Summary:
zig build does not generate compile_commands.json on its own, so I adapted an existing module for Zig 0.13 to generate it with a build step.
Zig's built-in build system can also compile C projects. There is one problem, though: clangd in the editor does not know which header paths or compiler flags to use for each file. The code ends up covered in red squiggles, and navigation does not work either.
clangd relies on the project's compile_commands.json. CMake can export it directly, but zig build has no built-in step for doing that.
Fortunately, someone had already written a module that extracts this information from the build graph and generates compile_commands.json. It used an older Zig API, though, and I was using 0.13. The build APIs had changed quite a bit in between, so it would not compile as-is. I adapted it, and the repository is at xxxbrian/zig-compile-commands.
Usage
First, add it as a dependency in build.zig.zon, using the tarball URL for a specific commit and its corresponding hash. Replace <commit> and <hash> below with the actual values.
Then import it in build.zig, collect the targets you want to include in a list, and call createStep at the end. The following illustrates how to wire it up; add C source files, header directories, and linked libraries as your project requires.
This adds a step named zcc. Run zig build zcc, and it will generate compile_commands.json in the project root, where clangd will find it automatically.
How It Generates the File
The module takes the C-related information in the build graph and translates it into clang command-line arguments.
For each target passed in, it walks the target's link_objects. When it encounters a C source file, it records the file path and any compiler flags set specifically for that file. System libraries become arguments starting with -l; if the target links libc or libc++, it also adds -lc or -lc++ as appropriate. Header directories become -I arguments, including directories from headers installed by another target. If a target links other targets, those are added to the list and traversed too, so passing only the top-level executable target is enough to collect the full set of information.
Each C source file becomes one entry in compile_commands.json, containing the working directory, file path, output path, and a full list of arguments. The arguments are written as a clang invocation, because clangd understands clang's command line.
Call createStep only after all relevant targets have been fully configured. The module reads the build graph when this function is called; if a target is not ready yet, its header directories and source files will be missed.
There is another detail: when the generation step runs, it cannot access the arguments originally passed to createStep. The module therefore stores the target list in a global variable so that the later step can read it.
Adapting It for Zig 0.13
The main API changes between the older version and 0.13 were these:
I handled the last change slightly differently: I check @import("builtin").zig_version at compile time and choose either a pointer or a value based on the version.
Pinning a Version
Zig pins dependencies by hash, so the URL in the README needs to point to a specific commit and include the corresponding hash. Both need updating when moving to a new commit, and maintaining them by hand is tedious. I wrote a GitHub Actions workflow that runs on every push to master, takes the latest commit, calculates its hash with zig fetch, and replaces the URL and hash in the README. That way, the dependency URL and hash in the README are ready to copy and use.