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-package | Contents |
|---|---|
mylib:main | Main Tool: Any accompanying CLI tools or utilities. |
mylib:lib | Runtime: Shared objects (.so, .dylib, .dll) and versioned runtime files. |
mylib:dev | Development: Headers (.h), pkg-config files, static libraries, and the unversioned .so symlink. |
mylib:docs | Documentation: 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
endKey Points
- Sub-package naming: Use
:libfor runtime,:devfor development, and:docsfor extra documentation. main_subs = { "lib" }:zoi install libmylibinstalls only the runtime library. Users add:devexplicitly when they need to compile against it.zlicense: Every sub-package MUST include its own copy of the license.UTILS.FS.existsguards: Thepackage()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")
endThe 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:
| Variable | Value |
|---|---|
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")
endA consumer that wants static linking adds the dependency to their build configuration:
dependencies({
build = {
required = { "zoi:libmylib:dev" }
}
})Summary
| Concern | Approach |
|---|---|
| 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 discovery | CFLAGS, LDFLAGS, PKG_CONFIG_PATH set automatically |
2026 © All Rights Reserved.
- All the content is available under CC BY-SA 4.0, expect where otherwise stated.
- Source code is available on GitLab, licensed under Apache 2.0.
Last updated on
