Compiling .asm files using build.zig

Hey ziggers,

I am trying to figure out how I could assemble a bunch of individual .asm files to executables using the zig build system.

I am following this x86_64 assembly guide using the nasm assembler/assembly flavor (i guess you call it?) to generate object files and then I have to link them to create executables like so:

nasm -f elf64 -o hello.o hello.asm
ld -o hello hello.o

Can you give me hints how I could use the zig build system to ideally assemble each .asm file I specify in build.zig to executables?

I’ve seen that you can add cli tools with std.Build.addSystemCommand, but im currently just stuck at figuring out the rest of accessing the generated object file and telling zig to link that etc.

tysm in advance!

you can do

const std = @import("std");

pub fn build(b: *std.Build) void {
    const run_nasm = b.addSystemCommand(&.{ "nasm", "-f", "elf64", "-MD" });
    _ = run_nasm.addDepFileOutputArg2("deps.d", .{});
    run_nasm.addArg("-o");
    const obj_file = run_nasm.addOutputFileArg2("hello.o", .{});
    run_nasm.addFileArg(b.path("hello.asm"));

    const mod = b.createModule(.{
        .target = b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .linux }), // what you target in your hello.asm file
    });
    mod.addObjectFile(obj_file);

    const exe = b.addExecutable(.{
        .name = "hello",
        .root_module = mod,
    });

    b.installArtifact(exe);
}
3 Likes

When you add git+https://github.com/allyourcodebase/nasm.git as a lazy dependency and use

    const run_nasm = if (b.systemIntegrationOption("nasm", .{}))
        b.addSystemCommand(&.{"nasm"})
    else nasm: {
        const nasm_dep = b.lazyDependency("nasm", .{
            .optimize = .ReleaseFast,
        }) orelse return;
        const nasm_exe = nasm_dep.artifact("nasm");
        break :nasm b.addRunArtifact(nasm_exe);
    };

    run_nasm.addArgs(&.{ "-f", "elf64", "-MD" });

instead of const run_nasm = b.addSystemCommand(&.{ "nasm", "-f", "elf64", "-MD" });, nasm does not need to be installed on the system, but system nasm can still be used with -fsys=nasm.

3 Likes

My comments above only work on master. For 0.16.0:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const run_nasm = if (b.systemIntegrationOption("nasm", .{}))
        b.addSystemCommand(&.{"nasm"})
    else nasm: {
        const nasm_dep = b.lazyDependency("nasm", .{
            .optimize = .ReleaseFast,
        }) orelse return;
        const nasm_exe = nasm_dep.artifact("nasm");
        break :nasm b.addRunArtifact(nasm_exe);
    };

    run_nasm.addArgs(&.{ "-f", "elf64", "-MD" });

    _ = run_nasm.addDepFileOutputArg("deps.d");
    run_nasm.addArg("-o");
    const obj_file = run_nasm.addOutputFileArg("hello.o");
    run_nasm.addFileArg(b.path("hello.asm"));

    const mod = b.createModule(.{
        .target = b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .linux }), // what you target in your hello.asm file
    });
    mod.addObjectFile(obj_file);

    const exe = b.addExecutable(.{
        .name = "hello",
        .root_module = mod,
    });

    b.installArtifact(exe);
}

This also works on master, but is deprecated.

3 Likes

Wow this is really helpful, thx!

I ended up with this implementation:

install nasm as a dependency with zig fetch git+https://github.com/allyourcodebase/nasm.git --save, then in build.zig

const std = @import("std");

const Build = std.Build;
const Step = Build.Step;
const LazyPath = Build.LazyPath;

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

    const nasm_dep = b.dependency("nasm", .{
        .optimize = optimize,
        .target = target,
    });

    const nasm_exe = nasm_dep.artifact("nasm");

    // build and install .asm files
    try installNasm(b, nasm_exe, b.path("src/hello_world.asm"));
    try installNasm(b, nasm_exe, b.path("src/some.asm"));
}

fn installNasm(b: *Build, nasm_exe: *Step.Compile, asm_path: LazyPath) !void {
    const nasm_tool = b.addRunArtifact(nasm_exe);

    // get the file name without extension
    const name = name: {
        const basename = asm_path.basename(b, null);
        const index = std.mem.lastIndexOfScalar(u8, basename, '.') orelse basename.len;
        break :name basename[0..index];
    };

    const obj_name = try std.mem.concat(b.allocator, u8, &[_][]const u8{ name, ".o" });

    nasm_tool.addArgs(&.{ "-f", "elf64", "-o" });
    const obj_file = nasm_tool.addOutputFileArg(obj_name);
    nasm_tool.addFileArg(asm_path);

    // assembly can only be run on linux x86_64
    const mod = b.createModule(.{
        .target = b.resolveTargetQuery(.{
            .cpu_arch = .x86_64,
            .os_tag = .linux,
        }),
    });
    mod.addObjectFile(obj_file);

    const exe = b.addExecutable(.{
        .name = name,
        .root_module = mod,
    });

    b.installArtifact(exe);
}

I’m obv. not very good with the build system, but to my understanding each Step.Run step can be assigned argv arguments, but can only be run once, so I am basically creating one run step per .asm file I want to assemble (but im pretty sure that will reuse the nasm artifact from the nasm dependency, so no problemo).

Improvements are welcome, cheers!

Edit: typos

1 Like

This means, that the cli can change the target and optimize of nasm. This is not what you want: Since nasm will be executed by the build system, any non native target does not really make sense. For optimize, I would use one of ReleaseFast and ReleaseSafe (optimize the compilation of nasm; it is cached).

    const nasm_dep = b.dependency("nasm", .{
        .optimize = .ReleaseFast,
    });

There is no problem this this, but it is also not really necessary. When using addOutputFileArg(sub_path), the build system will create a directory inside the cache dir and add [path to this dir]/[sub_path] as an argument (e.g. .zig-cache/o/4f924c86a282623275f7c1886d9e9f3a/hello_world.o). You can just always use e.g. obj.o as the sub_path. When you do zig build --verbose, the build system prints the commands it executes. This includes zig compiler commands as well as the nasm commands. There you can see, what addOutputFileArg() does. This might require you to clear the .zig-cache.

In the comments above, I have this line, which you don’t have:

    _ = run_nasm.addDepFileOutputArg("deps.d");

As long as your .asm files are simple assembly files, this shouldn’t matter, but nasm also supports include macros. If hello_world.asm would include a foo.asm file, and then you only change foo.asm, but not hello_world.asm, the build system will assume, that nothing changed and use a already cached object file. By using -MD deps.d, the run step communicates what files it depends on. This would then also include the foo.asm.