docs: More samples.

Change-Id: Ibcab3b04ae1650cd213a5a0915b7c6056a6a6964
This commit is contained in:
puck 2025-11-24 18:37:10 +00:00
parent 8385c3ac9b
commit 3ee4d894f5
14 changed files with 552 additions and 0 deletions

View file

@ -67,6 +67,7 @@ const highlighter = createHighlighterCoreSync({
require("@shikijs/langs/shellsession").default,
require("@shikijs/langs/bash").default,
require("@shikijs/langs/nix").default,
require("@shikijs/langs/json").default,
],
engine: createJavaScriptRegexEngine(),
});

View file

@ -13,6 +13,11 @@
*** xref:ninja/usage.adoc[]
*** xref:ninja/man.adoc[Man page]
* Samples
** xref:samples/1-go.adoc[1: Go]
** xref:samples/2-rust.adoc[2: Rust]
** xref:samples/3-cpp.adoc[3: C++]
* Core concepts
** xref:core/derivations.adoc[]
** xref:core/zexp.adoc[]

View file

@ -0,0 +1,79 @@
= 1: Go
:page-pagination: next
This is a minimal sample for Zilch's Go support, intended to show off the
ability to edit dependencies with ease, without losing the reproducibility that
Zilch provides.
To start, run `zilch-cli-go -m examples/go` and see the resulting binary
operate:
[,console]
----
$ zilch-cli-go -m examples/go
example.com/go /nix/store/wh1iaiplnkznh8x4rgqxs89qn90xaf3v-example.com_go
$ /nix/store/wh1iaiplnkznh8x4rgqxs89qn90xaf3v-example.com_go
1980-01-01T13:37:00.000Z INFO main/main.go:9 This is an example Go program. {"exampleString": "yes", "example": true}
----
---
If, say, you find a bug (e.g. you are of the opinion the line number should come
before the file name), this has to be patched in the logging module, which is
provided by zap. Clone the repo (`https://github.com/uber-go/zap`). In our case
we have it at `./zap`. To make sure the bug isn't patched in this current
version, let's first try building the example program again with it. This can be
done with the `--replace` command, or `-r` for short. This takes a directory
with a `go.mod`, and prioritizes it over the version of the Go module used in
the `go.mod` of the main module.
[,console]
----
$ zilch-cli-go -m examples/go -r zap
example.com/go /nix/store/75r8574ccq8vhsjyrr8jm9icdy5f19c1-example.com_go
$ /nix/store/75r8574ccq8vhsjyrr8jm9icdy5f19c1-example.com_go
1980-01-01T13:37:00.000Z INFO main/main.go:9 This is an example Go program. {"exampleString": "yes", "example": true}
----
Sadly, after updating, nothing changed. So we'll have to make changes. The
filename and line number are written in `zap/zapcore/entry.go`, around line 91;
inside `FullPath()`. After patching the function to swap them around, rerunning
the `zilch-cli-go` command shown previously will go through and recompile only
the changed files. So:
[,console]
----
$ zilch-cli-go -m examples/go -r zap
example.com/go /nix/store/vl6dd9d4ry7xhh31vpimjsn9khpqmxgb-example.com_go
$ /nix/store/vl6dd9d4ry7xhh31vpimjsn9khpqmxgb-example.com_go
1980-01-01T13:37:00.000Z INFO 9:main/main.go This is an example Go program. {"exampleString": "yes", "example": true}
----
As you can see, the bug is fixed, by testing with a project in a different repo!
---
Whenever you run `zilch-cli-go`, the script runs from scratch, not using any
previous information. It performs a series of steps to build a build graph:
First, it walks the `go.mod` of the main module, as well as all its
dependencies, using the `go.sum` as a guide to ensure this all stays content-
addressed, similar to the `go` tool. Once it has all the modules that are used
in the program, it looks for all packages inside each of the modules. Each
package is then analyzed for which other packages it imports, which is used to
build Zilch-native build information.
At this point, enough information has been gathered to build the resulting
build graph. The main package's derivation is materialized into the Nix store,
and with it, all its dependencies are pulled in. All its dependencies are built
with two outputs; both the native code and the API are separately stored. This
ensures that any changes that don't affect the interface that other packages
depend on do not cause a butterfly effect, and stay localized to the native
code.
At no point does Zilch run any of this code on the local system, either. All
builds and information gathering are done entirely inside the Nix daemon, which
provides for a stable and sandboxed environment.

View file

@ -0,0 +1,67 @@
= 2: Rust
:page-pagination:
This sample assumes you've tried the Go example, as the Rust implementation of
these concepts work identically. This example dives into the details of the
overrides, which are necessary to run Rust builds in a minimal sandbox.
Most Rust projects depend on external dependencies, written in e.g. C, and
cannot be compiled with purely `rustc` inside a sandbox. To allow for this,
Zilch's Rust support has been designed with this fact in mind.
If we try to compile the example in this directory as-is, it fails, as the
crate depends on `pyo3`, which depends on a working Python interpreter:
[,console]
----
$ zilch-cli-rust -m examples/rust
Error: (error Error 0 builder for '/nix/store/n5l4w8m3fj3m1s06p75r0pqj8s9l8cmf-build.rs-run.drv' failed with exit code 101;
last 17 log lines:
> error: no Python 3.x interpreter found
> Package: pyo3-build-config
> note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
----
To fix this, we'll need to write an override file. This file describes to Zilch
how to override the Rust environment. There's three override points, as described
in xref:rust/usage.adoc[the more direct usage file]. We'll only use `buildScript`
here, as `pyo3` is simple. Looking at the code, it seems that it expects a `python3`
on `PATH`. To do this, we just need to write a fragment for that. Zilch knows how
to append to these variables, so all we need is a file that contains the following:
.zilch-override.json
[,json]
----
{
"pyo3-build-config": {
"buildScript": {
"PATH": "${pkgs.python3}/bin"
}
}
}
----
Now, when the `build.rs` for `pyo3-build-config` is run, it will have `python3`
available to it. The output that this build script generates, which is stored
in the Nix store, also has references to the parts of `python3` that the build
script used, ensuring that it is properly passed through to the crates that, in
turn, depend on this build script; and the final binary has references to not
just libc, but also python3:
[,console]
----
$ zilch-cli-rust -m examples/rust -z override.json
zilch-pyo3-example zilch_pyo3_example bin /nix/store/vqpj68dgswf8v64sq45banq6vn0fshnz-rustc-bin-zilch_pyo3_example-link
$ nix-store -qR /nix/store/vqpj68dgswf8v64sq45banq6vn0fshnz-rustc-bin-zilch_pyo3_example-link | grep python
/nix/store/cfapjd2rvqrpry4grb0kljnp8bvnvfxz-python3-3.13.8
# hooray!
----
Zilch ships with a series of overrides, primarily around what I (the developer)
have come across. Some of these also use a helper that Zilch provides, which
sets up `pkg-config` based packages recursively, the same way Nixpkgs does.

View file

@ -0,0 +1,135 @@
= 3: C++
:page-pagination: prev
Following up from Rust, Zilch has support for using the build graph from a Ninja
file. However, unlike Rust and Go, there's no one "blessed" way to build C/C++
code, so this isn't as plug-and-play as the other two languages.
To start, the support in Zilch rests upon Nixpkgs' existing derivations. If you
don't have an existing Nixpkgs derivation for the program, you'll need one. As
an example, we'll use `libinput` here.
[,scheme]
----
(environment: "pkgs.libinput"
depfile-path: "libinput-deps.scm")
----
In this sample, `pkgs.libinput` is a Nix expression run inside `nixpkgs`, similar
to how `nix-shell -p` behaves. When you run `zilch-cli-ninja -f libinput.scm build`,
Zilch splits up the derivation into three steps, split up by the `stdenv` build phases:
[,subs="verbatim,macros"]
----
unpackPhase
patchPhase
configurePhase
----8<---- cut derivation up here...
pass:[<del>buildPhase</del>] replace this with Zilch magic...
---->8---- ...and cut here!
checkPhase
installPhase
fixupPhase
installCheckPhase
distPhase
----
The original derivation is run up to the point where it would actually build
code, then the information is extracted from it, and Zilch does its magic: it
finds the Ninja file, and evaluates it just as Ninja itself would do; just with
each output being generated in its own little sandboxed derivation. Header files,
as well as source files (e.g. `.c`, and `.cpp`) are only visible to a build step
when it needs it, as each of these files causes spurious rebuilds if they aren't
actually necessary.
Once the build is complete, Zilch runs the last few phases, and stitches the
resulting derivation back together; making for a store path that is identical
to what `nixpkgs` normally generates!
The `depfile-path`, as passed to Zilch, is the one exception to the "stateless"
behavior: When compiling a source file, a side effect is figuring out all the
header files that get included. To save compute time (and because having them
around shouldn't materially affect builds), Zilch will build files with all
headers the first time, and use that to figure out which headers to include
next time. This information is stored in the `depfile`, which has a big map
of files to their dependencies. When a file fails to build (because it includes
a new header that wasn't previously visible), it will get rebuilt with all
headers again, to regenerate this database. This way, the `depfile` always
contains the minimal amount of headers necessary to build each file.
---
In some cases, projects may not compile inside the Nix sandbox as-is, due to
various reasons, e.g. symlinks, or missing dependencies. In this case, it's
possible to add targeted patches. As an example, the expression I use to build
Lix inside the sandbox is as follows:
[,scheme]
----
(environment: "(pkgs.lixPackageSets.lix_2_93.override { enableDocumentation = false; }).overrideAttrs (a: { doCheck = false; doInstallCheck = false; separateDebugInfo = false; __structuredAttrs = false; })"
depfile-path: "lix-deps.scm"
; The `config.h` file needs to be available to _all_ builds this file is
; inserted via -include on the command line, which doesn't make it into the
; dependency files, so dependency tracking thinks it's unused.
disallow-elide: (lambda (path) (string=? path "config.h"))
patch:
(begin
(import (zilch lang ninja) (srfi 152))
(lambda (target)
(if
; If our first implicit dependency is "python"...
(and
(not (null? (build-edge-implicit-dependencies-target target)))
(string-contains (car (build-edge-implicit-dependencies target)) "python"))
; Materialize (copy the actual files, rather than symlink them) the code-generation logic,
; python resolves imports relative to the target of the symlink, which is somewhere in the
; Nix store, as opposed to the source build tree we build up.
(string-append
"rm -rf src/lix/code-generation; "
"cp -rf -L --no-preserve=ownership \"$_ZILCH_ROOT\"/src/lix/code-generation src/lix/code-generation; "
"chmod ugo+rw -R src/lix/code-generation")
#f))))
----
This is a bit more of a complicated configuration, but shows the
configurability that's (sadly necessarily) possible for Zilch.
---
Editing the source of a project in a way that is compatible with Zilch is a bit
more complex than Go and Rust. To simplify this, `zilch-cli-ninja` has two
subcommands to handle this complexity: `zilch-cli-ninja source [DIR]` and
`zilch-cli-ninja diff`. The former will extract the source code of the project
just before build, and the latter will show the current diff between the
original derivation's source and the exposed one.
Once you've made changes to the project, you can incrementally rebuild it using
`--source` (`-s`), or by adding `override-source: "new/path"` to the
configuration file. The latter approach is mostly convenient for the next feature
we'll cover: nested patches!
---
The `--rewrite` feature on `zilch-cli-go` and `zilch-cli-rust` has an equivalent
in its Ninja support. However, as with the previous explanations, it's not _quite_
as simple. It needs to be configured inside the configuration file:
[,scheme]
----
(environment: "pkgs.labwc"
depfile-path: "labwc-deps.scm"
rewrite:
("libinput-1.29.1"
environment: "pkgs.libinput"
override-source: "./libinput-src"
depfile-path: "libinput-deps.scm"))
----
As you can see, the original libinput configuration is now nested _inside_ labwc's,
and using `-p libinput-1.29.1` lets you operate on it. But now, running just
`zilch-cli-ninja build` builds `labwc`, with the patched `libinput`! And it's
possible to arbitrarily nest these, of course; and any changes should
incrementally percolate down the full chain.