Is it possible to `@import()` public declarations from `.zig` Modules?

I’m working on a custom build step for my library that can be imported into a project’s build.zig. The build step needs to access public declarations from a project’s main.zig (or similar). These declarations should be passed to a Type generating function from my library that will return a customized build step to the project’s build.zig. So far I’ve tried two approaches:

  1. Import the main.zig into the project’s build.zig, then pass the declarations directly to my library’s custom step Type function. This fails because the declarations I’m attempting to import require other imports to work, and those imports resolve to a build.zig when the main.zig is imported.

  2. Attempt to import from the project’s main_exe.root_module (built from b.addExectuable(). But I’m not sure this is possible or the right approach.

If there’s an alternative approach here, I’m open to other ideas.

I’ve also posted this question on Discord here but it got a little lost in the shuffle.

Generally I don’t think what you are trying to do is a good idea. As you realized you can’t import main.zig without actually replicating the other project’s build.zig. Note it is possible to access main.zig, in a module not used in build.zig by using @import("root"); though.

Can you explain more in general what your custom build step should be able to do?

1 Like

Appreciate the response. I’ll try and post code snippets later when I’m back on my computer, but the premise is as follows:

I’m working on a CLI library, Cova, that enables users to create robust CLIs for their Zig projects. My next step in the project is adding generation for Manpages, Tab-Completion Scripts, and other “meta” docs. These docs will be generated based on the root “Command” that users set up with Cova in their main.zig (or wherever they decide to make their CLI).

Much of the doc gen code is already complete and I’ve actually gotten this working as a separate executable. However, that requires users to include my executable source directly in their projects and edit it to include their root Command, which is obviously not ideal. What I’d like to do is push this generation into a build step. My idea being that users would import a Module from Cova into their build.zig, then somehow pass their root Command to that Module to start the doc gen in a build step.

I am not completely sure I understand what exactly you are doing, so I am not sure whether it is the same case, or you are doing something differently.

That said, what @kristoff describes here sounds very similar:

1 Like

Ah in that case @import("cova") is possible from your library users build.zig so they can import your custom build step from there.

1 Like

Yea, I got that part working (I should’ve clarified better). The issue is passing public declarations (the aforementioned root Command) from the user’s main.zig (or whichever .zig they make their CLI in) to my library module in order to create the build step.

I think what’s complicating things is that one of the public declarations is a comptime generic Type and the other depends on it (the custom Command Type and the root Command respectively). Which makes me think that I need to get these declarations after the file they’re in has been compiled, but I’m not sure if that’s correct or possible.

If I’m understading the problem correctly (the user defines commands at comptime then passes the command definition to parseArgs to parse at runtime), I would suggest starting off with something like this:

  • The user’s executable depends on the Cova module.
  • The Cova docs generator depends on the Cova module and the executable’s root module.
  • The Cova docs generator scans through the executable’s root module for command declarations and generates docs for them.

Quick minimal project to get the idea across:

// build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const cova_mod = b.addModule("cova", .{
        .root_source_file = .{ .path = "cova.zig" },
        .target = target,
        .optimize = optimize,
    });

    const main_exe = b.addExecutable(.{
        .name = "main",
        .root_source_file = .{ .path = "main.zig" },
        .target = target,
        .optimize = optimize,
    });
    main_exe.root_module.addImport("cova", cova_mod);

    const cova_generator_exe = b.addExecutable(.{
        .name = "cova_generator",
        .root_source_file = .{ .path = "cova_generator.zig" },
        .target = target,
        .optimize = optimize,
    });
    cova_generator_exe.root_module.addImport("cova", cova_mod);
    cova_generator_exe.root_module.addImport("user_program", &main_exe.root_module);

    const run = b.step("run", "Run main.exe");
    run.dependOn(&b.addRunArtifact(main_exe).step);

    const generate = b.step("generate", "Generate Cova docs for main.exe");
    generate.dependOn(&b.addRunArtifact(cova_generator_exe).step);
}
// cova.zig

pub const Command = struct {
    name: []const u8,
    description: []const u8,
};
// cova_generator.zig

const std = @import("std");
const cova = @import("cova");
const user_program = @import("user_program");

pub fn main() !void {
    const stdout_file = std.io.getStdOut().writer();
    var bw = std.io.bufferedWriter(stdout_file);
    const stdout = bw.writer();

    inline for (@typeInfo(user_program).Struct.decls) |decl| {
        const decl_type = @TypeOf(@field(user_program, decl.name));
        if (decl_type != cova.Command) continue;

        const command = @field(user_program, decl.name);
        try stdout.print("{s}: {s}\n", .{ command.name, command.description });
    }

    try bw.flush();
}
// main.zig

const std = @import("std");
const cova = @import("cova");

pub const create_command: cova.Command = .{
    .name = "create",
    .description = "create a thing",
};

pub const delete_command: cova.Command = .{
    .name = "delete",
    .description = "delete a thing",
};

pub fn main() void {
    std.debug.print("Hello, World!", .{});

    // cova.parseArgs(...);
}

Ideally Cova’s build.zig would provide helper functions for setting up these steps.

In the OP you say you’ve already tried importing the exe’s root module, so I’m curious why it didn’t work for you.

2 Likes

Hey, appreciate the reference. This is somewhat related, but funnily enough I think it’s the behavior I’m trying to avoid.

This is very close to what I had originally, and what I was trying to avoid because I didn’t want the user to have to include the generator’s source directly in their codebase.

However, looking at it again, maybe there’s a simple in between. Could I make the generator’s source a module in Cova, then make an exe from that in the user’s build.zig as you’ve done here? If that’s possible, the user still has to create the exe I’m their own build.zig as you’ve done, but at least the source is still just a module. Optimally, I would have helpers in Covas build.zig` that could create this exe, but I run into the same issue of importing the user’s declarations.

To your question on importing the user’s root module: I was attempting to import the user’s main.zig directly into the user’s build.zig, then push the declarations (Command & Type) to my generator code (which is available via Cova’s build.zig). In this case the “user code” is a separate project of mine I’m using to test this integration. When I attempted to build with this setup, I was unable to use the declarations because they relied on Cova which was being resolved as the Cova build.zig at the point in the build.

Apologies if this is a bit confusing. I’ll be able to post the code and errors in a few hours if needed.

That’s the idea I had, at least. Cova’s build.zig would export some function like this:

pub fn addGenerateCovaDocs(
    b: *std.Build,
    cova_dep: *std.Build.Dependency,
    program_mod: *std.Build.Module,
) *std.Build.Step.Run {
    const cova_generator_exe = cova_dep.artifact("cova_generator");
    cova_generator_exe.root_module.addImport("cova", cova_dep.module("cova"));
    cova_generator_exe.root_module.addImport("program", program_mod);

    return b.addRunArtifact(cova_generator_exe);
}

Which could then be imported and used by the user like this:

const cova = @import("cova"); // Cova's build.zig

const cova_dep = b.dependency("cova", .{
    .target = target,
    .optimize = optimize,
});

const exe = ...
exe.root_module.addImport("cova", cova_dep.module("cova"));

const generate = b.step("generate", "Generate Cova docs for main.exe");
const generate_docs = cova.addGenerateCovaDocs(b, cova_dep, &main_exe.root_module);
generate.dependOn(&generate_docs.step);
1 Like

I think this might be what I need! I’ll try it out as soon as I can.

Thank you!

I finally got some time to sit down and work on this. Your solution worked exactly like I was hoping!

I did have to do some contrived stuff to get my Config structs to the Generator using Build Options though. I’m hoping this is temporary until Build Options are able to support Structs, Enums, Unions, etc. That said, I figured I’d post what I did here for posterity and in case anyone knows of a better way.

Cova’s build.zig function.

/// Add Cova's Meta Doc Generation Step to a project's `build.zig`.
pub fn addCovaDocGenStep(
    b: *std.Build,
    /// The Cova Dependency of the project's `build.zig`.
    cova_dep: *std.Build.Dependency,
    /// The Program Module where the Command Type and Setup Command can be found.
    program_mod: *std.Build.Module,
    /// The Config for Meta Doc Generation.
    doc_gen_config: generate.MetaDocConfig,
) *std.Build.Step.Run {
    const cova_gen_exe = cova_dep.artifact("cova_generator");
    cova_gen_exe.root_module.addImport("cova", cova_dep.module("cova"));
    cova_gen_exe.root_module.addImport("program", program_mod);

    const md_conf_opts = b.addOptions();
    var sub_conf_map = std.StringHashMap(?*std.Build.Step.Options).init(b.allocator);
    sub_conf_map.put("manpages_config", null) catch @panic("OOM");
    sub_conf_map.put("tab_complete_config", null) catch @panic("OOM");

    inline for (@typeInfo(generate.MetaDocConfig).Struct.fields) |field| {
        switch(@typeInfo(field.type)) {
            .Struct, .Enum => continue,
            .Optional => |optl| {
                switch (@typeInfo(optl.child)) {
                    .Struct => |struct_info| {
                        const maybe_conf = @field(doc_gen_config, field.name);
                        if (maybe_conf) |conf| {
                            const doc_conf_opts = b.addOptions();
                            inline for (struct_info.fields) |s_field| {
                                if (@typeInfo(s_field.type) == .Enum) {
                                    doc_conf_opts.addOption(usize, s_field.name, @intFromEnum(@field(conf, s_field.name)));
                                    continue;
                                }
                                doc_conf_opts.addOption(s_field.type, s_field.name, @field(conf, s_field.name));
                            }
                            doc_conf_opts.addOption(bool, "provided", true);
                            sub_conf_map.put(field.name, doc_conf_opts) catch @panic("OOM");
                        }
                        continue;
                    },
                    else => {},
                }
            },
            .Pointer => |ptr| {
                if (ptr.child == generate.MetaDocConfig.MetaDocKind) {
                    var kinds_list = std.ArrayList(usize).init(b.allocator);
                    for (@field(doc_gen_config, field.name)) |kind|
                        kinds_list.append(@intFromEnum(kind)) catch @panic("There was an issue with the Meta Doc Config.");
                    md_conf_opts.addOption(
                        []const usize,
                        field.name,
                        kinds_list.toOwnedSlice() catch @panic("There was an issue with the Meta Doc Config."),
                    );
                    continue;
                }
            },
            else => {},
        }
        md_conf_opts.addOption(
            field.type,
            field.name,
            @field(doc_gen_config, field.name),
        );
    }
    cova_gen_exe.root_module.addOptions("md_config_opts", md_conf_opts);
    var sub_conf_map_iter = sub_conf_map.iterator();
    while (sub_conf_map_iter.next()) |conf| {
        cova_gen_exe.root_module.addOptions(
            conf.key_ptr.*,
            if (conf.value_ptr.*) |conf_opts| conf_opts
            else confOpts: {
                const conf_opts = b.addOptions();
                conf_opts.addOption(bool, "provided", false);
                break :confOpts conf_opts;
            }
        );
    }

    return b.addRunArtifact(cova_gen_exe);
}

Relevant code from generator.zig:

const cova = @import("cova");
const generate = cova.generate; 

/// This is a reference module for the program being built. Typically this is the main `.zig` file
/// in a project that has both the `main()` function and `setup_cmd` Command. 
const program = @import("program");
/// This is a reference to the Meta Doc Build Options passed in from `build.zig`.
const md_config = @import("md_config_opts");
/// Manpages Config
const manpages_config = optsToConf(generate.ManpageConfig, @import("manpages_config"));
/// Tab Completion Config
const tab_complete_config = optsToConf(generate.TabCompletionConfig, @import("tab_complete_config"));

/// Translate Build Options to Meta Doc Generation Configs.
///TODO Refactor this once Build Options support Types.
fn optsToConf(comptime ConfigT: type, comptime conf_opts: anytype) ?ConfigT {
    if (!conf_opts.provided) return null;
    var conf = ConfigT{};
    for (@typeInfo(ConfigT).Struct.fields) |field| {
        if (std.mem.eql(u8, field.name, "provided")) continue;
        @field(conf, field.name) = @field(conf_opts, field.name);
    }
    return conf;
}

Relevant code from the project’s build.zig:

    // Docs
    // - Meta
    const cova_gen = @import("cova").addCovaDocGenStep(b, cova_dep, &zing_exe.root_module, .{
        .kinds = &.{ .manpages, .bash },
        .manpages_config = .{
            .local_filepath = "meta",
            .version = "0.1.0",
            .ver_date = "04 FEB 2024",
            .man_name = "User's Manual",
            .author = "00JCIV00",
            .copyright = "Copyright info here",
        },
        .tab_complete_config = .{
            .local_filepath = "meta",
            .include_opts = true,
        }
    });
    const meta_doc_gen = b.step("gen-meta", "Generate Meta Docs using Cova");
    meta_doc_gen.dependOn(&cova_gen.step);
1 Like