9  Build System

In this chapter, we are going to talk about the build system in Zig, and how an entire project is built in Zig. One key advantage of Zig is that it includes a build system embedded in the language itself. This is great, because then you do not have to depend on a external system, separated from the compiler, to build your code.

You can find a good description of Zig’s build system on the article entitled “Build System”1 available in the official Zig’s website. We also have the excellent series of posts written by Felix2. But this chapter represents an extra resource for you to consult and rely on.

Building code is one of the things that Zig is best at. One thing that is particularly difficult in C/C++ and even in Rust, is to cross-compile source code to multiple targets (e.g. multiple computer architectures and operational systems), and the zig compiler is known for being one of the best existing pieces of software for this particular task.

9.1 How source code is built?

We already talked about the challenges of building source code in low-level languages at Section 1.2.1. As we described at that section, programmers invented Build Systems to surpass these challenges on the building processes of low-level languages.

Low-level languages uses a compiler to compile (or to build) your source code into binary instructions. In C and C++, we normally use compilers like gcc, g++ or clang to compile our C and C++ source code into these instructions. Every language have it’s own compiler, and this is no different in Zig.

In Zig, we have the zig compiler to compile our Zig source code into binary instructions that can be executed by our computer. In Zig, the compilation (or the build) process involves the following components:

  • The Zig modules that contains your source code;
  • Library files (either a dynamic library or a static library);
  • Compiler flags that tailors the build process to your needs.

These are the things that you need to connect together in order to build your source code in Zig. In C and C++, you would have an extra component, which is the header files of the libraries that you are using. But header files do not exist in Zig, so, you only need to care about them if you are linking your Zig source code with a C library. If that is not your case, you can forget about it.

Your build process is usually organized in a build script. In Zig, we normally write this build script into a Zig module in the root directory of our project, named as build.zig. You write this build script, then, when you run it, your project get’s built into binary files that you can use and distribute to your users.

This build script is normally organized around target objects. A target is simply something to be built, or, in other words, it’s something that you want the zig compiler to build for you. This concept of “targets” is present in most Build Systems, specially in CMake3.

There are four types of target objects that you can build in Zig, which are:

  • An executable, which is simply a binary executable file (e.g. a .exe file on Windows).
  • A shared library, which is simply a binary library file (e.g. a .so file in Linux or a .dll file on Windows).
  • A static library, which is simply a binary library file (e.g. a .a file in Linux or a .lib file on Windows).
  • An unit tests executable, which is an executable file that executes only unit tests.

We are going to talk more about these target objects at Section 9.3.

9.2 The build() function

A build script in Zig always contains a public (and top-level) build() function declared. It is like the main() function on the main Zig module of your project, that we discussed at Section 1.2.3. But instead of creating the entrypoint to your code, this build() function is the entrypoint to the build process.

This build() function should accept a pointer to a Build object as input, and it should use this “build object” to perform the necessary steps to build your project. The return type of this function is always void, and this Build struct comes directly from the Zig Standard Library (std.Build). So, you can access this struct by just importing the Zig Standard Library into your build.zig module.

Just as a very simple example, here you can see the source code necessary to build an executable file from the hello.zig Zig module.

const std = @import("std");
pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("hello.zig"),
        .target = b.host,
    });
    b.installArtifact(exe);
}

You can define and use other functions and objects in this build script. You can also import other Zig modules as you would normally do in any other module of your project. The only real requirement for this build script, is to have a public and top-level build() function defined, that accepts a pointer to a Build struct as input.

9.3 Target objects

As we described over the previous sections, a build script is composed around target objects. Each target object is normally a binary file (or an output) that you want to get from the build process. You can list multiple target objects in your build script, so that the build process generates multiple binary files for you at once.

For example, you may be a developer working in a cross-platform application, and, because this application is cross-platform, you probably need to realease binary files of your software for each OS supported by your application to your end users. You can define a target object in your build script for each OS (Windows, Linux, etc.) where you want to publish your software. This will make the zig compiler to build your project to multiple target OS’s at once. The Zig Build System official documentation have a great code example that demonstrates this strategy4.

A target object is created by the following methods of the Build struct that we introduced at Section 9.2:

  • addExecutable() creates an executable file;
  • addSharedLibrary() creates a shared library file;
  • addStaticLibrary() creates a static library file;
  • addTest() creates an executable file that executes unit tests.

These functions are methods from the Build struct that you receive as input of the build() function. All of them, create as output a Compile object, which represents a target object to be compiled by the zig compiler. All of these functions accept a similar struct literal as input. This struct literal defines three essential specs about this target object that you are building: name, target and root_source_file.

We already saw these three options being used on the previous example, where we used the addExecutable() method to create an executable target object. This example is reproduced below. Notice the use of the path() method from the Build struct, to define a path in the root_source_file option.

exe = b.addExecutable(.{
    .name = "hello",
    .root_source_file = b.path("hello.zig"),
    .target = b.host,
});

The name option specificy the name that you want to give to the binary file defined by this target object. So, in this example, we are building an executable file named hello. Is traditional to set this name option to the name of your project.

Furthermore, the target option specify the target computer architecture (or the target operational system) of this binary file. For example, if you want this target object to run on a Windows machine that uses a x86_64 architecture, you can set this target option to x86_64-windows-gnu for example. This will make the zig compiler to compile the project to run on a x86_64 Windows machine. You can see the full list of architectures and OS’s that the zig compiler supports by running the zig targets command in the terminal.

Now, if you are building the project to run on the current machine that you are using to run this build script, you can set this target option to the host method of the Build object, like we did in the example above. This host method identifies the current machine where you are currently running the zig compiler.

At last, the root_source_file option specifies the root Zig module of your project. That is the Zig module that contains the entrypoint to your application (i.e. the main() function), or, the main API of your library. This also means that, all the Zig modules that compose your project are automatically discovered from the import statements that you have inside this “root source file”. The zig compiler can detect when a Zig module depends on the other through the import statements, and, as a result, it can discover the entire map of Zig modules used in your project.

This is handy, and it is different from what happens in other build systems. In CMake for example, you have to explicitly list the paths to all source files that you want to include in your build process. This is probably a symptom of the “lack of conditional compilation” in C and C++ compilers. Since they lack this feature, you have to explicitly choose which source files are sent to the C/C++ compiler, since not every C/C++ code is portable or supported in every operational system, and, therefore, would cause a compilation error in the C/C++ compiler.

Now, one important detail about the build process is that, you have to explicitly install the target objects that you create in your build script, by using the installArtifact() method of the Build struct.

Everytime you invoke the build process of your project, by calling the build command of the zig compiler, a new directory named zig-out is created in the root directory of your project. This new directory contains the output of the build process, that is, the binary files built from your source code.

What the installArtifact() method do is install (or copy) the built target objects that you defined to this zig-out directory. This means that, if you do not install the target objects you define in your build script, these target objects are essentially discarded at the end of the build process.

For example, you might be building a project that uses a third party library that is built together with the project. So, when you build your project, you would need first, to build the third party library, and then, you link it with the source code of your project. So, in this case, we have two binary files that are generated in the build process (the executable file of your project, and the third party library). But only one is of interest, which is the executable file of our project. We can discard the binary file of the third party library, by simply not installing it into this zig-out directory.

So, is easy to use this installArtifact() method. Just remember to apply it to every target object that you want to save it into the zig-out directory, like in the example below:

exe = b.addExecutable(.{
    .name = "hello",
    .root_source_file = b.path("hello.zig"),
    .target = b.host,
});

b.installArtifact(exe);

9.4 Setting the build mode

We talked about the three essential options that are set when you create a new target object. But there is also a fourth option that you can use to set the build mode of this target object, which is the optimize option. This option is called this way, because the build modes in Zig are treated more of an “optimization vs safety” problem. So optmization plays an important role here. Don’t worry, I’m going back to this question very soon.

In Zig, we have the four build modes listed below. Each one of these build modes offer different advantages and characteristics. As we described at Section 5.2.1, the zig compiler uses the Debug build mode by default, when you don’t explicitly choose a build mode.

  • Debug, mode that produces and includes debugging information in the output of the build process (i.e. the binary file defined by the target object);
  • ReleaseSmall, mode that tries to produce a binary file that is small in size;
  • ReleaseFast, mode that tries to optimize your code, in order to produce a binary file that is as fast as possible;
  • ReleaseSafe, mode that tries to make your code as safe as possible, by including safeguards when possible.

So, when you build your project, you can set the build mode of your target object to ReleaseFast for example, which will tell the zig compiler to apply important optimizations in your code. This creates a binary file that simply runs faster on most contexts, because it contains a more optimized version of your code. However, as a result, we normally loose some security funcionalities in our code. Because some safety checks are removed from the final binary file, which makes your code run faster, but in a less safe manner.

This choice depends on your current priorities. If you are building a cryptography or banking system, you might prefer to prioritize safety in your code, so, you would choose the ReleaseSafe build mode, which is a little slower to run, but much more secure, because it includes all possible runtime safety checks in the binary file built in the build process. In the other hand, if you are writing a game for example, you might prefer to prioritize performance over safety, by using the ReleaseFast build mode, so that your users can experience faster frame rates in your game.

In the example below, we are creating the same target object that we used on previous examples. But this time, we are specifying the build mode of this target object to use the ReleaseSafe mode.

const exe = b.addExecutable(.{
    .name = "hello",
    .root_source_file = b.path("hello.zig"),
    .target = b.host,
    .optimize = .ReleaseSafe
});
b.installArtifact(exe);

9.5 Setting the version of your build

Everytime you build a target object in your build script, you can assign a version number to this specific build, following a semantic versioning framework. You can find more about semantic versioning by visiting the Semantic Versioning website5. Anyway, in Zig, you can specify the version of your build, by providing a SemanticVersion struct to the version option, like in the example below:

const exe = b.addExecutable(.{
    .name = "hello",
    .root_source_file = b.path("hello.zig"),
    .target = b.host,
    .version = .{
        .major = 2, .minor = 9, .patch = 7
    }
});
b.installArtifact(exe);

9.6 Detecting the OS in your build script

Is very commom in Build Systems to use different options, or, to include different modules, or, to link against different libraries depending on the Operational System (OS) that you are targeting in the build process.

In Zig, you can detect the target OS of the build process, by looking at the os.tag inside the builtin module from the Zig library. In the example below, we are using an if statement to run some arbitrary code when the target of the build process is a Windows system.

const builtin = @import("builtin");
if (builtin.target.os.tag == .windows) {
    // Code that runs only when the target of
    // the compilation process is Windows.
}

9.7 Adding a run step to your build process

One thing that is neat in Rust is that you can compile and run your source code with one single command (cargo run) from the Rust compiler. We saw at Section 1.2.5 how can we perform a similar job in Zig, by building and running our Zig source code through the run command from the zig compiler.

But how can we, at the same time, build and run the binary file specified by a target object in our build script? The answer is by including a “run artifact” in our build script. A run artifact is created through the addRunArtifact() method from the Build struct. We simply provide as input to this function the target object that describes the binary file that we want to execute, and the function creates as output, a run artifact capable of executing this binary file.

In the example below, we are defining an executable binary file named hello, and we use this addRunArtifact() method to create a run artifact that will execute this hello executable file.

const exe = b.addExecutable(.{
    .name = "hello",
    .root_source_file = b.path("src/hello.zig"),
    .target = b.host
});
b.installArtifact(exe);
const run_arti = b.addRunArtifact(exe);

Now that we created the run artifact, we need to include it in the build process. We do that by declaring a new step in our build script to call this artifact, through the step() method of the Build struct. We can give any name we want to this step, but, for our context here, I’m going to name this step as “run”. Also, I give it a brief description to this step (“Run the project”).

const run_step = b.step(
    "run", "Run the project"
);

Now that we declared this “run step” we need to tell Zig that this “run step” depends on the run artifact. In other words, a run artifact always depends on a “step” to effectively be executed. By creating this dependency we finally stablish the necessary commands to build and run the executable file from the build script.

We establish a dependency between the run step and the run artifact by using the dependsOn() method from the run step. So, we first create the run step, and then, we link it with the run artifact, by using this dependsOn() method from the run step.

run_step.dependOn(&run_arti.step);

The entire source code of this specific build script that we wrote, piece by piece, in this section, is available in the build_and_run.zig module. You can see this module by visiting the official repository of this book 6.

When you declare a new step in your build script, this step becomes available through the build command in the zig compiler. You can actually see this step by running zig build --help in the terminal, like in the example below, where we can see that this new “run” step that we declared in the build script appeared in the output.

zig build --help
Steps:
  ...
  run   Run the project
  ...

Now, everything that we need to is to call this “run” step that we created in our build script. We call it by using the name that we gave to this step after the build command from the zig compiler. This will cause the compiler to build our executable file and execute it at the same time.

zig build run

9.8 Build unit tests in your project

We talk at length about writing unit tests in Zig, and we also talk about how to execute these unit tests through the test command of the zig compiler at Chapter 8. However, as we did with the run command on the previous section, we also might want to include some commands in our build script to also build and execute the unit tests in our project.

So, once again, we are going to discuss how a specific built-in command from the zig compiler, (in this case, the test command) can be used in a build script in Zig. This means that, we can include a step in our build script to build and run all unit tests in our project at once.

Here is where a “test target object” comes into play. As was described at Section 9.3, we can create a test target object by using the addTest() method of the Build struct. So the first thing that we need to do is to declare a test target object in our build script.

const test_exe = b.addTest(.{
    .name = "unit_tests",
    .root_source_file = b.path("src/main.zig"),
    .target = b.host,
});
b.installArtifact(test_exe);

A test target object essentially filter all test blocks in all Zig modules across your project, and builds only the source code present inside these test blocks in your project. As a result, this target object creates an executable file that contains only the source code present in all of these test blocks (i.e. the unit tests) in your project.

Perfect! Now that we declared this test target object, an executable file named unit_tests is built by the zig compiler when we trigger the build script with the build command. After the build process is finished, we can simply execute this unit_tests executable in the terminal.

However, if you remember the previous section, we already learned how can we create a run step in our build script, to execute an executable file built by the build script.

So, we could simply add a run step in our build script to run these unit tests from a single command in the zig compiler, to make our lifes easier. In the example below, we demonstrate the commands to register a new build step called “tests” in our build script to run these unit tests.

const run_arti = b.addRunArtifact(test_exe);
const run_step = b.step("tests", "Run unit tests");
run_step.dependOn(&run_arti.step);

Now that we registered this new build step, we can trigger it by calling the command below in the terminal. You can also checkout the complete source code for this specific build script at the build_tests.zig module at the official repository of this book 7.

zig build tests

9.9 Tailoring your build process with user-provided options

Sometimes, you want to make a build script that is customizable by the user of your project. You can do that by creating user-provided options in your build script. In Zig, we create these options using the option() method from the Build struct.

With this method, we create a “build option” which can be passed to the build.zig script at the command line. The user have the power of setting this option at the build command from the zig compiler. In other words, each build option that we create becomes a new command line argument accessible in the build command of the compiler.

These “user-provided options” are set by using the prefix -D in the command line. For example, if we declare an option named use_zlib, that receives a boolean value which indicates if we should link our source code to zlib or not, we can set the value of this option in the command line with -Duse_zlib. The code example below demonstrates this idea:

const std = @import("std");
pub fn build(b: *std.Build) void {
    const use_zlib = b.option(
        bool,
        "use_zlib",
        "Should link to zlib?"
    ) orelse false;
    const exe = b.addExecutable(.{
        .name = "hello",
        .root_source_file = b.path("example.zig"),
        .target = b.host,
    });
    if (use_zlib) {
        exe.linkSystemLibrary("zlib");
    }
    b.installArtifact(exe);
}

You can set this use_zlib option at the command line when you are invoking the build command from the zig compiler. In the example below, we set this option to false, which means that the build script will not link our binary executable to the zlib library.

zig build -Duse_zlib=false

9.10 Linking to external libraries

One essential part of every build process is the linking stage. This stage is responsible for combining the multiple object files that represent your code, into a single executable file. It also links this executable file to an external libraries, if you use any in your code.

In Zig, we have two notions of a “library”, which are: 1) a system’s library; 2) a local library. A system’s library is just a library that already is installed in your system. While a local library is a library that belongs to the current project. Is a library that is present in your project directory, and that you are building together with your project source code.

The basic difference between the two, is that a system’s library is already built and installed in your system, supposedly, and all you need to do is to link your source code to this library to start using it. We do that by using the linkSystemLibrary() method from a Compile object. This method accepts the name of the library in a string as input. Remember from Section 9.3 that a Compile object is a target object that you declare in your build script.

When you link a particular target object with a system’s library, the zig compiler will use pkg-config to find where in your system are the binary files and also the header files of this library that you requested for. When it finds these files, the linker present in the zig compiler will link your object files with the files of this library to produce a single executable file.

In the example below, we are creating an executable file named image_filter, and, we are linking this executable file to the C Standard Library with the method linkLibC(), but we also are linking this executable file to the C library libpng that is currently installed in my system.

const std = @import("std");
pub fn build(b: *std.Build) void {
    const exe = b.addExecutable(.{
        .name = "image_filter",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });
    exe.linkLibC();
    exe.linkSystemLibrary("png");
    b.installArtifact(exe);
}

If you are linking with a C library in your project, is generally a good idea to also link your code with the C Standard Library. Because is very likely that this C library uses some functionality of the C Standard Library at some point. The same goes to C++ libraries. So, if you are linking with C++ libraries, is a good idea to link your project with the C++ Standard Library using the linkLibCpp() method.

On the order side, when you want to link with a local library, you should use the linkLibrary() method of a Compile object. This method expects to receive another Compile object as input. That is, another target object defined in your build script, using either the addStaticLibrary() or addSharedLibrary() methods which defines a library to be built.

Because as we discussed earlier, a local library is a library that is local to your project, and that is being built together with your project. So, you need to create a target object in your build script to build this local library. Then, you link the target objects of interest in your project, with this target object that identifies this local library.

Take a look at this example extracted from the build script of the libxev library8. You can see in this snippet that we are declaring a shared library file, from the c_api.zig module. Then later in the build script, we declare an executable file named “dynamic-binding-test”, which links to this shared library that we defined earlier in the script.

const optimize = b.standardOptimizeOption(.{});
const target = b.standardTargetOptions(.{});

const dynamic_lib = b.addSharedLibrary(.{
    .name = dynamic_lib_name,
    .root_source_file = b.path("src/c_api.zig"),
    .target = target,
    .optimize = optimize,
});
b.installArtifact(dynamic_lib);
// ... more lines in the script
const dynamic_binding_test = b.addExecutable(.{
    .name = "dynamic-binding-test",
    .target = target,
    .optimize = optimize,
});
dynamic_binding_test.linkLibrary(dynamic_lib);

9.11 Building C code

The zig compiler comes with a C compiler embedded in it. In other words, you can use the zig compiler to build C projects. This C compiler is available through the cc command of the zig compiler.

As an example, let’s use the famous FreeType library9. FreeType is one of the most widely used pieces of software in the world. It is a C library designed to produce high-quality fonts. But it is also heavily used in the industry to natively render text and fonts in the screen of your computer.

In this section, we are going to write a build script, piece by piece, that is capable of building the FreeType project from source. You can find the source code of this build script on the freetype-zig repository10 available at GitHub.

After you download the source code of FreeType from the official website11, you can start writing the build.zig module. We begin by defining the target object that defines the binary file that we want to compile.

As an example, I will build the project as a static library file using the addStaticLibrary() method to create the target object. Also, since FreeType is a C library, I will also link the library against libc through the linkLibC() method, to garantee that any use of the C Standard Library is covered in the compilation process.

const target = b.standardTargetOptions(.{});
const opti = b.standardOptimizeOption(.{});
const lib = b.addStaticLibrary(.{
    .name = "freetype",
    .optimize = opti,
    .target = target,
});
lib.linkLibC();

9.11.1 Creating C compiler flags

Compiler flags are also known as “compiler options” by many programmers, or also, as “command options” in the GCC official documentation. It’s fair to also call them as the “command-line arguments” of the C compiler. In general, we use compiler flags to turn on (or turn off) some features from the compiler, or to tweak the compilation process to fit the needs of our project.

In build scripts written in Zig, we normally list the C compiler flags to be used in the compilation process in a simple array, like in the example below.

const c_flags = [_][]const u8{
    "-Wall",
    "-Wextra",
    "-Werror",
};

In theory, there is nothing stopping you from using this array to add “include paths” (with the -I flag) or “library paths” (with the -L flag) to the compilation process. But there are formal ways in Zig to add these types of paths in the compilation process. Both are discussed at Section 9.11.5 and Section 9.11.4.

Anyway, in Zig, we add C flags to the build process together with the C files that we want to compile, using the addCSourceFile() and addCSourceFiles() methods. In the example above, we have just declared the C flags that we want to use. But we haven’t added them to the build process yet. To do that, we also need to list the C files to be compiled.

9.11.2 Listing your C files

The C files that contains “cross-platform” source code are listed in the c_source_files object below. These are the C files that are included by default in every platform supported by the FreeType library. Now, since the amount of C files in the FreeType library is big, I have omitted the rest of the files in the code example below, for brevity purposes.

const c_source_files = [_][]const u8{
    "src/autofit/autofit.c",
    "src/base/ftbase.c",
    // ... and many other C files.
};

Now, in addition to “cross-platform” source code, we also have some C files in the FreeType project that are platform-specific, meaning that, they contain source code that can obly be compiled in specific platforms, and, as a result, they are only included in the build process on these specific target platforms. The objects that list these C files are exposed in the code example below.

const windows_c_source_files = [_][]const u8{
    "builds/windows/ftdebug.c",
    "builds/windows/ftsystem.c"
};
const linux_c_source_files = [_][]const u8{
    "src/base/ftsystem.c",
    "src/base/ftdebug.c"
};

Now that we declared both the files that we want to include and the C compiler flags to be used, we can add them to the target object that describes the FreeType library, by using the addCSourceFile() and addCSourceFiles() methods.

Both of these functions are methods from a Compile object (i.e. a target object). The addCSourceFile() method is capable of adding a single C file to the target object, while the addCSourceFiles() method is used to add multiple C files in a single command. You might prefer to use addCSourceFile() when you need to use different compiler flags on specific C files in your project. But, if you can use the same compiler flags across all C files, then, you will probably find addCSourceFiles() a better choice.

Notice that we are using the addCSourceFiles() method in the example below, to add both the C files and the C compiler flags. Also notice that we are using the os.tag that we learned about at Section 9.6, to add the platform-specific C files.

const builtin = @import("builtin");
lib.addCSourceFiles(
    &c_source_files, &c_flags
);

switch (builtin.target.os.tag) {
    .windows => {
        lib.addCSourceFiles(
            &windows_c_source_files,
            &c_flags
        );
    },
    .linux => {
        lib.addCSourceFiles(
            &linux_c_source_files,
            &c_flags
        );
    },
    else => {},
}

9.11.3 Defining C Macros

C Macros are an essential part of the C programming language, and they are commonly defined through the -D flag from a C compiler. In Zig, you can define a C Macro to be used in your build process by using the defineCMacro() method from the target object that defines the binary file that you are building.

In the example below, we are using the lib object that we defined on the previous sections to define some C Macros used by FreeType in the compilation process. These C Macros specify if FreeType should (or should not) include functionalities from different external libraries.

lib.defineCMacro("FT_DISABLE_ZLIB", "TRUE");
lib.defineCMacro("FT_DISABLE_PNG", "TRUE");
lib.defineCMacro("FT_DISABLE_HARFBUZZ", "TRUE");
lib.defineCMacro("FT_DISABLE_BZIP2", "TRUE");
lib.defineCMacro("FT_DISABLE_BROTLI", "TRUE");
lib.defineCMacro("FT2_BUILD_LIBRARY", "TRUE");

9.11.4 Adding library paths

Library paths are paths in your computer where the C compiler will look (or search) for library files to link against your source code. In other words, when you use a library in your C source code, and you ask the C compiler to link your source code against this library, the C compiler will search for the binary files of this library across the paths listed in this “library paths” set.

These paths are platform specific, and, by default, the C compiler starts by looking at a pre-defined set of places in your computer. But you can add more paths (or more places) to this list. For example, you may have a library installed in a non-conventional place of your computer, and you can make the C compiler “see” this “non-conventional place” by adding this path to this list of pre-defined paths.

In Zig, you can add more paths to this set by using the addLibraryPath() method from your target object. First, you defined a LazyPath object, containing the path you want to add, then, you provide this object as input to the addLibraryPath() method, like in the example below:

const lib_path: std.Build.LazyPath = .{
    .cwd_relative = "/usr/local/lib/"
};
lib.addLibraryPath(lib_path);

9.11.5 Adding include paths

The preprocessor search path is a popular concept from the C community, but it is also known by many C programmers as “include paths”, because the paths in this “search path” relate to the #include statements found in the C files.

Include paths are similar to library paths. They are a set of pre-defined places in your computer where the C compiler will look for files during the compilation process. But instead of looking for library files, the include paths are places where the compiler looks for header files included in your C source code. This is why many C programmers prefer to call these paths as the “preprocessor search path”. Because header files are processed during the preprocessor stage of the compilation process.

So, every header file that you include in your C source code, through a #include statements needs to be found somewhere, and the C compiler will search for this header file across the paths listed in this “include paths” set. Include paths are added to the compilation process through the -I flag.

In Zig, you can add new paths to this pre-defined set of paths, by using the addIncludePath() method from your target object. This method also accepts a LazyPath object as input.

const inc_path: std.Build.LazyPath = .{
    .path = "./include"
};
lib.addIncludePath(inc_path);

  1. https://ziglang.org/learn/build-system/#user-provided-options↩︎

  2. https://zig.news/xq/zig-build-explained-part-1-59lf↩︎

  3. https://cmake.org/cmake/help/latest/manual/cmake-buildsystem.7.html↩︎

  4. https://ziglang.org/learn/build-system/#handy-examples↩︎

  5. https://semver.org/↩︎

  6. https://github.com/pedropark99/zig-book/blob/main/ZigExamples/build_system/build_and_run.zig↩︎

  7. https://github.com/pedropark99/zig-book/blob/main/ZigExamples/build_system/build_tests.zig↩︎

  8. https://github.com/mitchellh/libxev/tree/main↩︎

  9. https://freetype.org/↩︎

  10. https://github.com/pedropark99/freetype-zig/tree/main↩︎

  11. https://freetype.org/↩︎