We added support for incremental builds in garnix! The approach (at a very high level) is to override a special input with whatever the latest successful garnix build was; any “intermediates” outputs in one build then become an input in the next build. This can dramatically build times, especially for large projects.
We wrote a blog with more detailed explanation, and a comparison with other approaches.
Do you have a non-toy example? E.g. for a meson or cmake project? For some reason the example in the blog post is not clicking for me and I have a hard time understanding how to use this
I haven’t figured out a good way to do that, since it relies on infrastructure such as a database with previous build results for the same repo. But I put up this gist with our code for creating the normalized flake (which will be used with --override-input) given the right list of builds to base off of.
Relatedly, it’d be nice to have a standardized protocol here, so using incrementalism across CIs doesn’t result in vendor lock in. I’m happy to organize with anyone who wants to make that happen.
Yeah in retrospect I should have filled that explanation out a bit more.
In general, there are two existing ways in which tooling supports incrementalism: via timestamps and hashes. If with the latter, the idea is to just copy the directory where intermediate files go from the cache and into the build before building, and then out of the build and into $intermediates after the build is done.
GHC > 9.4 is like this, and actually the Nix functions in nixpkgs even already take arguments for incrementalism. Unfortunately they are very brittle (changing the package version breaks the build!). Still, incrementaliziing a build is quite easy:
That should work (it does for our project at least). And when I get around to upstreaming the preBuild changes it’ll be even simpler.
For systems that use a timestamp it’s a bit more work (since the timestamps will be clobbered by Nix and git), but not much. The basic idea is to again copy e.g. object files back and forth, but this time to set the modification time correctly enough. Let’s say you have a build that puts object files in ODIR and has source files in SRCDIR. The changes might look like this:
cache : pkgs.mkDerivation {
# Add "intermediates" to whatever you already had
outputs = [ "out" "intermediates" ];
preBuild = ''
cp -R ${cache}/* $ODIR
# Here we see if the file had the same hash as before. If not, it changed, so we
# update it's timestamp. Note that this might need to change if SRCDIR is not
# modifiable
if [ -f ${cache}/hashes]; then
for FILE in $(find $SRCDIR -type f); do
SHA=$(shasum $FILE)
if [ -z "$(grep $SHA ${cache}/hashes)" ]; then
touch $FILE
fi
fi
''
postInstall = ''
# Here we keep track of the hashes of the source files we saw. In preBuild we use
# this to set the timestamp: if we saw it before, it's old, if not, it's new
find $SRCDIR -not -path '*/\.*' -type f -exec shasum "{}" + > ${cache}/hashes
mv "$ODIR"/* "$intermediates"
'';
# the rest is as usual
}
I haven’t tried this one though, so there probably are bugs. Of course, it too can become simpler if some of the logic is upstreamed or provided as an external lib.