zls icon indicating copy to clipboard operation
zls copied to clipboard

Build-On-Save doesn't work in sub projects

Open bernardassan opened this issue 8 months ago • 3 comments

Zig Version

0.15.0-dev.369+1a2ceb36c

ZLS Version

0.15.0-dev.64+e81b740

Client / Code Editor / Extensions

Helix

Steps to Reproduce and Observed Behavior

mkdir -p main && cd main
zig init

add the following

    const exe_check = b.addExecutable(.{
        .name = "main",
        .root_module = exe_mod,
    });
    const check = b.step("check", "Check compilation errors");
    check.dependOn(&exe_check.step);

to main/build.zig

mkdir -p examples/sub && cd examples/sub
zig init

Add a similar check step to examples/sub/build.zig

    const exe_check = b.addExecutable(.{
        .name = "sub",
        .root_module = exe_mod,
    });
    const check = b.step("check", "Check compilation errors");
    check.dependOn(&exe_check.step);

Expected Behavior

Biuld-On-Save should work in the sub Zig project just as it does in the main project. Because I noticed if I move main/examples/sub to become a top level project mv main/examples/sub sub Build-On-Save works as expected.

Relevant log output

info  ( main ): Starting ZLS      0.15.0-dev.64+e81b740 @ '/home/ultracode/.local/bin/zls'
info  ( main ): Log File:         /home/ultracode/.cache/zls/zls.log (info)
info  (server): Client Info:      helix-25.01.1 (340934db)
info  (server): Autofix Mode:     source.fixall
info  (server): added Workspace Folder: file:///home/ultracode/repos/zig/zvips
info  (server): Set config option 'enable_build_on_save' to true
info  (server): Set config option 'build_on_save_args' to ["check","--watch","-fincremental"]
info  (server): Set config option 'warn_style' to true
info  (server): Set config option 'highlight_global_var_declarations' to true
info  (server): Set config option 'builtin_path' to "/home/ultracode/.cache/zls/builtin.zig"
info  (server): Set config option 'zig_lib_path' to "/home/ultracode/.local/zig/zig-linux-x86_64-0.15.0-dev.369+1a2ceb36c/lib"
info  (server): Set config option 'zig_exe_path' to "/home/ultracode/.local/bin/zig"
info  (server): Set config option 'build_runner_path' to "/home/ultracode/.cache/zls/build_runner/06ec558a7bde93433271005c5551b33e/build_runner.zig"
info  (server): Set config option 'global_cache_path' to "/home/ultracode/.cache/zls"
info  (server): trying to start Build-On-Save for 'file:///home/ultracode/repos/zig/zvips'
info  (store ): Loaded build file 'file:///home/ultracode/repos/zig/zvips/examples/basic/build.zig'
info  (store ): Loaded build file 'file:///home/ultracode/repos/zig/zvips/build.zig'
info  (store ): Loaded build file 'file:///home/ultracode/repos/zig/zvips/bindings/build.zig'

Helix Config

[language-server.zls.config]
enable_build_on_save = true
build_on_save_args = ["check", "--watch", "-fincremental"]

I would love to fix this issue as it is affecting the developer experience on zvips, a new project I'm working on. I would appreciate some guidance since I'm new to the codebase.

bernardassan avatar Apr 22 '25 17:04 bernardassan

As it stands right now, ZLS is intentionally only running build on save in the workspace root directory. The project may contain arbitrarily many subprojects with their own "check" step which does not scale well if ZLS would run build on save separately for every one of them.

One possible approach to get build on save running for sub projects would be to modify the "check" step of the root project to run the "check" step of your subprojects:

Update the build.zig.zon of the root project to depend on the sub project:

.{
    .dependencies = .{
        .my_subproject = .{
            .path = "examples/sub",
        },
    },
}

Then hook up the "check" steps in the build.zig of the root project:

const my_subproject = b.dependency("my_subproject", .{ .target = target, .optimize = optimize });
const my_subproject_check = &my_subproject.builder.top_level_steps.get("check").?.step;

const check = b.step("check", "Check compilation errors");
check.dependOn(&exe_check.step);
check.dependOn(my_subproject_check);

This approach also has the advantage that manually running zig build check in the root project will also report compile errors of the linked subprojects.

Let me know if this approach works for you.

Techatrix avatar May 08 '25 21:05 Techatrix

I don't think it would be appropriate for my use case because the examples directory is ignored in the release artifact and the Zig build runner requires all dependencies to be available at build time but won't be in the end users package.

As it stands right now, ZLS is intentionally only running build on save in the workspace root directory. The project may contain arbitrarily many subprojects with their own "check" step which does not scale well if ZLS would run build on save separately for every one of them.

I agree with the above. What I'm rooting for is a slight modification to how we determine the workspace root directory.

The idea

  1. Every directory/subdirectory with a dedicated build.zig is a workspace candidate
  2. We set the workspace root relative to the present working directory For instance if we have a directory structure like bellow

📂 Directory structure

    📂 zvips
    ├── 📂 examples
    │  └── 📂 src
    │  │   └── ⚡ sub_main.zig
    │  └── ⚡ build.zig
    ├── 📂 lib
    │      └── ⚡ main.zig
    ├── ⚡ build.zig
    ├── ⚡ build.zig.zon
    ├──  LICENSE
    └──  README.md

When I cd zvips, zvips is the project workspace root and zvips/build.zig is the build file of the workspace so zls would only provide Build-On-Save daignostics for only files/projects(dependencies) referenced from zvips/build.zig hence the current behaviour of not running arbitrary check steps

But when I cd zvips/examples, since the examples subdirectory contains a dedicated build.zig the workspace root is zvips/examples and the zvips/examples/build.zig is used by the build runner and Build-On-Save for dependency fetching and Diagnostics. So If from my Editor I open src/sub_main.zig I get Build-On-Save diagnostics but if I later open ../lib/main.zig from my editor, only basic syntax diagnostics is given because it isn't referenced in anyway in examples/build.zig.

TLDR

  1. We determine the workspace root relative to the pwd where an editor instance was opened initially
  2. If that directory doesn't have a build.zig we move up parent directories a number of time (1-3max - maybe configurable) to see if we find a build.zig, If found, we try to use it for the project else fallback to only syntax error with some information in zls.log about why we can't do Build-On-Save diagnostics
  3. If that directory has a build.zig then our job is done and we use this build.zig as input for our build runner and Build-On-Save. Modules in this workspace and linked dependencies like your recommendation above works but anything outside of that scope give only basic diagnostics.

What do you think?

bernardassan avatar May 09 '25 00:05 bernardassan

There are also cases where the directory initially opened in the editor is a non-zig project (for example a JavaScript project), and it has a child directory with build.zig. Would be great if this could be handled somehow.

kpietraszko avatar Aug 07 '25 23:08 kpietraszko