Zillowe FoundationZillowe Documentation

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:

  1. metadata{...}: (Required) A function call that declares all the static information about your package, like its name, version, and description.
  2. dependencies{...}: A function call that defines the package's build-time and runtime dependencies.
  3. 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.

FieldTypeDescription
namestringRequired. The name of the package.
repostringRequired. The repository tier where the package belongs (e.g. core, community).
versionstringThe package version. Can be hardcoded or determined dynamically.
versionstableA map of channels to versions (e.g. { stable = "1.2.3", beta = "1.4.0" }). Used for version resolution if version is not set.
descriptionstringA short, one-line description of the package.
websitestringThe official website of the software.
gitstringThe source code's git repository URL.
manstringA URL to the package's manual page (can be a raw markdown or text file).
maintainertableRequired. A table with name, email, and optionally website of the package maintainer.
authortableA table with name, email, and website of the original software author.
licensestringThe SPDX license identifier for the software (e.g. MIT, GPL-3.0-or-later).
binslistA list of binary names the package provides. Zoi will create symlinks for these in a directory on the user's PATH.
conflictslistA list of other package names that this package conflicts with.
provideslistA list of virtual package names this package provides. Other packages can depend on these.
replaceslistA list of packages that this package replaces. Zoi will offer to remove them upon installation.
backuplistA list of configuration files to preserve during upgrades. Zoi will create .zoinew and .zoisave files for these.
typeslistA list of supported build methods (e.g. source, pre-compiled).
package_typestringThe type of package. Can be package (default), collection, app, or extension. See Package Types.
sub_packageslistA list of sub-package names to define a split package. See Split Packages.
main_subslistFor a split package, a list of the default sub-packages to install.
updateslistA list of tables providing structured update information (e.g. for security notices).
altstringA URL or source string for an alternative package definition to use instead of this one.
tagslistA 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 with verifySignature().
  • 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 --version flag, checking for the existence of specific library files, or running a small test script.
  • Return value: Should return true if all tests pass and false otherwise. A failure will cause zoi package test to 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

VariableTypeDescription
SYSTEMtableContains system info: OS (linux, macos, windows), ARCH (amd64, arm64), DISTRO, MANAGER.
ZOItableContains Zoi-specific info: VERSION (the version being built), PATH.user, PATH.system.
PKGtableA read-only table containing all the fields you defined in your metadata{...} block.
BUILD_DIRstringThe absolute path to the temporary build directory. Use this as the destination for all downloaded and built files.
STAGING_DIRstringThe absolute path to the staging directory. Use this as the base for all zcp operations.
BUILD_TYPEstringThe build type specified by the user (e.g. "source", "pre-compiled").
SUBPKGstringFor split packages, the name of the sub-package currently being processed. nil for non-split packages.

Helper Functions

FunctionDescription
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:

  1. A path relative to the BUILD_DIR (e.g. "source/my-binary").
  2. An absolute path on the build machine.
  3. A path relative to your .pkg.lua file 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")

Destination Path

The destination argument uses special variables to place files in the correct location within the final package archive.

VariableDescription
${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")
end

extension

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 linux will install linux:kernel and linux:headers.
  • zoi install linux:docs will 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
end

Staging 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.lua

Step 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.lua

This 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.lua

You 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.lua

zoi 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 the test() 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.zst

zoi 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() end

Next 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