zilch/docs/modules/ROOT/pages/samples/3-cpp.adoc
Puck Meerburg 3ee4d894f5 docs: More samples.
Change-Id: Ibcab3b04ae1650cd213a5a0915b7c6056a6a6964
2025-11-28 15:24:57 +00:00

135 lines
5.6 KiB
Text

= 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.