Getting Started¶
This introductory guide will help you run your first build with zb and explain the basic concepts you will need to use zb. zb is a build system that manages your dependencies so that you can be confident that a build that works on one machine works on any machine with the same operating system and CPU architecture.
Prerequisites¶
This guide assumes:
Familiarity with the command-line interface for your operating system. (For example, Terminal.app on macOS, Command Prompt or PowerShell on Windows, etc.)
Knowledge of at least one programming language. We will be building a C program in this tutorial, but you do not need to know C or have any specific version of developer tools installed. Installing tools automatically is a key feature of zb!
zb uses the Lua programming language to configure builds. Learning Lua is helpful for using zb. zb uses the Lua 5.4 language, with some standard libraries omitted to limit the complexity of builds. As such, any learning resources for Lua will be applicable to zb.
The standard library currently supports:
x86_64-unknown-linux
aarch64-apple-macos
Installation¶
If you haven’t already, follow the installation instructions.
First Steps¶
Let’s start with a C “Hello, World” program.
Open your editor of choice and enter the following into a new file hello.c
:
/* hello.c */
#include <stdio.h>
int main() {
printf("Hello, World!\n");
return 0;
}
Now let’s learn how to build hello.c
into an executable with zb.
We will write a small Lua script that describes the build, and then use the zb command-line interface (CLI) to run the script.
Out of the box, zb only knows how to run programs and download source.
However, zb has a standard library that can be fetched
to provide tools for some common programming languages.
In your editor, enter the following into a new file zb.lua
in the same directory as hello.c
(we’ll walk through this code in a moment):
-- zb.lua
-- Download the standard library.
local zb <const> = fetchArchive {
url = "https://github.com/256lights/zb-stdlib/releases/download/v0.1.1/zb-stdlib-v0.1.1.tar.gz";
hash = "sha256:ee4c78f4b1915c7dafb0d55e8cd6f20fe82396a21a6ab2add9bb879fb9301bc2";
}
-- Import modules from the standard library.
local stdenv = import(zb.."/stdenv/stdenv.lua")
-- Copy the source to the store.
local src = path {
path = ".";
name = "hello-source";
filter = function(name)
return name == "hello.c"
end;
}
-- Create our build target.
-- Replace with your system, if necessary.
-- One of:
-- x86_64-unknown-linux
-- aarch64-apple-macos
local buildSystem <const> = "x86_64-unknown-linux"
hello = stdenv.makeDerivation {
pname = "hello";
src = src;
buildSystem = buildSystem;
buildPhase = "gcc -o hello hello.c";
installPhase = '\z
mkdir -p "$out/bin"\n\z
mv hello "$out/bin/hello"\n';
};
Now we can build the program with zb build
.
Note that the first time you run zb build
,
will take a while,
since it is building the standard library tools from source.
(Because zb build artifacts can be safely shared among machines,
there are plans to speed this up.
#43 tracks this work.)
zb build 'zb.lua#hello'
zb build
takes in a URL of Lua file to run.
The fragment (i.e. everything after the #
)
names a variable to build.
In this case, we’re building hello
.
zb build
will automatically look for a global called hello
defined inside zb.lua
.
At the end, zb build
will print the path to the directory it created,
something like /opt/zb/store/2lvf1cavwkainjz32xzja04hfl5cimx6-hello
.
As you might expect from the installPhase
we used above,
it will be inside the bin
directory we created inside the output directory.
% /opt/zb/store/2lvf1cavwkainjz32xzja04hfl5cimx6-hello/bin/hello
Hello, World!
In the next few sections, we’ll explain the zb.lua
script in more detail.
Derivation Basics¶
The first section downloads the standard library from GitHub:
local zb <const> = fetchArchive {
url = "https://github.com/256lights/zb-stdlib/releases/download/v0.1.1/zb-stdlib-v0.1.1.tar.gz";
hash = "sha256:ee4c78f4b1915c7dafb0d55e8cd6f20fe82396a21a6ab2add9bb879fb9301bc2";
}
fetchArchive()
is a built-in global function that returns a derivation
that extracts the tarball or zip file downloaded from a URL.
A derivation in zb is a build step:
a description of a program — called a builder — to run to produce files.
Derivations can depend on the results of other derivations.
Creating a derivation does not run its builder;
creating a derivations records how to invoke its builder.
We use zb build
to run builders,
or as we’ll see in a moment,
the import()
function will implicitly run the builder.
Finally, derivations can be used like strings.
For example, derivations can be concatenated or passed as an argument to tostring
.
Such a string is a placeholder for the derivation’s output file or directory,
and when used for other derivations,
it implicitly adds a dependency on the derivation.
Modules and Imports¶
The next section loads a Lua module from the zb standard library:
local stdenv = import(zb.."/stdenv/stdenv.lua")
Every Lua file that zb encounters is treated as a separate module.
The import()
built-in global function returns the module at the path given as an argument.
This is similar to the dofile
and require
functions in standalone Lua
(which are not supported in zb),
but import
is special in a few ways:
import
will load the module for any given path at most once during a run ofzb
.import
does not execute the module right away. Instead,import
returns a placeholder object that acts like the module. When you do anything with the placeholder object other than pass it around, it will then wait for the module to finish initialization.Globals are not shared among modules. Setting a “global” variable in a zb module will place it in a table which is implicitly returned by
import
if the module does not return any values.Everything in a module will be “frozen” when the end of the file is reached. This means that any changes to variables or tables (even locals) will raise an error.
Together, these aspects allow imports to be reordered or run lazily without fear of unintended side effects.
One other interesting property of the import()
function
is that if you use a path created from a derivation,
it will build the derivation.
So zb.."/stdenv/stdenv.lua"
will build the zb
derivation
and then import the stdenv/stdenv.lua
file
inside the output.
Making the Source Available to the Build¶
The path()
built-in function imports files for use in a derivation:
local src = path {
path = ".";
name = "hello-source";
filter = function(name)
return name == "hello.c"
end;
}
The filter
function allows us to create an allow-list of files in the folder to use.
Changing any file inside a source causes the derivation to be rebuilt on the next zb build
,
so minimizing the number of files is important for faster incremental builds.
Creating a Derivation¶
Finally, we declare a hello
variable with a derivation value:
-- Replace with your system, if necessary.
-- One of:
-- x86_64-unknown-linux
-- aarch64-apple-macos
local buildSystem <const> = "x86_64-unknown-linux"
hello = stdenv.makeDerivation {
pname = "hello";
src = src;
buildSystem = buildSystem;
buildPhase = "gcc -o hello hello.c";
installPhase = '\z
mkdir -p "$out/bin"\n\z
mv hello "$out/bin/hello"\n';
};
stdenv.makeDerivation
is a function that returns a derivation.
It provides GCC and a minimal set of standard Unix tools.
If the source contains a Makefile, then it uses that to build.
However, for our simple single-file program, we provide a buildPhase
directly.
buildPhase
specifies a snippet of Bash script that builds the program in the source directory.
The installPhase
specifies a snippet of Bash script
to copy the program to $out
,
the path to where the derivation’s output must be placed.
Using Dependencies¶
Now that we know the basics, let’s see how to pull in a C library from the internet.
Add the following to the end of zb.lua
:
sqlite3 = stdenv.makeDerivation {
pname = "sqlite3";
version = "3.50.1";
src = fetchurl {
url = "https://www.sqlite.org/2025/sqlite-autoconf-3500100.tar.gz";
hash = "sha256:00a65114d697cfaa8fe0630281d76fd1b77afcd95cd5e40ec6a02cbbadbfea71";
};
buildSystem = "aarch64-apple-macos";
}
Like before, we can build it with zb build zb.lua#sqlite3
.
Because the source archive includes a configure
script and a Makefile,
then stdenv.makeDerivation
knows how to build the package.
You can see that the resulting directory includes bin/sqlite3
,
a lib
directory,
and an include
directory.
Now let’s see how to compile the SQLite Quickstart example.
Create another file, hello_sql.c
, in the same directory as zb.lua
:
/* hello_sql.c */
#include <stdio.h>
#include <sqlite3.h>
static int callback(void *NotUsed, int argc, char **argv, char **azColName){
int i;
for(i=0; i<argc; i++){
printf("%s = %s\n", azColName[i], argv[i] ? argv[i] : "NULL");
}
printf("\n");
return 0;
}
int main(int argc, char **argv){
sqlite3 *db;
char *zErrMsg = 0;
int rc;
if( argc!=3 ){
fprintf(stderr, "Usage: %s DATABASE SQL-STATEMENT\n", argv[0]);
return(1);
}
rc = sqlite3_open(argv[1], &db);
if( rc ){
fprintf(stderr, "Can't open database: %s\n", sqlite3_errmsg(db));
sqlite3_close(db);
return(1);
}
rc = sqlite3_exec(db, argv[2], callback, 0, &zErrMsg);
if( rc!=SQLITE_OK ){
fprintf(stderr, "SQL error: %s\n", zErrMsg);
sqlite3_free(zErrMsg);
}
sqlite3_close(db);
return 0;
}
Then add to the end of zb.lua
:
local strings = import(zb.."/strings.lua")
hello_sql = stdenv.makeDerivation {
pname = "hello-sql";
src = path {
path = ".";
name = "hello-sql-source";
filter = function(name)
return name == "hello_sql.c"
end;
};
buildSystem = "aarch64-apple-macos";
C_INCLUDE_PATH = strings.makeIncludePath {
sqlite3
};
LIBRARY_PATH = strings.makeLibraryPath {
sqlite3
};
buildPhase = "gcc -o hello-sql -lsqlite3 hello_sql.c";
installPhase = '\z
mkdir -p "$out/bin"\n\z
mv hello-sql "$out/bin/hello-sql"\n';
}
This is mostly the same as our hello
example from before,
but we do a few new things:
We import the
strings.lua
file from the standard library. Thestrings.makeIncludePath
andstrings.makeLibraryPath
functions join the elements in the table with colons and appends/include
or/lib
, respectively, to each element.We set the
C_INCLUDE_PATH
andLIBRARY_PATH
environment variables to point to thesqlite3
derivation. As you’ll recall, derivations can be used like strings and automatically introduce a dependency fromhello_sql
tosqlite3
.
Wrapping Up¶
In this guide, we wrote a simple build configuration for a single-file C program. Then, we built SQLite from source, and finally, we built a C program that used SQLite as a dependency.
Throughout this tutorial, there have been links to reference documentation. The language reference describes the flavor of Lua that zb understands, as well as its built-in functions. The standard library repository includes other packages and utility functions that can be useful.
Here are the final versions of the files:
zb is still in early development. If you have questions or feedback, see the support guide. If you’re interested in getting involved, see the contributing guide.