Zillowe FoundationZillowe Documentation

Packaging Libraries

How to package shared and static libraries with Zoi, and how to consume them from other packages.

This guide covers the lifecycle of a library in Zoi: packaging it for distribution and consuming it from another package. Libraries follow a specific sub-package convention to separate runtime binaries from headers and development artefacts.

Package Shape

A typical library package uses three sub-packages to minimize the installation footprint for users who only need the runtime:

Sub-packageContents
mylib:mainMain Tool: Any accompanying CLI tools or utilities.
mylib:libRuntime: Shared objects (.so, .dylib, .dll) and versioned runtime files.
mylib:devDevelopment: Headers (.h), pkg-config files, static libraries, and the unversioned .so symlink.
mylib:docsDocumentation: API manuals, examples, and HTML documentation.

Installing mylib (the base name) should install lib by default. The dev sub-package is pulled in only when another package needs to compile against the library.

1. Writing the Library Package

Below is an authoritative example for a C library, libmylib, that uses CMake. The same pattern applies to any build system.

libmylib.pkg.lua

local version = "2.1.0"
local archive = "v" .. version .. ".tar.gz"
local url = "https://github.com/example/libmylib/archive/refs/tags/" .. archive

metadata({
  name = "libmylib",
  repo = "community",
  version = version,
  description = "An example C library for string processing",
  website = "https://github.com/example/libmylib",
  license = "MIT", -- MUST use a valid SPDX identifier
  types = { "source" },
  platforms = { "linux", "macos" },
  sub_packages = { "lib", "dev", "docs" },
  main_subs = { "lib" },
  tags = { "library", "c", "string-processing" },
})

dependencies({
  build = {
    types = {
      source = {
        required = { "native:cmake", "native:gcc", "native:ninja" }
      }
    }
  },
  runtime = {
    sub_packages = {
      dev = {
        required = { "native:cmake" }
      }
    }
  }
})

function prepare()
  -- Mandatory integrity verification
  local file = UTILS.DOWNLOAD(url, archive, "sha512-abc123...")
  UTILS.EXTRACT(file, "src")

  cmd("cmake -S src/libmylib-" .. version
    .. " -B build -G Ninja"
    .. " -DCMAKE_BUILD_TYPE=Release"
    .. " -DCMAKE_INSTALL_PREFIX=/usr"
    .. " -DBUILD_SHARED_LIBS=ON")

  cmd("cmake --build build")
  cmd("DESTDIR=" .. BUILD_DIR .. "/install cmake --install build")
end

function verify()
  return true
end

function package(args)
  local sub = args.sub or "lib"
  local root = "install/usr"

  if sub == "lib" then
    -- Runtime library only, versioned .so files
    cmd("rm -rf lib-runtime && mkdir -p lib-runtime")
    cmd("cp -a " .. root .. "/lib/. lib-runtime/ 2>/dev/null; true")
    cmd("rm -rf lib-runtime/pkgconfig 2>/dev/null; true")
    cmd("rm -f lib-runtime/libmylib.a 2>/dev/null; true")
    cmd("rm -f lib-runtime/libmylib.so 2>/dev/null; true")
    zcp("lib-runtime", "${pkgstore}/lib")
  elseif sub == "dev" then
    if UTILS.FS.exists(root .. "/include") then
      zcp(root .. "/include", "${pkgstore}/include")
    end
    if UTILS.FS.exists(root .. "/lib/pkgconfig") then
      zcp(root .. "/lib/pkgconfig", "${pkgstore}/lib/pkgconfig")
    end
    -- Static library (if any)
    if UTILS.FS.exists(root .. "/lib/libmylib.a") then
      zcp(root .. "/lib/libmylib.a", "${pkgstore}/lib/libmylib.a")
    end
    -- Unversioned .so symlink for -lmylib linking
    if UTILS.FS.exists(root .. "/lib/libmylib.so") then
      cmd("mkdir -p dev-lib")
      cmd("cp -a " .. root .. "/lib/libmylib.so dev-lib/")
      zcp("dev-lib", "${pkgstore}/lib")
    end
  elseif sub == "docs" then
    if UTILS.FS.exists(root .. "/share/doc/libmylib") then
      zcp(root .. "/share/doc/libmylib", "${pkgstore}/share/doc/libmylib")
    end
  end

  -- Stage license (Mandatory for all sub-packages)
  zlicense("src/libmylib-" .. version .. "/LICENSE")
end

function test(args)
  local sub = args.sub or "lib"
  if sub == "lib" then
    -- Verify the shared library was staged correctly
    local lib_path = STAGING_DIR .. "/data/lib/pkgstore/lib/libmylib.so"
    if UTILS.FS.exists(lib_path) then
      return true
    end
    -- Try versioned variant
    local files = UTILS.FIND.file(STAGING_DIR .. "/data/lib/pkgstore/lib", "libmylib.so*")
    return files ~= nil and #files > 0
  end
  return true
end

Key Points

  • Sub-package naming: Use :lib for runtime, :dev for development, and :docs for extra documentation.
  • main_subs = { "lib" }: zoi install libmylib installs only the runtime library. Users add :dev explicitly when they need to compile against it.
  • zlicense: Every sub-package MUST include its own copy of the license.
  • UTILS.FS.exists guards: The package() function checks for files before copying, ensuring the build remains robust even if certain artefacts are missing.

2. Consuming the Library from Another Package

Once libmylib is packaged and published, other packages declare it as a dependency.

Depending on the Runtime Library

A tool that dynamically links against libmylib only needs the runtime library:

-- mytool.pkg.lua
metadata({
  name = "mytool",
  repo = "community",
  version = "1.0.0",
  description = "A tool that uses libmylib at runtime",
  bins = { "mytool" },
  types = { "pre-compiled" }
})

dependencies({
  runtime = {
    required = { "zoi:libmylib" }
  }
})

Zoi resolves "zoi:libmylib" to the latest version and installs the default sub-packages (lib, as defined by main_subs).

Depending on the Development Headers

A package that compiles against libmylib needs the dev sub-package at build time:

-- myapp.pkg.lua
metadata({
  name = "myapp",
  repo = "community",
  version = "1.0.0",
  description = "An app that compiles against libmylib",
  bins = { "myapp" },
  types = { "source" }
})

dependencies({
  build = {
    types = {
      source = {
        required = {
          "zoi:libmylib:dev",
          "native:gcc", "native:make"
        }
      }
    }
  },
  runtime = {
    required = { "zoi:libmylib" }
  }
})

function prepare()
  cmd("git clone https://github.com/example/myapp.git source")
end

function package()
  -- The compiler finds libmylib headers and .so via Zoi's store paths,
  -- which are automatically added to CFLAGS and LDFLAGS
  cmd("cd source && make")
  zcp("source/myapp", "${pkgstore}/bin/myapp")
end

The key line is "zoi:libmylib:dev" in build.types.source.required. This tells Zoi to install the dev sub-package of libmylib (headers, .pc file, static lib) during the build phase.

How Zoi Resolves Library Paths

When Zoi installs a package, it sets environment variables so compilers and linkers can find the library:

VariableValue
CFLAGS-I${ZOI_PKG_STORE}/libmylib/include
LDFLAGS-L${ZOI_PKG_STORE}/libmylib/lib
PKG_CONFIG_PATH${ZOI_PKG_STORE}/libmylib/lib/pkgconfig

If the dev sub-package provides a .pc file, pkg-config --cflags --libs libmylib works automatically. Build systems like CMake or Meson that use pkg-config will discover the library without additional configuration.

3. Version Pinning

To depend on a specific version of a library, use the @ syntax:

dependencies({
  build = {
    types = {
      source = {
        required = { "zoi:libmylib:dev@^2.0" }
      }
    }
  },
  runtime = {
    required = { "zoi:libmylib@>=2.0.0" }
  }
})

Supported version operators: ^, ~, >=, <=, >, <, =, or bare version strings. Ranges work too: ">=1.0 <3.0".

4. Static Libraries

For packages that provide static libraries (.a files), the static archive belongs in the dev sub-package, not lib. The lib sub-package should contain only the shared library (.so.* / .dylib) needed at runtime.

elseif sub == "dev" then
  -- Headers
  if UTILS.FS.exists(root .. "/include") then
    zcp(root .. "/include", "${pkgstore}/include")
  end
  -- Static library
  if UTILS.FS.exists(root .. "/lib/libmylib.a") then
    zcp(root .. "/lib/libmylib.a", "${pkgstore}/lib/libmylib.a")
  end
  -- pkg-config
  if UTILS.FS.exists(root .. "/lib/pkgconfig") then
    zcp(root .. "/lib/pkgconfig", "${pkgstore}/lib/pkgconfig")
  end

A consumer that wants static linking adds the dependency to their build configuration:

dependencies({
  build = {
    required = { "zoi:libmylib:dev" }
  }
})

Summary

ConcernApproach
Runtime library:lib sub-package, main_subs = { "lib" }
Headers and link-time files:dev sub-package, consumed via "zoi:pkg:dev"
Documentation:docs sub-package (optional)
Static libraries.a in :dev sub-package
Cross-package dependency"zoi:pkg" in dependencies{}
Version pinning"zoi:pkg@^1.0" syntax
Library discoveryCFLAGS, LDFLAGS, PKG_CONFIG_PATH set automatically

A software organization

2026 © All Rights Reserved.

  • All the content is available under CC BY-SA 4.0, expect where otherwise stated.

Last updated on