Creating Packages
A complete guide on how to create and build Zoi packages.
This guide provides a comprehensive overview of how to create packages for Zoi. The core of Zoi packaging is the .pkg.lua file, a Lua script that gives you immense power and flexibility to define how your software is built, packaged, and installed.
Anatomy of a .pkg.lua File
A Zoi package is defined by a single .pkg.lua file. This file is a Lua script that Zoi executes to get information about your package and to perform the build. It's not a simple configuration file; it's a powerful script that can contain logic, functions, and variables.
A typical .pkg.lua file is structured around a few key global functions:
metadata{...}: (Required) A function call that declares all the static information about your package, like its name, version, and description.dependencies{...}: A function call that defines the package's build-time and runtime dependencies.- Lifecycle Functions: A set of functions (
prepare,package,verify,uninstall) that contain the logic for fetching, building, and installing the software.
Let's look at the core components in detail.
1. Metadata
The metadata{...} function is the only required part of a .pkg.lua file. It takes a single Lua table as an argument, where you define all the essential properties of your package.
Example
metadata({
name = "my-cli",
repo = "community",
version = "1.2.3",
description = "A cool command-line tool.",
license = "MIT",
bins = { "my-cli" },
types = { "source", "pre-compiled" }
})All Metadata Fields
This table lists all the available fields you can use inside the metadata block.
| Field | Type | Description |
|---|---|---|
name | string | Required. The name of the package. |
repo | string | Required. The repository tier where the package belongs (e.g. core, community). |
version | string | The package version. Can be hardcoded or determined dynamically. |
versions | table | A map of channels to versions (e.g. { stable = "1.2.3", beta = "1.4.0" }). Used for version resolution if version is not set. |
description | string | A short, one-line description of the package. |
website | string | The official website of the software. |
git | string | The source code's git repository URL. |
man | string | A URL to the package's manual page (can be a raw markdown or text file). |
maintainer | table | Required. A table with name, email, and optionally website of the package maintainer. |
author | table | A table with name, email, and website of the original software author. |
license | string | The SPDX license identifier for the software (e.g. MIT, GPL-3.0-or-later). |
bins | list | A list of binary names the package provides. Zoi will create symlinks for these in a directory on the user's PATH. |
conflicts | list | A list of other package names that this package conflicts with. |
provides | list | A list of virtual package names this package provides. Other packages can depend on these. |
replaces | list | A list of packages that this package replaces. Zoi will offer to remove them upon installation. |
backup | list | A list of configuration files to preserve during upgrades. Zoi will create .zoinew and .zoisave files for these. |
types | list | A list of supported build methods (e.g. source, pre-compiled). |
package_type | string | The type of package. Can be package (default), collection, app, or extension. See Package Types. |
sub_packages | list | A list of sub-package names to define a split package. See Split Packages. |
main_subs | list | For a split package, a list of the default sub-packages to install. |
updates | list | A list of tables providing structured update information (e.g. for security notices). |
alt | string | A URL or source string for an alternative package definition to use instead of this one. |
tags | list | A list of keywords to help users find the package via zoi search. |
2. Dependencies
The dependencies{...} block declares build-time and runtime dependencies. Zoi can pull these from over 40 different package managers.
Learn More
For a complete guide on the dependency format, version pinning, and all supported managers, see the Dependencies & Supported Package Managers page.
Example
dependencies({
-- Dependencies needed to build the package
build = {
-- Dependencies for building from source, organized by type
types = {
source = {
required = { "native:make", "native:gcc" }
}
}
},
-- Dependencies needed to run the package
runtime = {
required = { "native:openssl" },
optional = { "zoi:my-plugin:Provides extra features" }
}
})3. Lifecycle Functions
These functions define the imperative steps for building your package. Zoi calls them in a specific order: prepare -> package -> verify.
prepare()
This function runs first. Its main job is to fetch all necessary source code, binaries, or other assets and place them inside the temporary BUILD_DIR.
- Common tasks: Cloning git repos, downloading release archives, extracting files.
package()
This function runs after prepare(). It takes the prepared files from the BUILD_DIR and copies only the final, distributable files into the STAGING_DIR. The contents of the staging directory are what will be included in the final .pkg.tar.zst archive.
- Common tasks: Running
make install, copying binaries, libraries, and documentation.
verify()
This function runs after package(). It's a security step where you can verify the integrity and authenticity of the files you downloaded in the prepare step.
- Common tasks: Checking checksums with
verifyHash(), verifying PGP signatures withverifySignature(). - Return value: If this function returns
false, the build process is aborted.
test()
This optional function runs after a successful package() step and is executed by the zoi package test command. It allows you to run validation and integration tests against your staged package files.
- Common tasks: Running the packaged binary with a
--versionflag, checking for the existence of specific library files, or running a small test script. - Return value: Should return
trueif all tests pass andfalseotherwise. A failure will causezoi package testto exit with an error.
uninstall()
This function is not part of the build process. Its contents are executed when a user runs zoi uninstall on the package. It's used for cleanup tasks outside of the main package directory.
- Common tasks: Removing user-specific configuration files or system-wide service files.
4. The Lua Environment
When Zoi executes your script, it provides a rich environment of global variables and helper functions to make packaging easier.
Global Variables
| Variable | Type | Description |
|---|---|---|
SYSTEM | table | Contains system info: OS (linux, macos, windows), ARCH (amd64, arm64), DISTRO, MANAGER. |
ZOI | table | Contains Zoi-specific info: VERSION (the version being built), PATH.user, PATH.system. |
PKG | table | A read-only table containing all the fields you defined in your metadata{...} block. |
BUILD_DIR | string | The absolute path to the temporary build directory. Use this as the destination for all downloaded and built files. |
STAGING_DIR | string | The absolute path to the staging directory. Use this as the base for all zcp operations. |
BUILD_TYPE | string | The build type specified by the user (e.g. "source", "pre-compiled"). |
SUBPKG | string | For split packages, the name of the sub-package currently being processed. nil for non-split packages. |
Helper Functions
| Function | Description |
|---|---|
cmd(command) | Executes a shell command within the BUILD_DIR. |
zcp(source, dest) | Copies files/directories to the STAGING_DIR. The source can be a path inside BUILD_DIR, an absolute path, or a path relative to the .pkg.lua file using ${pkgluadir}. See Staging Files with zcp. |
zrm(path) | Used in uninstall() to remove files/directories. See Target Paths for zrm. |
verifyHash(file, "algo-hash") | Verifies a file's checksum (e.g. "sha512-..."). |
verifySignature(file, sig, key) | Verifies a PGP signature. |
addPgpKey(url_or_path, name) | Adds a PGP key to Zoi's keyring for verification. |
UTILS.EXTRACT(url_or_path, out_dir) | Downloads and extracts a .zip or .tar.* archive. |
UTILS.FETCH.url(url) | Fetches a URL's content as a string. |
UTILS.FETCH.GITHUB.LATEST.release{...} | Fetches the latest release tag from a GitHub repository. |
UTILS.FIND.file(dir, name) | Finds a file within the BUILD_DIR. |
UTILS.FS.exists(path) | Returns true if a file or directory exists at the given path. |
UTILS.PARSE.checksumFile(content, file) | Parses a checksum from a string (e.g. the content of a checksums.txt file). |
Staging Files with zcp
The zcp(source, destination) function is used to copy files from your build environment into the final package archive.
Source Path
The source argument can be:
- A path relative to the
BUILD_DIR(e.g."source/my-binary"). - An absolute path on the build machine.
- A path relative to your
.pkg.luafile using the${pkgluadir}variable. This is useful for bundling assets that are stored alongside your package definition.- Example:
zcp("${pkgluadir}/my-asset.png", "${pkgstore}/share/my-asset.png")
- Example:
Destination Path
The destination argument uses special variables to place files in the correct location within the final package archive.
| Variable | Description |
|---|---|
${pkgstore} | The main directory for the package's files in the Zoi store. This is where most files should go (e.g. ${pkgstore}/bin/my-cli). |
${createpkgdir} | For app type packages, this is the root of the project template. |
${usrroot} | Copies files to the system's root (/). Use with extreme caution. This requires administrator privileges to install. |
${usrhome} | Copies files to the user's home directory (~). |
5. Package Types
Zoi supports different types of packages, specified via the package_type field in your metadata.
package (Default)
This is the standard type for distributing software, tools, libraries, or any other set of files. If you don't specify a package_type, it defaults to this.
collection
A collection is a meta-package that groups other packages. Its main purpose is to list other packages in its dependencies block. Installing a collection simply installs all of its dependencies.
Example:
metadata({
name = "rust-dev-tools",
package_type = "collection",
-- ...
})
dependencies({
runtime = {
"zoi:rust",
"cargo:cargo-watch",
"cargo:cargo-edit"
}
})app
An app package is a template or boilerplate used to bootstrap new projects with the zoi create <app-name> command. Files staged with zcp to the ${createpkgdir} destination will be copied into the user's current directory when they run zoi create.
Example:
metadata({
name = "rust-starter",
package_type = "app",
-- ...
})
function package()
-- These files will be copied when a user runs `zoi create rust-starter`
zcp("template/Cargo.toml", "${createpkgdir}/Cargo.toml")
zcp("template/src/main.rs", "${createpkgdir}/src/main.rs")
endextension
An extension is a special package that modifies the user's Zoi configuration. It's used for distributing repository lists, PGP keys, or project starters.
Learn More
For a complete guide on creating extensions, see the Extensions page.
6. Split Packages
Zoi supports "split packages," allowing a single .pkg.lua to produce multiple, smaller packages. This is ideal for large projects where you want to separate components, like a kernel, its headers, and its documentation.
Defining a Split Package
You use the sub_packages and main_subs fields in your metadata:
sub_packages: A list of all sub-packages that can be built.main_subs: A list of the default sub-packages to install when a user installs the base package name.
Example:
metadata({
name = "linux",
sub_packages = { "kernel", "headers", "docs" },
main_subs = { "kernel", "headers" },
-- ...
})zoi install linuxwill installlinux:kernelandlinux:headers.zoi install linux:docswill install only the documentation.
Lifecycle Functions in Split Packages
The prepare, package, verify, and uninstall functions receive a table containing the name of the sub-package being processed.
function package(args)
if args.sub == "kernel" then
-- Packaging logic for the kernel
zcp("src/vmlinuz", "${pkgstore}/boot/vmlinuz-linux")
elseif args.sub == "headers" then
-- Packaging logic for the headers
zcp("src/include", "${pkgstore}/usr/include")
end
endStaging Files
When building a split package, zcp automatically stages files into a sub-package-specific directory (e.g. data/kernel/..., data/headers/...) within the final archive. This ensures that installing a sub-package only extracts the correct files.
7. Hooks
Zoi allows you to run scripts at specific points in a package's lifecycle using the hooks{...} block. This is useful for tasks like starting/stopping system services, creating users, or other setup/cleanup actions that fall outside of simple file installation.
The hooks block is a top-level function in your .pkg.lua, similar to dependencies.
Available Hooks
The following hooks are available, executed at different stages:
pre_install: Before the package's files are installed.post_install: After the package's files are installed and binaries are linked.pre_upgrade: Before an existing version of the package is upgraded.post_upgrade: After the package has been successfully upgraded to a new version.pre_remove: Before the package's files are removed.post_remove: After the package's files have been removed.
Hook Definition
Each hook can be defined as a simple list of command strings or as a platform-specific map.
hooks({
-- A simple list of commands to run after installation
post_install = {
"echo 'My package was installed!'",
"echo 'Run my-cli --help to get started.'"
},
-- Platform-specific commands to run before removal
pre_remove = {
linux = { "systemctl stop my-service" },
windows = { "sc.exe stop my-service" },
default = { "echo 'Stopping my-service...'" }
},
-- Commands to run after an upgrade
post_upgrade = {
"echo 'Package upgraded. Restarting service...'",
"systemctl restart my-service"
}
})8. The Packaging Workflow
Follow these steps to develop, build, and test your package.
Step 1: Develop and Iterate
The fastest way to iterate on your .pkg.lua file is to use zoi install with the direct path to your script. This runs the full dependency resolution and build logic without creating an intermediate archive.
zoi install /path/to/your-package.pkg.luaStep 2: Test Your Package
Once your package seems to be working, run your tests using the zoi package test command.
zoi package test --type <build-type> /path/to/your-package.pkg.luaThis command will execute the prepare(), package(), and test() functions. If test() returns false or errors, the command will fail.
Step 3: Build the Distributable Archive
Once your tests are passing, build the final .pkg.tar.zst archive.
zoi package build --type <build-type> /path/to/your-package.pkg.luaYou can also combine testing and building in one step with the --test flag. The build will only proceed if the tests pass.
zoi package build --type <build-type> --test /path/to/your-package.pkg.luazoi package build Flags:
--type <TYPE>: (Required) The build type to use (e.g.source).--platform <PLATFORM>: Build for a specific platform (e.g.linux-arm64). Defaults to your current platform.--sub <SUB_PACKAGE...>: For a split package, build only the specified sub-packages.--sign <KEY>: Sign the resulting archive with a PGP key from your Zoi keyring.--test: Run thetest()function before building the archive.
Step 4: Verify the Final Archive
Install the built archive to ensure it's correct and installs properly for end-users.
zoi package install /path/to/your-package-1.0.0-linux-amd64.pkg.tar.zstzoi package install Flags:
--scope <user|system>: Override the installation scope.--sub <SUB_PACKAGE...>: For a split package, install only the specified sub-packages from the archive.
9. Full Walkthrough: 'hello' Package
This example demonstrates many of the concepts above by packaging a simple "Hello, World!" program written in Zig.
For the full source code of this example, see the hello package in the Zoidberg repository.
hello.pkg.lua
-- 1. Helper variables
local repo_owner = "Zillowe"
local repo_name = "Hello"
local version = ZOI.VERSION or "3.0.0"
local git_url = "https://github.com/" .. repo_owner .. "/" .. repo_name .. ".git"
local release_base_url = "https://github.com/" .. repo_owner .. "/" .. repo_name .. "/releases/download/v" .. version
-- 2. Metadata
metadata({
name = "hello",
repo = "zillowe",
version = version,
description = "Hello World",
website = "https://github.com/Zillowe/Hello",
git = git_url,
man = "https://raw.githubusercontent.com/Zillowe/Hello/refs/heads/main/app/man.md",
maintainer = { name = "Zillowe", email = "[email protected]" },
license = "Apache-2.0",
bins = { "hello" },
types = { "source", "pre-compiled" },
})
-- 3. Dependencies
dependencies({
build = {
types = {
source = { required = { "native:zig" } }
}
},
})
-- 4. Lifecycle Functions
function prepare()
if BUILD_TYPE == "pre-compiled" then
local ext = (SYSTEM.OS == "windows") and "zip" or "tar.xz"
local file_name = "hello-" .. SYSTEM.OS .. "-" .. SYSTEM.ARCH .. "." .. ext
local url = release_base_url .. "/" .. file_name
UTILS.EXTRACT(url, "precompiled")
elseif BUILD_TYPE == "source" then
cmd("git clone " .. PKG.git .. " source")
cmd("cd source && zig build-exe main.zig -O ReleaseSmall --name hello")
end
end
function package()
local bin_name = (SYSTEM.OS == "windows") and "hello.exe" or "hello"
local source_path = (BUILD_TYPE == "source") and "source/" or "precompiled/"
zcp(source_path .. bin_name, "${pkgstore}/bin/" .. bin_name)
end
function verify()
if BUILD_TYPE == "pre-compiled" then
-- (Checksum and signature verification logic would go here)
end
return true
end
function uninstall() endNext Steps
Once you have created and tested your package, the next step is to make it available to others. See the Publishing Packages guide for details.
Last updated on
