Using zig cc to compile a C library, then use it as a module

Im trying to compile a c-library using build.zig, the library is called htslib (used heavily in genomics). My goal is to have zig build the library, then be able to use the lib as a zig module, so eventually one could add htslib to a zig project using the package manager. I dont plan on adding any zig wrappers to functions, just to use them using cImport.

I managed to get a static lib built using build.zig, although building a shared library does not seem to work for some reason (I see lots of missing symbol errors during compile).
Next I created a zights.zig file which pulls in all the header files from htslib e.g. each line follows

pub const hts = @cImport({@cInclude("hts.h");});
pub const hfile = @cImport({@cInclude("hfile.h");});

Not sure if this is a good design? I was hoping the user could then access the functions using a separate program e.g.:

const h = @import("zights");

pub fn main() !void {
    const f: h.hts.htsFile = h.sam.sam_open("blah.bam", "r");
    _ = f;

Problem is the module doesn’t find the header files, and I see errors like ’ error: root struct of file ‘cimport’ has no member named …'. I can fix this by adding the full path to the zights.zig file though, indicating its probably something simple to fix.

Some questions I had:

  • How do I add the include directory for the module, it can’t see the correct location at the moment?
  • Where am I going wrong for building a shared library?
  • Do I need to do anything with the libhts.a artefact - where should this live if its part of a zig module?

My build.zig file so far is:

const std = @import("std");

pub fn generateHeaders(b: *std.Build) !void {
    var genConfigFile = try std.fs.cwd().createFile("htslib/config.h", .{ .truncate = true });
    defer genConfigFile.close();
    const writer = genConfigFile.writer();
    try writer.print(
    \\ /* Default config.h generated by build.zig */
    \\#define HAVE_LIBBZ2 1\n#define HAVE_LIBLZMA 1
    \\#ifndef __APPLE__
    \\#define HAVE_LZMA_H 1
    \\#define HAVE_DRAND48 1
    \\#define HAVE_LIBCURL 1
    \\#define HAVE_POPCNT 1
    \\#define HAVE_SSE4_1 1
    \\#define HAVE_SSSE3 1
    \\#define HAVE_AVX2 1
    \\#define HAVE_AVX512 1
    \\#define UBSAN 1
    , .{});

    var genVarsFile = try std.fs.cwd().createFile("htslib/config_vars.h", .{ .truncate = true });
    const vars_writer = genVarsFile.writer();
    const CPPFLAGS: []const u8 = b.graph.env_map.get("CPPFLAGS") orelse "";
    const CFLAGS: []const u8 = b.graph.env_map.get("CFLAGS") orelse "";
    const LDFLAGS: []const u8 = b.graph.env_map.get("LDFLAGS") orelse "";
    const LIBS: []const u8 = b.graph.env_map.get("LIBS") orelse "";
    try vars_writer.print(
    \\#define HTS_CC "zig cc"
    \\#define HTS_CPPFLAGS "{s}"
    \\#define HTS_CFLAGS "{s} -g -Wall -O2 -fvisibility=hidden -fpic"
    \\#define HTS_LDFLAGS "{s} -fvisibility=hidden "
    \\#define HTS_LIBS "{s} -lz -lm -lbz2 -llzma -lcurl -lpthread"

    // version.h
    const hts_version = "1.19.1";
    var genVerFile = try std.fs.cwd().createFile("htslib/version.h", .{ .truncate = true });
    const ver_writer = genVerFile.writer();
    try ver_writer.print(
    \\#define HTS_VERSION_TEXT "{s}"
    , .{hts_version});

pub fn build(b: *std.Build) !void {

    const LIBHTS_SOVERSION = 3;

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = std.builtin.OptimizeMode.ReleaseFast });

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    const allocator = gpa.allocator();

    defer _ = gpa.deinit();

    var cflags = std.ArrayList([]const u8).init(allocator);
    defer cflags.deinit();

    try cflags.append("-g");
    try cflags.append("-Wall");
    try cflags.append("-O2");
    try cflags.append("-fvisibility=hidden");
    try cflags.append("-fpic");
    // try cflags.append("-D_XOPEN_SOURCE=600");  // Needed? Maybe 700?

    // Platform specific build options
    const target_os = target.query.os_tag;
    try cflags.append("-dynamiclib");
    if (target_os == .macos) {
        std.debug.print("Building for macOS\n", .{});
        try cflags.append("-dynamiclib");

    } else if (target_os == .linux) {
        std.debug.print("Building for Linux\n", .{});
        try cflags.append("-shared");
        try cflags.append("-Wl");
        const soname = try std.fmt.allocPrint(allocator, "-Wl,-soname,{}", .{LIBHTS_SOVERSION});
        try cflags.append(soname);


    try generateHeaders(b);

    const htslib = b.addStaticLibrary(.{
    // const htslib = b.addSharedLibrary(.{. // <- this causes errors
        .name = "hts",
        .target = target,
        .optimize = optimize,

    //LIBHTS sources
    const src_files = &[_][]const u8{

    for (src_files) |file| {
            .file = .{.path = file},
            .flags = cflags.items,

    htslib.addIncludePath(.{ .path = "htslib" });
    htslib.addIncludePath(.{ .path = "htslib/cram" });
    htslib.addIncludePath(.{ .path = "htslib/htscodecs/htscodecs" });
    htslib.addIncludePath(.{ .path = "htslib/htslib" });
    htslib.addIncludePath(.{ .path = "htslib/m4" });
    htslib.addIncludePath(.{ .path = "htslib/os" });




    const zights = b.addModule("zights", .{ .root_source_file = .{ .path = "src/zights.zig" } });

    zights.addIncludePath(.{ .path = "htslib/htslib" });
    zights.addIncludePath(.{ .path = "htslib/cram" });
    zights.addLibraryPath(.{ .path = "zig-out/lib"});
    // zights.linkSystemLibrary("hts");  // <- is something like this needed?

    const zights_demo = b.addExecutable(.{
        .name = "zights_demo",
        .root_source_file = .{ .path = "src/main.zig" },
        .target = target,
        .optimize = optimize,

    // these dont seem to do anything:
    zights_demo.addIncludePath(.{ .path = "htslib/htslib" });
    zights_demo.addIncludePath(.{ .path = "htslib/cram" });
    zights_demo.addLibraryPath(.{ .path = "zig-out/lib"});

    zights_demo.root_module.addAnonymousImport("zights", .{
        .root_source_file = .{ .path = "src/zights.zig" },


Thanks for the help, and any feedback would be much appreciated!

@kcleal welcome to the forum.

I noticed in the function generateHeaders the following line:

    \\#define HAVE_LIBBZ2 1\n#define HAVE_LIBLZMA 1

\n does not work as line separator because multiline string literals have no escapes.

Unfortunately I have no idea what could be wrong with creating a shared library.

Thanks for spotting that :+1:
Any ideas about how to add the include path for htslib?

Its there:

zights_demo.addIncludePath(.{ .path = "htslib/htslib" });

When building, add the verbose flag to print the compile and link commands.

zig build --verbose

The printed command must have a -I with the correct include path.

The command has the correct path:

zig build-exe -ODebug -I /Users/sbi8kc2/CLionProjects/zights/htslib/htslib -I /Users/sbi8kc2/CLionProjects/zights/htslib/cram -L /Users/sbi8kc2/CLionProjects/zights/zig-out/lib --dep zights -Mroot=/Users/sbi8kc2/CLionProjects/zights/src/main.zig -Mzights=/Users/sbi8kc2/CLionProjects/zights/src/zights.zig --cache-dir /Users/sbi8kc2/CLionProjects/zights/zig-cache --global-cache-dir /Users/sbi8kc2/.cache/zig --name zights_demo --listen=- 

The -I path seems fine, the error is:

└─ install zights_demo
   └─ zig build-exe zights_demo Debug native 2 errors
src/zights.zig:3:17: error: C import failed
pub const hts = @cImport({@cInclude("hts.h");});
referenced by:
    main: src/main.zig:6:15
    callMain: /Users/sbi8kc2/.zvm/master/lib/std/start.zig:585:32
    remaining reference traces hidden; use '-freference-trace' to see all reference traces
/Users/sbi8kc2/CLionProjects/zights/zig-cache/o/182690d277b7b2fbf32993b44460cb97/cimport.h:1:10: error: 'hts.h' file not found
#include <hts.h>


try this:

zig translate-c /Users/sbi8kc2/CLionProjects/zights/htslib/htslib/hts.h -I /Users/sbi8kc2/CLionProjects/zights/htslib/htslib -I /Users/sbi8kc2/CLionProjects/zights/htslib/cram

It prints on stdout the same zig code that is generated when using @cInclude.
If the result is error: FileNotFound, it cannot find some file that is included in hts.h or in its included files.

Thanks, it didn’t produce a FileNotFound error, but I did see some other compile errors e.g.:

pub const __SWIFT_UNAVAILABLE_MSG = @compileError("unable to translate macro: undefined identifier `swift`"); // /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/Availability.h:370:13


pub const HTS_DEPRECATED = @compileError("unable to translate macro: undefined identifier `__deprecated__`"); // /Users/sbi8kc2/CLionProjects/zights/htslib/htslib/hts_defs.h:84:9

The output is very big, looks like there are quite a few of these. I guess the hts_defs.h could be rewritten without all the macros, but not sure what to do about the MacOSX ones. Do you think these compile errors all need resolving?

Thanks for the help! I managed to get it working