Access b.install_path after v0.17

Hi,
Previous to v0.17, I accessed zig-out (or the installed dir) using b.install_path. This was useful because it let me create steps based on the produced files. Example where I used to add a flag to a Qemu command.

b.fmt("file=fat:rw:{s}/{s},format=raw", .{ b.install_path, IMG_DIR_NAME }),

What’s now the best approach to do this?

Thanks in advance

The recommended way has always been to utilise LazyPath, WriteFiles, and the various helper APIs for them instead of the manually dealing paths.

I can’t give an exact answer to the example as its missing too much context: is it an input or output file/dir, if its an input then where is it coming from? from the source tree or generated?

I was looking to Zig Build System ⚡ Zig Programming Language but I couldn’t figure out from there how to actually access the prefix.

It’s an output directory where I’m placing 2 files I generate during the compilation.
It will look something like this:

./img
  |--efi/BOOTX64.EFI         //generated in one compilation step
  \--kernel.elf              //generated in the following compilation step

And here is the code of my compilation:

const std = @import("std");
const IMG_DIR_NAME = "img";

pub fn buildUefi(b: *std.Build) *std.Build.Step.Compile {
    // Create the bootloader executable
    const bootloader = b.addExecutable(.{
        .name = "BOOTX64.EFI",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/bootloader/boot.zig"),
            .target = b.resolveTargetQuery(.{
                .cpu_arch = .x86_64,
                .os_tag = .uefi,
            }),
            .optimize = .Debug,
            .dwarf_format = .@"64",
        }),
        .linkage = .static,
        .use_lld = true,
        .use_llvm = true,
    });
    b.installArtifact(bootloader);

    // Place the executable in the EFI fs
    const install_bootloader = b.addInstallFile(
        bootloader.getEmittedBin(),
        b.fmt("{s}/efi/boot/{s}", .{ IMG_DIR_NAME, bootloader.name }),
    );
    b.getInstallStep().dependOn(&install_bootloader.step);
    return bootloader;
}
pub fn buildKernel(b: *std.Build) *std.Build.Step.Compile {
    // Create the kernel executable
    const kernel = b.addExecutable(.{
        .name = "kernel.elf",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/kernel/main.zig"),
            .target = b.resolveTargetQuery(.{
                .os_tag = .freestanding,
                .cpu_arch = .x86_64,
                .ofmt = .elf,
            }),
            .code_model = .kernel,
        }),
        .linkage = .static,

        .use_lld = true,
        .use_llvm = true,
    });
    kernel.entry = .{ .symbol_name = "kernelEntry" };
    kernel.linker_script = b.path("src/kernel/linker.ld");
    b.installArtifact(kernel);
    // Place the kernel inside EFI
    const install_kernel = b.addInstallFile(
        kernel.getEmittedBin(),
        b.fmt("{s}/{s}", .{ IMG_DIR_NAME, kernel.name }),
    );
    b.getInstallStep().dependOn(&install_kernel.step);

    return kernel;
}
// omiting non related code:
pub fn build(b: *std.Build) void {
    const ovmf_path = b.option([]const u8, "ovmf-path", "Path to OVMF firmware file") orelse findOvmfPath(b);
    const log_level_str = b.option([]const u8, "log-level", "Log level of the application.") orelse "info";
    const log_level: std.log.Level = std.meta.stringToEnum(std.log.Level, log_level_str) orelse @panic("Invalid log level provided");

    const build_options = b.addOptions();
    build_options.addOption(std.log.Level, "log_level", log_level);

    const bootloader = buildUefi(b);
    setupBootloaderTests(b, build_options);
    const kernel = buildKernel(b);

    build_options.addOption([]const u8, "kernel_main", kernel.name);
    build_options.addOption([]const u8, "kernel_path", b.fmt("{s}/{s}", .{ IMG_DIR_NAME, kernel.name }));
    bootloader.root_module.addOptions("build_options", build_options);

    const qemu_args = [_][]const u8{
        "qemu-system-x86_64",
        "-m",
        "512M",
        "-bios",
        ovmf_path,
        // "-drive",
        // "if=pflash,format=raw,unit=0,file.filename=" ++ ovmf_path ++ ",file.locking=off,readonly=on",
        "-drive",
        b.fmt("file=fat:rw:{s}/{s},format=raw", .{ b.install_path, IMG_DIR_NAME }),
        "-nographic",
        "-serial",
        "mon:stdio",
        "-no-reboot",
        "-enable-kvm",
        "-cpu",
        "host",
    };
    const qemu_cmd = b.addSystemCommand(&qemu_args);
    qemu_cmd.step.dependOn(b.getInstallStep());
    const run_qemu_step = b.step("run", "Run QEMU");
    run_qemu_step.dependOn(&qemu_cmd.step);

    const debug_qemu_cmd = b.addSystemCommand(&qemu_args ++ [_][]const u8{"-S", "-s"});
    debug_qemu_cmd.step.dependOn(b.getInstallStep());
    const debug_qemu_step = b.step("debug", "Run QEMU and stop execution before boot.");
    debug_qemu_step.dependOn(&debug_qemu_cmd.step);
}

Makes sense.

The API deliberately limits what you can do with the install prefix, anything other than copying files to it should go through a WriteFile step instead as that gives you a lot more features.

something like this:

pub fn buildUefi(b: *std.Build, img_wf: *std.Build.Step.WriteFile) *std.Build.Step.Compile {
    // ...
    _ = img_wf.addCopyFile(bootloader.getEmittedBin(), "efi/boot/");
    // ...
}

pub fn buildKernel(b: *std.Build, img_wf: *std.Build.Step.WriteFile) *std.Build.Step.Compile {
    // ...
    _ = img_wf.addCopyFile(kernel.getEmittedBin(), "");
    // ...
}

pub fn build(b: *std.Build) void {
    const img_wf = b.addWriteFiles();

    buildUefi(b, img_wf);
    buildKernel(b, img_wf);
    // ...
    qemu_cmd.addDirectoryArg2(img_wf.getDirectory(), .{ .prefix = "file=fat:rw:", .suffix = ",format=raw" });

    // if you need to install it
    b.installDirectory(.{
        .install_dir = .prefix,
        .install_subdir = IMG_DIR_NAME,
        .source_dir = img_wf.getDirectory(),
    });
}

The above removes the need for many of your explicit dependOn as the lazy path API’s will do this for you :3