I am used to seeing in medium- to large-scale projects, specifically as in a DBMS, but also likely in OS kernels, web browsers and compilers, a directory tree that has at least two or three layers and, at the second or third layer, multiple “components”, e.g., a server area with a parser directory, an executor directory and a storage or access methods directory, possibly with subdirectories, e.g. for btree, hash, rtree methods. Each directory typically has a Makefile or CMakeLists.txt file that addresses the build of the files in that directory, or “delegates” the task to its subdirectories.
Am I correct that, in the current stage of Zig development, the only option is to have a singlebuild.zig file that takes care of building everything from the root down? I took a look at Zig itself and it appears there’s a single 1500+ line file and at Tigerbeetle which has a single 2000+ line file.
no of course this is not correct. consider: build.zig is a Zig file, and all Zig files have access to the @import() compiler directive. ziglua has several build files.
for sufficiently self-contained parts of a project, one can also use zig run or zig test directly.
I may be mistaken, but following the link to ziglua, first I wouldn’t consider it even a medium scale project: it appears to be just bindings to Lua C API, so it doesn’t have multiple layers nor different components. I’m talking about something on the scale of Postgres, which is visible at the mirror in https://github.com/postgres/postgres. For example, if you go to backend/storageyou’ll find both a Makefile and a meson.build file and those “drive” the build of the subdirectories.
Second, I don’t see “several build files” unless you’re referring to those files in the builddirectory which would appear to be disjoint (as “in a different place”) from the actual code being built.
As @alanza points out, there’s nothing stopping a repo from having several build scripts. Should it? Probably not, exceptions surely exist. But probably not.
Having only one build.zig file is not the only option, but it’s the option that makes sense, basically.
In C, it makes sense for each directory to have a self-contained build script, because it needs to define macros and stuff, sure.
In Zig, you can basically compile a deeply nested subdirectory as a module, and that same module should be completely fine being imported by any other module. The compilation order doesn’t matter, because there are no macros.
Another factor is lazy evaluation. By choosing what its root file references, a module is already effectively the “master” of exactly what is being compiled into it, and all the build system needs to say is “build this”. Meaning that the build system gets to be blissfully uncomplicated.
You can just create a, e.g. src/build.zig and in your build.zig, you say once const src = @import("src/build.zig") (or something similar) and then, in the build function in the build.zig, you invoke your build function from src/build.zig. So like the following:
Yes, but now there’s cmake and meson and ninja, for example.
Perhaps a “fully-connected build graph is optimal” for build performance, but I still think that having build instructions close to what is being built, particularly in a large project, is preferable from a human ergonomics viewpoint. For example, if I have to refactor some files in a btree subdirectory, it’s much easier to modify the local CMakeLists.txt, than to go find where those files are being built in a large top-level build file.
Thanks. Ghostty’s src/build/main.zig tends to confirm what I was thinking:
//! Build logic for Ghostty. A single "build.zig" file became far too complex
//! and spaghetti, so this package extracts the build logic into smaller,
//! more manageable pieces.
What you’re calling a “build script”, i.e., a Makefile, CMakeLists.txt or meson.build file for building a C or C++ subdirectory, does not typically need to define any C or C++ macros. See for example, that 1997 paper linked earlier by @mnemnion. You won’t find any -D usage in those examples. You may find makemacros in a Makefile, like $(CC), but modern tools like CMake and Meson (and even Gnu make) make (pun somewhat intentional) such macros generally unnecessary.
Sure, but multiple build.zig files is not the optimal way to get this.
Have a .zig file (idk, build_script.zig? doesn’t matter) in the directory you want to treat as semi-self-contained, and have it export a pub fn which takes a *Build, then @import that into the (singular) build.zig and call the function.
That is exactly what Ghostty does, and it’s identical to the module.mk pattern in Miller’s paper. build.zig is special, it creates a binary executable despite having no main function, and Ghostty has exactly one of those.
It’s presumably possible to write a ‘polyglot’ build.zig which can be executed directly with zig build, but also imported into the parent build.zig and used that way. But I doubt there’s any upside in doing that.
also, unsaid so far, one of the points of having so many dispersed C/C++ build tools is that C/C++ compilation units are small. Zig compilation units are not small; typically whole executables or libraries wind up being one compilation unit, even counting Zig dependencies.
Sorry, I’m no “standards lawyer”, but your statements appear somewhat ambiguous. For example, you mention “so many dispersed C/C++ build tools”, but my thread was asking about usage of multiple Makefiles or similar build files, not tools.
Second, I looked for “compilation unit” at the Zig documentation and found only one instance of “compilation unit” and none of “translation unit”, which according to Wikipedia is the more formal term. The Zig docs, at least at first glance, don’t appear to define “compilation unit”, so what do you consider a unit? If it’s, let’s say, everything that goes into a library, then I cannot see any practical difference with a C or C++ project directory that, as a “unit”, gets built into a library by some Makefile or similar file.
Furthermore, with the advent of C++20 modules, at least in C++, I believe the translation or compilation of a single C++ file has probably become much more akin to the compilation of a Zig source file, because both compilers have to take as input not just a single text file (and some header files in the C case, and possibly in the C++ case), but module information that is imported into that file.
forgive me for failing to pass the shibboleth test here. if it makes you feel better, i’ve basically never programmed professionally? happy to pay whatever pound of flesh you’re looking for here.
what i meant is that my inexpert understanding is that each .c file produces a .o file, and these are linked together to produce the binary. zig isn’t doing that. indeed it cannot: given the existence of language features like inferred error sets, a certain amount of whole-program analysis needs to occur to compile Zig code, and my bystander understanding is that the core team is leaning into the advantages that that can bring.
Ah, are you implying I’m Shylock? LOL.
Seriously though, yes, in C each .c file generally produces a single .o file, but you also have to consider that it also has to read every #include’d .h file and nested therein. In C++ since the beginning, and in modern C, it’s not possible to use some function without declaring it somewhere first, it’s not like K&R C where you could invoke printfwithout including the necessary header, so a .cfile cannot be analyzed in a completely “standalone” fashion.
In C++20 and later, with the introduction of the importand export of modules, the “whole-program analysis” also needs to occur, perhaps not as deep as what Zig does. This has affected C++ build systems like CMake because a “once-over-lightly” compilation pass has to occur before CMake and other build systems can analyze inter-module dependencies. Which brings me to my very first post here, which has gone unanswered and, although I haven’t really tried zig c++ yet, it seems it won’t work for compiling a C++ file that imports some other module, whether std or not.
You can structure your code however you like. Make the build process as complex or simple as you want. There are no rules you need to follow for the filetree.
Of course you can use multiple build scripts or even multiple seperate packages.
If a complex build process is worth it is a different discussion.
…I’m not sure if that’s a good idea though since for some reason this approach creates local .zig-cache directories in each directory with a build.zig. If build.zig gets too big it probably makes more sense to have a directory structure like this:
build.zig // top-level build.zig
build/ // subdirectory with build-helper files imported into toplevel build.zig
emus.zig
tests.zig
tools.zig