ZLS does not work for @import("root") when multiple artifacts are installed

Hello.
I’m trying to use @import("root") to access modules, as Zig’s standard library and Bun are doing (actually, I always refer to them as the best example :slight_smile: ). My project has below structure:

.
├── build.zig
├── tool.zig
├── root.zig
└── src
    ├── main.zig
    ├── me.zig
    └── mod_a.zig

me.zig is a kind of utility to expose mod_a.zig (and other more modules in an actual project):

pub const mod_a = @import("mod_a.zig");

mod_a.zig is a module which defines one function:

pub fn mod_a_fn() void {
    @import("std").log.info("hello mod_a", .{});
}

main.zig implements a main function that calls mod_a_fn:

const me = @import("root").me;

pub fn main() !void {
    me.mod_a.mod_a_fn();
}

root.zig is a root_source_file of the executable and exposes main.zig with usingnamespace and me.zig with @import:

pub usingnamespace @import("src/main.zig");
pub const me = @import("src/me.zig");

When I build them, it can compile and ZLS works completely fine, showing type information of mod_a_fn.
However, suppose I have one more executable(tool.zig) to build that is used as a tool. If I added it in build.zig by b.installArtifact(), ZLS suddenly stops showing the type information of @import("root") while it still can compile without errors.

I guess that b.addExecutable is doing things unexpectedly. Could someone explain why ZLS does not work when I installed more than two executables?

Here’s my build.zig:

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

    const tool = b.addExecutable(.{
        .name = "tool",
        .root_source_file = b.path("tool.zig"),
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(tool);

    const exe = b.addExecutable(.{
        .name = "zig-test",
        .root_source_file = .{ .path = "root.zig" },
        .target = target,
        .optimize = optimize,
    });
    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);
    run_cmd.step.dependOn(b.getInstallStep());

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

Do you use zig version 0.12.0 and zls version 0.12.0 or something else?

Yes, ZLS and Zig is version 0.12.0

This is expected for zls, because there are more than one root modules in your repository.
@import("root") is actually the .root_source_file expected to have a main function for each executable.

The zig way to expose modules is:

const mod_a = b.createModule(.{ .root_source_file = b.path("src/mod_a.zig") });
tool.root_module.addImport("a_name", mod_a);
exe.root_module.addImport("a_name", mod_a);

Use @import("a_name") in both executables to access mod_a.

Thanks for your reply.

@import("root") is actually the .root_source_file expected to have a main function for each executable.

I didn’t know that :slight_smile:

there are more than one root modules in your repository.

Is it common? Or at least is it valid in Zig? (I suppose it is valid because it can compile.)

I believe this kind of import is very useful. I’m trying to write a toy OS and my project looks like:

/src
   pci.zig
   ...
   /arch/x64
      pci.zig

In /src/arch/x64/pci.zig. I want to use structures or something defined in /src/pci.zig. The same is true for other architecture-specific files other than PCI. Creating modules for each files by createModule() seems annoying. I’d prefer to exporting the whole files by @import("root").

To my knowledge, some projects are using this type of import (such as Bun and stdlib). If this is valid as a language, I want ZLS to analyze the import.

I’d appreciate if you told me your opinion or my misunderstanding.

Yes, it is valid and common.
There is one root for each artifact (executable or library) that corresponds to the .root_source_file.

It is fine, but the name root is reserved; you can have one module with your desired namespace, simply call it something else.

It is valid to import(“root”). Standard library is using it for accessing options or other handles placed in your root source file (where main lives). ZLS cannot analyze multiple roots; I find this perfectly normal, because zls does not know which root you are building (tool.zig or root.zig).


For example:

const me = b.createModule(.{ .root_source_file = b.path("src/me.zig") });
tool.root_module.addImport("me", me);
exe.root_module.addImport("me", me);

In your main.zig:

const me = @import("me");

pub fn main() !void {
    me.mod_a.mod_a_fn();
}

me.zig and mod_a.zig remain the same.

ZLS cannot analyze multiple roots; I find this perfectly normal, because zls does not know which root you are building (tool.zig or root.zig ).

It helped me a lot to understand…! It’s really reasonable :slight_smile: And I understand that I can add a module me that exposes files under /src by calling createModule().

One problem occurred regarding creating module me. Suppose that I have below project (tool is deleted cuz it is no more problem, instead mod_b is added):

./src
├── main.zig
├── me.zig
├── mod_a.zig
└── mod_b.zig

mod_b defines one function:

pub fn mod_b_fn() void {
    @import("std").log.info("hello mod_b", .{});
}

mod_a now wants to import mod_b to call mod_bfn()`:

const mod_b = @import("me").mod_b;

pub fn mod_a_fn() void {
    @import("std").log.info("hello mod_a", .{});
    mod_b.mod_b_fn();
}

main function imports me module and just calls mod_a_fn():

const me = @import("me");
pub fn main() !void {
    me.mod_a.mod_a_fn();
}

I added a module me in build.zig as you kindly told me:

    const me = b.createModule(.{
        .root_source_file = b.path("src/me.zig"),
    });
    const exe = b.addExecutable(.{
        .name = "zig-test",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,
    });
    exe.root_module.addImport("me", me);
    b.installArtifact(exe);

This time, ZLS completely works fine, but build fails saying that:

src/mod_a.zig:1:23: error: no module named 'me' available within module me
const mod_b = @import("me").mod_b;

I guess that this issue happens when creating module me, because there is no module named me while creating me.

I know that const mod b = @import("mod_b.zig") works, but it cannot achieve my goal to avoid writing ../../pci.zig in the previous reply. (module_a corresponds to /src/arch/x64/pci.zig and module_b to /src/pci.zig)

Do you know what’s wrong with my approach? I really thank you for your kind support!

So either you have a single root:

// in src/main.zig
pub const me = @import("me.zig");

// in src/mod_a.zig
const me = @import("root").me;

or you create an acyclic graph of modules.
EDIT: I was wrong. zig allows cyclical modules

Oh, sorry. I omit tool artifact for the simplicity here, but my project actually has other ones. So I cannot use @import("root") for ZLS to determine which root to use. Do I still have options?

It looks strange, but try to add this:

    me.addImport("me", me);
1 Like

Wow, it works like a charm!

Before this thread, Zig’s build system was kinda dark matter for me due to its lack of documents and examples, but now I’m starting to understand :slight_smile:
Big thanks for your kind help!

1 Like