zig

How to objcopy a bin file as part of a Zig build script


How can I export a binary output from my elf output as part of my Zig build process? This should happen as part of the default build command zig build.

I am using Zig 0.11.

My build.zig contains the code below. I have added comments to describe what I think should be happening.

const std = @import("std");
const Builder = std.build.Builder;

pub fn build(b: *Builder) void {
    const optimization = std.builtin.OptimizeMode.ReleaseFast;
    const cpu_model = &std.Target.arm.cpu.cortex_m0;

    // elf should be a build step that emits output.elf
    const elf = b.addExecutable(.{
        .name = "output.elf",
        .root_source_file = .{ .path = "src/startup.zig" },
        .target = .{
            .cpu_arch = .thumb,
            .os_tag = .freestanding,
            .abi = .none,
            .cpu_model = .{ .explicit = cpu_model },
        },
        .optimize = optimization,
    });

    elf.setLinkerScript(.{ .path = "src/STM32Z/MCU/STM32L052.ld" });
    
    // bin should be a build step that objcopy's output.bin from output.elf
    const bin = b.addObjCopy(.{ elf.getEmittedBin(), .{
        .format = .bin,
    });
    // This step requires output.elf to exist
    bin.step.dependOn(&elf.step);
    
    // This is the step that I want to be completed when I run `zig build`.
    // I would expect zig build to generate the elf and then the bin file
    b.default_step = &bin.step;
}

This builds with no error at all. Yet a zig-out directory is not created, so I cannot find either output.elf or output.bi.

I'm not really clear on:

  1. If elf.getEmittedBin() is the correct thing to pass into the b.addObjCopy method
  2. How to debug what steps are getting run at all.
  3. How to declare what my output artifact is and where it should go. I assume I have to somehow specify that output.bin is the thing I want.

Solution

  • Not sure if this was the case in 2023, but the latest available now (0.14.0-dev.1713) you can call std.Build.Step.Compile.addObjCopy and only need to set the build dependency for the object copy install step:

    b.installArtifact(elf); // as normal
    
    const bin = elf.addObjCopy(.{.format: .bin}); // no need for the elf emitted path
    const installBin = b.addInstallBinFile(bin.getOutput(), "output.bin");
    b.getInstallStep().dependOn(&installBin.step);
    

    so the full build script can become:

    const std = @import("std");
    const Builder = std.build.Builder;
    
    pub fn build(b: *Builder) void {
        const optimization = std.builtin.OptimizeMode.ReleaseSmall;
        const cpu_model = &std.Target.arm.cpu.cortex_m0;
    
        // Build the elf
        const elf = b.addExecutable(.{
            .name = "output.elf",
            .root_source_file = .{ .path = "src/startup.zig" },
            .target = .{
                .cpu_arch = .thumb,
                .os_tag = .freestanding,
                .abi = .none,
                .cpu_model = .{ .explicit = cpu_model },
            },
            .optimize = optimization,
            .single_threaded = true,
        });
        elf.setLinkerScript(.{ .path = "src/STM32Z/MCU/STM32L052.ld" });
    
        b.installArtifact(elf);
    
        const bin = elf.addObjCopy(.{.format: .bin});
        const installBin = b.addInstallBinFile(bin.getOutput(), "output.bin");
        b.getInstallStep().dependOn(&installBin.step);
    }