How do I statically/dynamically link to a C exe using build.zig?

Here I am conditionally building either a static or a dynamic library (haven’t tested out dynamic linking yet) but with static linking I am having problems:

zb -Dcapi -Ddemo
install
└─ install demo-c
   └─ compile exe demo-c Debug native 1 errors
error: ld.lld: .zig-cache/o/bef9b298ef8e2c610d51e05466e699fe/libreplace-exe.a: Archive::children failed: truncated or malformed archive (offset to next archive member past the end of the archive after member c_api.o)
error: the following command failed with 1 compilation errors:
/home/rolex/.zv/versions/0.15.2/zig build-exe /home/rolex/zig-stuff/replace-exe/demo/demo.c .zig-cache/o/bef9b298ef8e2c610d51e05466e699fe/libreplace-exe.a -ODebug -I .zig-cache/o/a6fb089e932f705ef1065695b36c6e90 -I /home/rolex/zig-stuff/replace-exe/include -Mroot -lc --cache-dir .zig-cache --global-cache-dir /home/rolex/.cache/zig --name demo-c --zig-lib-dir /home/rolex/.zv/versions/0.15.2/lib/ --listen=-

Build Summary: 8/11 steps succeeded; 1 failed
install transitive failure
└─ install demo-c transitive failure
   └─ compile exe demo-c Debug native 1 errors

As far as I can interpret, this seems to be that the lib is not built before the linking step is initiated for demo_c causing it to be a malformed archive.

I thought demo_c.step.dependOn(&lib.step); would fix it but it didn’t

Can someone point out what’s wrong with this build script?

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});
    const capi = b.option(bool, "capi", "Build libreplace-exe for use with C/C++") orelse false;
    const so = b.option(bool, "shared", "Build shared library: libreplace-exe.so instead of default static lib") orelse false;
    const demo = b.option(bool, "demo", "Build & Install demo executables") orelse true;
    switch (target.result.os.tag) {
        .windows, .linux, .macos, .freebsd, .netbsd, .dragonfly, .openbsd => {},
        else => {
            std.log.err("Unsupported Target OS: {s}", .{@tagName(target.result.os.tag)});
            std.log.err("Supported: Windows, Linux, macOS, FreeBSD, NetBSD, DragonFly, OpenBSD", .{});
            std.process.exit(1);
        },
    }
    // Create module for the library
    const lib_mod = b.addModule("replace_exe", .{
        .root_source_file = b.path("root.zig"),
        .target = target,
        .optimize = optimize,
    });
    // C API needs to import the core module and link libc
    var c_lib: ?*std.Build.Step.Compile = null;
    if (capi) {
        const lib = b.addLibrary(.{
            .name = "replace-exe",
            .linkage = if (so) .dynamic else .static,
            .root_module = b.createModule(.{
                .root_source_file = b.path("c_api.zig"),
                .target = target,
                .optimize = optimize,
            }),
        });
        lib.root_module.addImport("replace_exe", lib_mod);
        lib.linkLibC();
        c_lib = lib;
        // Install header file for C/C++ users
        const header = b.addInstallFile(b.path("include/replace_exe.h"), "include/replace_exe.h");
        b.getInstallStep().dependOn(&header.step);
        b.installArtifact(lib);
    }

    // Tests
    const lib_tests = b.addTest(.{
        .root_module = lib_mod,
    });
    const run_lib_tests = b.addRunArtifact(lib_tests);

    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&run_lib_tests.step);

    const demo_exe = b.addExecutable(.{
        .name = "demo",
        .root_module = b.createModule(.{
            .root_source_file = b.path("demo/demo.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });
    demo_exe.root_module.addImport("replace-exe", lib_mod);

    const demo_exe2 = b.addExecutable(.{
        .name = "demo2",
        .root_module = b.createModule(.{
            .root_source_file = b.path("demo/demo2.zig"),
            .target = target,
            .optimize = optimize,
        }),
    });
    demo_exe2.root_module.addImport("replace-exe", lib_mod);

    if (c_lib) |lib| {
        const demo_c = b.addExecutable(.{ .name = "demo-c", .root_module = b.createModule(.{
            .target = target,
            .optimize = optimize,
        }) });
        demo_c.root_module.addCSourceFile(.{ .file = b.path("demo/demo.c") });
        demo_c.root_module.link_libc = true;
        demo_c.root_module.linkLibrary(lib);
        demo_c.addIncludePath(b.path("include"));
        demo_c.step.dependOn(&lib.step);

        if (demo) {
            b.installArtifact(demo_c);
        }
    }

    if (demo) {
        b.installArtifact(demo_exe);
        b.installArtifact(demo_exe2);
    }

    const run_demo = b.addRunArtifact(demo_exe);
    run_demo.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_demo.addArgs(args);
    }

    const run_step = b.step("run", "Run the demo executable");
    run_step.dependOn(&run_demo.step);
}

btw I tried addInstallArtifact hoping that it would give me a step to dependOn but that didn’t work out either:

    var c_lib: ?*std.Build.Step.Compile = null;
    var lib_install_step: ?*std.Build.Step.InstallArtifact = null;
    if (capi) {
        const lib = b.addLibrary(.{
            .name = "replace-exe",
            .linkage = if (so) .dynamic else .static,
            .root_module = b.createModule(.{
                .root_source_file = b.path("c_api.zig"),
                .target = target,
                .optimize = optimize,
            }),
        });
        lib.root_module.addImport("replace_exe", lib_mod);
        lib.linkLibC();
        c_lib = lib;
        // Install header file for C/C++ users
        const header = b.addInstallFile(b.path("include/replace_exe.h"), "include/replace_exe.h");
        b.getInstallStep().dependOn(&header.step);
        lib_install_step = b.addInstallArtifact(lib, .{});
    }

and then

 if (c_lib) |lib| {
        if (lib_install_step) |step| {
            const demo_c = b.addExecutable(.{ .name = "demo-c", .root_module = b.createModule(.{
                .target = target,
                .optimize = optimize,
            }) });
            demo_c.root_module.addCSourceFile(.{ .file = b.path("demo/demo.c") });
            demo_c.root_module.link_libc = true;
            demo_c.step.dependOn(&step.step);
            demo_c.root_module.linkLibrary(lib);
            demo_c.addIncludePath(b.path("include"));
            if (demo) {
                b.installArtifact(demo_c);
            }
        }
    }

Results in the same error: ld.lld: .zig-cache/o/bef9b298ef8e2c610d51e05466e699fe/libreplace-exe.a: Archive::children failed: truncated or malformed archive (offset to next archive member past the end of the archive after member c_api.o)

I don’t think there’s anything wrong with your build script, rather it’s the self-hosted ELF linker (default for debug builds) which is broken, it’s not producing correctly formed static libraries:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f

00000000  21 3c 61 72 63 68 3e 0a 2f 53 59 4d 36 34 2f 20  !<arch>./SYM64/ 
00000010  20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20                  
00000020  20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20                  
00000030  20 20 20 20 20 20 20 20 32 34 20 20 20 20 20 20          24      
00000040  20 20 60 0a                                        `.

It’s supposed to look something like this, with each header field containing a number instead of being all whitespace:

Offset(h) 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f

00000000  21 3c 61 72 63 68 3e 0a 2f 53 59 4d 36 34 2f 20  !<arch>./SYM64/ 
00000010  20 20 20 20 20 20 20 20 30 20 20 20 20 20 20 20          0       
00000020  20 20 20 20 30 20 20 20 20 20 30 20 20 20 20 20      0     0     
00000030  30 20 20 20 20 20 20 20 32 34 20 20 20 20 20 20  0       24      
00000040  20 20 60 0a                                        `.

Similar issue reported here: `zig build-lib` sometimes creates truncated or malformed archives · Issue #25129 · ziglang/zig · GitHub

It looks like an easy fix so I’ll see if I can open a PR.

As a workaround you can set c_lib.use_llvm = true in you build script to force Zig to use ld.lld regardless of optimization mode.


Side note: demo_c.step.dependOn(&lib.step) is redundant, demo_c.root_module.linkLibrary(lib) already registers a dependency on lib.step (the same is true for most other build system APIs that take artifacts, modules or lazy paths).

2 Likes

Thanks for the response. I don’t pretend to understand what exactly is wrong with the generated archive but I will try the llvm solution you proposed in the meantime and report back if it works out as an edit here.

Edit:
lib.use_llvm = true; was able to solve this. Thanks!

1 Like