How can zls get a configurable system include path for c library?

Hi, I’m working on a cross-platform Zig project that builds and links several upstream C libraries dynamically, using zig build and a build.zig script. Since I cannot assume fixed include/library paths or system dependencies across platforms (e.g. Linux, MinGW, MSVC), I pass these paths into the build system using custom -D options like this:
build.zig:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const add_include_path = b.option([]std.Build.LazyPath, "add_include_path", "Add Include Path");
    const add_library_path = b.option([]std.Build.LazyPath, "add_library_path", "Add Library Path");
    const add_syslinks = b.option([][]const u8, "add_syslinks", "Add System Links");

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

    const exe = b.addExecutable(.{
        .name = "my_app",
        .root_source_file = b.path("tests/zig-tests/test.zig"),
        .target = target,
        .optimize = optimize,
    });
    // exe.addSystemIncludePath(.{ .cwd_relative = "C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\l\\libgit2\\v1.9.1\\ec21fe5c9cd84177a9b2ae7b10364e0e\\include" });
    if (add_include_path) |include_paths| {
        for (include_paths) |include_path| {
            exe.addSystemIncludePath(include_path);
        }
    }
    if (add_library_path) |library_paths| {
        for (library_paths) |library_path| {
            exe.addLibraryPath(library_path);
        }
    }
    if (add_syslinks) |syslinks| {
        for (syslinks) |syslink| {
            exe.linkSystemLibrary(syslink);
        }
    }
    exe.linkLibC();
    b.installArtifact(exe);
}

.vscode/settings.json:

{
    "zig.buildArgs": [
        "-Dadd_include_path=C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\l\\libgit2\\v1.9.1\\ec21fe5c9cd84177a9b2ae7b10364e0e\\include",
        "-Dadd_library_path=C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\l\\libgit2\\v1.9.1\\ec21fe5c9cd84177a9b2ae7b10364e0e\\lib",
        "-Dadd_library_path=C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\p\\pcre2\\10.44\\b9cd3e52829b49b5a6277a0f8d46b73b\\lib",
        "-Dadd_library_path=C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\l\\llhttp\\v9.2.1\\f789a618795045469bb19f8380337983\\lib",
        "-Dadd_library_path=C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\z\\zlib\\v1.3.1\\64e7237eeea44845a55e4edf99cec725\\lib",
        "-Dadd_syslinks=ole32",
        "-Dadd_syslinks=rpcrt4",
        "-Dadd_syslinks=winhttp",
        "-Dadd_syslinks=ws2_32",
        "-Dadd_syslinks=user32",
        "-Dadd_syslinks=crypt32",
        "-Dadd_syslinks=advapi32",
        "-Dadd_syslinks=git2",
        "-Dadd_syslinks=pcre2-posix",
        "-Dadd_syslinks=pcre2-8",
        "-Dadd_syslinks=llhttp",
        "-Dadd_syslinks=z"
    ]
}

This works perfectly fine when building the project with zig build. However, zls is unable to see these include paths, which causes problems that, the vscode ziglang plugin cannot find the files to include with @cImport and @cInclude, even though the build works.
I understand that zls likely doesn’t run build.zig with the full options, or can’t observe runtime logic in the build script. But in my use case, is there any way to make zls aware of dynamically configured system include paths?

If you have build_on_save enabled for zls build.zig will be used.
You could @embedFile a config file, and parse it in the build script, instead of passing arguments.

1 Like

Actually, you can pass build options in a zls.build.json….

Thank you for your advice. Now it works!
Here is my example.
build.zig:

const std = @import("std");
const raw_build_config = @embedFile("build_config.json");

pub fn build(b: *std.Build) void {
    var arena_state = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    const BuildConfig = struct { add_system_include_paths: [][]const u8, add_library_paths: [][]const u8, link_system_librarys: [][]const u8 };
    const parsed = std.json.parseFromSlice(
        BuildConfig,
        arena,
        raw_build_config,
        .{},
    ) catch |err| {
        std.debug.print("failed to parse build_config.json: {}\n", .{err});
        std.process.exit(1);
    };
    defer parsed.deinit();

    const build_config = parsed.value;

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

    const exe = b.addExecutable(.{
        .name = "my_app",
        .root_source_file = b.path("tests/zig-tests/test.zig"),
        .target = target,
        .optimize = optimize,
    });
    // exe.addSystemIncludePath(.{ .cwd_relative = "C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\l\\libgit2\\v1.9.1\\ec21fe5c9cd84177a9b2ae7b10364e0e\\include" });
    for (build_config.add_system_include_paths) |system_include_path| {
        exe.addSystemIncludePath(.{ .cwd_relative = system_include_path });
    }
    for (build_config.add_library_paths) |library_path| {
        exe.addLibraryPath(.{ .cwd_relative = library_path });
    }
    for (build_config.link_system_librarys) |system_library| {
        exe.linkSystemLibrary(system_library);
    }
    exe.linkLibC();
    b.installArtifact(exe);

    const exe_check = b.addExecutable(.{
        .name = "my_app",
        .root_source_file = b.path("tests/zig-tests/test.zig"),
        .target = target,
        .optimize = optimize,
    });
    for (build_config.add_system_include_paths) |system_include_path| {
        exe.addSystemIncludePath(.{ .cwd_relative = system_include_path });
    }
    const check = b.step("check", "Check if my_app compiles");
    check.dependOn(&exe_check.step);
}

build_config.json:

{
    "add_system_include_paths": [
        "C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\l\\libgit2\\v1.9.1\\ec21fe5c9cd84177a9b2ae7b10364e0e\\include"
    ],
    "add_library_paths": [
        "C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\l\\libgit2\\v1.9.1\\ec21fe5c9cd84177a9b2ae7b10364e0e\\lib",
        "C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\p\\pcre2\\10.44\\b9cd3e52829b49b5a6277a0f8d46b73b\\lib",
        "C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\l\\llhttp\\v9.2.1\\f789a618795045469bb19f8380337983\\lib",
        "C:\\Users\\ly\\AppData\\Local\\.xmake\\packages\\z\\zlib\\v1.3.1\\64e7237eeea44845a55e4edf99cec725\\lib"
    ],
    "link_system_librarys": [
        "ole32",
        "rpcrt4",
        "winhttp",
        "ws2_32",
        "user32",
        "crypt32",
        "advapi32",
        "git2",
        "pcre2-posix",
        "pcre2-8",
        "llhttp",
        "z"
    ]
}

I am still very curious about zls.build.json, since I cannot find documentions about this.

ZLS docs are here: Per-Build Config - zigtools

1 Like

Thank you! I learn how it work. In the end, I think an indepedent embed json file to analyse is the correct way to do it, but more complicated. I’m not sure whether the option is a good way, maybe not, though it seems easier and more official.