Build.zig addObject static library

I would like to include the contents of one static library in another static library. I tried to use addObject for this purpose, but addObject asserts that object.kind == .obj. What’s the recommended approach?

1 Like

The build.zig in question:

1 Like

If you have a static or dynamic library you can use linkLibrary.

That doesn’t seem to be doing the trick:

$ nm libzimg.a 


(meanwhile, compiled linked by libtool):

$ nm .libs/libzimg.a  | wc -l

Looking at --verbose-link output, the “linked” archives are not included in the ar invocation:

ar rcs /home/user/src/github/zimg/zig-cache/o/8cec577754ed3e368cc61f01dafd5ff0/libzimg.a /home/user/src/github/zimg/zig-cache/o/eb3717467756580a8ef4f855e6930fac/dummy.o

It is not clear to me exactly what are you trying to achieve.
I am guessing that you have one library called A and you want to create a new library B with all the contents of A plus some objects. If that is the case, as a work around, you can built library B without A contents and then merge them to AB using:

ar -x libA.a
ar -x libB.a
ar -c libAB.a *.o
1 Like

Looking at make V=1 output, that’s what libtool does. Unfortunate that build.zig doesn’t do this, but oh well. Thanks for your help.

Try to pass the flag --whole-archive (Force load of all members in a static library) to the linker.

In the --verbose-link output, I don’t see ld being invoked anywhere, which would make sense, since no executables/shared libraries are being linked. Seems like that option just forces inclusion of every object in the archive:

           For each archive mentioned on the command line after the --whole-archive option, include every object file in the archive in the link, rather than searching the archive  for  the  required  object
           files.  This is normally used to turn an archive file into a shared library, forcing every object to be included in the resulting shared library.  This option may be used more than once.

           Two  notes  when using this option from gcc: First, gcc doesn’t know about this option, so you have to use -Wl,-whole-archive.  Second, don’t forget to use -Wl,-no-whole-archive after your list of
           archives, because gcc will add its own list of archives to your link and you may not want this flag to affect those as well.

Currently trying to implement that behavior with this function:

fn addLibraryObjects(b: *std.Build, destination: *std.Build.Step.Compile, source: *std.Build.Step.Compile) !void {

    const src_path = source.getEmittedBin();

    var prev_cwd = try std.fs.cwd().openDir(".", .{});
    defer prev_cwd.close();
    defer prev_cwd.setAsCwd() catch @panic("failed to cd back");

    const tmp = b.makeTempPath();
    var d = try std.fs.cwd().openDir(tmp, .{ .iterate = true });
    defer d.close();

    try d.setAsCwd();

    _ ={ "ar", "x", src_path.getPath(b) });

    var d_iter = d.iterate();

    while (try |ent| {
        if (ent.kind == .file or (ent.kind == .unknown and (try d.statFile( == .file)) {
            destination.addObjectFile(.{ .path = try d.realpathAlloc(b.allocator, });

But it fails with this error:

getPath() was called on a GeneratedFile that wasn't built yet.
  source package path: /home/user/src/github/zimg
  Is there a missing Step dependency on step 'zig build-lib sse Debug native'?

I’m guessing this is caused by my misunderstanding of the build system? How can I make this a separate step that gets the generated path at step run time, not configure time?

Declaring to run ar at configure time:

    const run_ar = b.addSystemCommand(&.{
pub const AddLibraryObjectsStep = struct {
    const Self = @This();

    step: std.Build.Step,
    b: *std.Build,
    destination: *std.Build.Step.Compile,
    source: *std.Build.Step.Compile,
    tmp_dir: []const u8,

    pub fn create(b: *std.Build, destination: *std.Build.Step.Compile, source: *std.Build.Step.Compile) *AddLibraryObjectsStep {
        const alos = b.allocator.create(Self) catch @panic("out of memory");

        alos.* = .{
            .step = std.Build.Step.init(
                    .id = .custom,
                    .name = std.fmt.allocPrint(b.allocator, "{s} <- {s} objects", .{, }) catch "",
                    .owner = b,
                    .makeFn = make,
            .tmp_dir = b.makeTempPath(),
            .b = b,
            .destination = destination,
            .source = source,

        const run_ar = alos.b.addSystemCommand(&.{ "ar", "xv", "--output", alos.tmp_dir });


        return alos;

    fn make(step: *std.Build.Step, prog_node: *std.Progress.Node) !void {
        _ = prog_node;

        const alos = @fieldParentPtr(Self, "step", step);

        var prev_cwd = try std.fs.cwd().openDir(".", .{});
        defer prev_cwd.close();
        defer prev_cwd.setAsCwd() catch @panic("failed to cd back");

        var d = try std.fs.cwd().openDir(alos.tmp_dir, .{ .iterate = true });
        defer d.close();

        try d.setAsCwd();

        var d_iter = d.iterateAssumeFirstIteration();

        std.debug.print(" --- {s}\n", .{alos.tmp_dir});

        while (try |ent| {
            if (ent.kind == .file or (ent.kind == .unknown and (try d.statFile( == .file)) {
                std.debug.print(" found {s}\n", .{});
                alos.destination.addObjectFile(.{ .path = try d.realpathAlloc(alos.b.allocator, });
pub fn addLibraryObjects(b: *std.Build, destination: *std.Build.Step.Compile, source: *std.Build.Step.Compile) !void {
    const alos = AddLibraryObjectsStep.create(b, destination, source);

This could have worked, but it turns out that Zig doesn’t actually set the names of the generated object files correctly, so doing

foo.addCSourceFile(.{.files = &.{

produces incorrect results and replaces one of the objects with the other:

$ nm .libs/libzimg.a | gawk -F '(_la-|/)' ' /:/ {print $NF}' | sort >good_libs
$ nm zig-out/lib/libzimg.a | gawk -F '(_la-|/)' ' /:/ {print $NF}' | sort >bad_libs
$ diff bad_libs good_libs

> cpuinfo.o:
> cpuinfo_x86.o:
> depth_convert_x86.o:
> dither_x86.o:
> graph.o:
> operation_impl_x86.o:
> resize_impl_x86.o:
> unresize_impl_x86.o:
> x86util.o:

Ugh. I guess I have to set the object names manually now, fucking hell. Autotools may be complex, but at least have proper abstractions for these technicalities.

(also zig passes absolute paths to ar, causing it to complain on x, that can’t possibly be portable)

I’m slowly going through my project and removing absolute paths, too. It’s quite a bit of work to get that right but there’s still a couple lurking around. You’re probably well aware of this, but you can use std.fs.path.join for that - I just made some helper functions to cover up some of the boilerplate, but this seems like what you have to do right now.

That makes sense. The issue is that Zig itself seems to insert these absolute paths, even when relative paths are given:
ar rcs /home/user/src/github/zimg/zig-cache/o/2220447880fd619ad038890ac99f6c29/libavx2.a /home/user/src/github/zimg/zig-cache/o/64da418a964c65ff4a46a9459e5af54e/operation_impl_avx2.o /home/user/src/github/zimg/zig-cache/o/bc7be4f3f6da7c2fcc6a411fb6c1b68e/depth_convert_avx2.o /home/user/src/github/zimg/zig-cache/o/7764edcb1e902e0fee575923b0ac64c4/dither_avx2.o /home/user/src/github/zimg/zig-cache/o/64a0b35fd0bca2eaa74f00b3e16edd19/error_diffusion_avx2.o /home/user/src/github/zimg/zig-cache/o/0238dfdbdf6714c0ff632df9056f61e0/resize_impl_avx2.o

1 Like

– edited to add current working directory –

I went through my code to gather some of this stuff up - you could easily do this another way, but I figured I’d post it here if you wanted to mine some of this for content. I really don’t like dealing with path stuff either… it’s boring and tedious. I’m trimming some stuff out here for readability but this works:

// getting my paths together from an init function...

    const cwd_path = std.fs.cwd().realpathAlloc(self.allocator, ".")
        catch @panic("Out of Memory");

    self.current_directory = cwd_path;

    self.source_extension = config.source_extension;

    self.source_directory = std.fs.path.join(self.allocator, &.{ cwd_path, config.source_directory })
        catch @panic("Out of Memory");

    self.target_directory = std.fs.path.join(self.allocator, &.{ cwd_path, config.target_directory })
        catch @panic("Out of Memory");

    self.zigsrc_directory = std.fs.path.join(self.allocator, &.{ cwd_path, config.zigsrc_directory })
        catch @panic("Out of Memory");

// helper functinos for quality of life improvements:

pub fn appendSourceDirectory(self: *Self, source_name: []const u8) []const u8 {
    return std.fs.path.join(self.allocator, &.{ self.source_directory, source_name })
        catch @panic("Out of Memory");

pub fn appendLibraryDirectory(self: *Self, source_name: []const u8) []const u8 {
    return std.fs.path.join(self.allocator, &.{ self.zigsrc_directory, "lib", source_name })
        catch @panic("Out of Memory");

pub fn appendCudaDirectory(self: *Self, source_name: []const u8) []const u8 {
    return std.fs.path.join(self.allocator, &.{ self.zigsrc_directory, "cuda", source_name })
        catch @panic("Out of Memory");

pub fn appendTargetDirectory(self: *Self, target_name: []const u8) []const u8 {
    return std.fs.path.join(self.allocator, &.{ self.target_directory, target_name })
        catch @panic("Out of Memory");

pub fn appendZigsrcDirectory(self: *Self, zigsrc_name: []const u8) []const u8 {
    return std.fs.path.join(self.allocator, &.{ self.zigsrc_directory, zigsrc_name })
        catch @panic("Out of Memory");

Anyhow, I hope that helps - good luck with your project :slight_smile:

1 Like

I left out one important part, whoops lol:

    const cwd_path = std.fs.cwd().realpathAlloc(self.allocator, ".")
        catch @panic("Out of Memory");

Thanks for your help! This is very interesting.

I resolved my issue with a fittingly cursed hack:

pub const AddCSourceFilesWithOptionsAndCorrectObjectNamesOptions = struct {
    root_source_file: ?std.Build.LazyPath = null,
    target: std.Build.ResolvedTarget,
    code_model: std.builtin.CodeModel = .default,
    optimize: std.builtin.OptimizeMode,
    max_rss: usize = 0,
    link_libc: ?bool = null,
    single_threaded: ?bool = null,
    pic: ?bool = null,
    strip: ?bool = null,
    unwind_tables: ?bool = null,
    omit_frame_pointer: ?bool = null,
    sanitize_thread: ?bool = null,
    error_tracing: ?bool = null,
    use_llvm: ?bool = null,
    use_lld: ?bool = null,
    zig_lib_dir: ?std.Build.LazyPath = null,

    files: []const []const u8 = &.{},
    flags: []const []const u8 = &.{},
    link_libcpp: ?bool = null,

pub fn addCSourceFilesWithOptionsAndCorrectObjectNames(
    b: *std.Build,
    destination: *std.Build.Step.Compile,
    options: AddCSourceFilesWithOptionsAndCorrectObjectNamesOptions,
) void {
    var obj_opt: std.Build.ObjectOptions = .{
        .name = undefined,
        .target = undefined,
        .optimize = undefined,

    inline for (@typeInfo(AddCSourceFilesWithOptionsAndCorrectObjectNamesOptions).Struct.fields) |field| {
        if (@hasField(std.Build.ObjectOptions, {
            @field(obj_opt, = @field(options,;
        } else if (comptime !std.mem.eql(u8, "files", and !std.mem.eql(u8, "flags", and !std.mem.eql(u8, "link_libcpp", {
            @compileLog("unaccounted field " ++;

    for (options.files) |file| { = mangleObjectNameMonstrosity(b.allocator,, file) catch @panic("oom");
        const obj = b.addObject(obj_opt);

        obj.root_module.include_dirs = destination.root_module.include_dirs.clone(b.allocator) catch @panic("oom");

        if (options.link_libcpp == true) obj.linkLibCpp(); // lol? I think that's just missing?

        obj.addCSourceFile(.{ .file = .{ .path = file }, .flags = options.flags });

pub fn mangleObjectNameMonstrosity(ally: std.mem.Allocator, dest_name: []const u8, path: []const u8) ![]const u8 {
    const duped = try std.fmt.allocPrint(ally, "o_{s}_{s}", .{ dest_name, path });

    for (duped) |*c| {
        c.* = switch (c.*) {
            '/', '\\' => '_',
            else => c.*,

    return duped;

That’s funny but also clever with the duped name mangler lol.