Merge pull request #167670 from messemar/incremental-builds

incremental builds: add derivation override functions

+1
doc/build-helpers/special.md
···
special/makesetuphook.section.md
special/mkshell.section.md
special/vm-tools.section.md
```
···
special/makesetuphook.section.md
special/mkshell.section.md
special/vm-tools.section.md
+
special/checkpoint-build.section.md
```
+36
doc/build-helpers/special/checkpoint-build.section.md
···
···
+
# pkgs.checkpointBuildTools {#sec-checkpoint-build}
+
+
`pkgs.checkpointBuildTools` provides a way to build derivations incrementally. It consists of two functions to make checkpoint builds using Nix possible.
+
+
For hermeticity, Nix derivations do not allow any state to carry over between builds, making a transparent incremental build within a derivation impossible.
+
+
However, we can tell Nix explicitly what the previous build state was, by representing that previous state as a derivation output. This allows the passed build state to be used for an incremental build.
+
+
To change a normal derivation to a checkpoint based build, these steps must be taken:
+
- apply `prepareCheckpointBuild` on the desired derivation
+
e.g.:
+
```nix
+
checkpointArtifacts = (pkgs.checkpointBuildTools.prepareCheckpointBuild pkgs.virtualbox);
+
```
+
- change something you want in the sources of the package. (e.g. using a source override)
+
```nix
+
changedVBox = pkgs.virtualbox.overrideAttrs (old: {
+
src = path/to/vbox/sources;
+
}
+
```
+
- use `mkCheckpointedBuild changedVBox buildOutput`
+
- enjoy shorter build times
+
+
## Example {#sec-checkpoint-build-example}
+
```nix
+
{ pkgs ? import <nixpkgs> {} }: with (pkgs) checkpointBuildTools;
+
let
+
helloCheckpoint = checkpointBuildTools.prepareCheckpointBuild pkgs.hello;
+
changedHello = pkgs.hello.overrideAttrs (_: {
+
doCheck = false;
+
patchPhase = ''
+
sed -i 's/Hello, world!/Hello, Nix!/g' src/hello.c
+
'';
+
});
+
in checkpointBuildTools.mkCheckpointBuild changedHello helloCheckpoint
+
```
+69
pkgs/build-support/checkpoint-build.nix
···
···
+
{ pkgs }:
+
rec {
+
/* Prepare a derivation for local builds.
+
*
+
* This function prepares checkpoint builds by provinding,
+
* containing the build output and the sources for cross checking.
+
* The build output can be used later to allow checkpoint builds
+
* by passing the derivation output to the `mkCheckpointBuild` function.
+
*
+
* To build a project with checkpoints follow these steps:
+
* - run prepareIncrementalBuild on the desired derivation
+
* e.G `incrementalBuildArtifacts = (pkgs.checkpointBuildTools.prepareCheckpointBuild pkgs.virtualbox);`
+
* - change something you want in the sources of the package( e.G using source override)
+
* changedVBox = pkgs.virtuabox.overrideAttrs (old: {
+
* src = path/to/vbox/sources;
+
* }
+
* - use `mkCheckpointedBuild changedVBox buildOutput`
+
* - enjoy shorter build times
+
*/
+
prepareCheckpointBuild = drv: drv.overrideAttrs (old: {
+
outputs = [ "out" ];
+
name = drv.name + "-checkpointArtifacts";
+
# To determine differences between the state of the build directory
+
# from an earlier build and a later one we store the state of the build
+
# directory before build, but after patch phases.
+
# This way, the same derivation can be used multiple times and only changes are detected.
+
# Additionally Removed files are handled correctly in later builds.
+
preBuild = (old.preBuild or "") + ''
+
mkdir -p $out/sources
+
cp -r ./* $out/sources/
+
'';
+
+
# After the build the build directory is copied again
+
# to get the output files.
+
# We copy the complete build folder, to take care for
+
# Build tools, building in the source directory, instead of
+
# having a build root directory, e.G the Linux kernel.
+
installPhase = ''
+
runHook preCheckpointInstall
+
mkdir -p $out/outputs
+
cp -r ./* $out/outputs/
+
runHook postCheckpointInstall
+
'';
+
});
+
+
/* Build a derivation based on the checkpoint output generated by
+
* the `prepareCheckpointBuild function.
+
*
+
* Usage:
+
* let
+
* checkpointArtifacts = prepareCheckpointBuild drv
+
* in mkCheckpointedBuild drv checkpointArtifacts
+
*/
+
mkCheckpointedBuild = drv: previousBuildArtifacts: drv.overrideAttrs (old: {
+
# The actual checkpoint build phase.
+
# We compare the changed sources from a previous build with the current and create a patch
+
# Afterwards we clean the build directory to copy the previous output files (Including the sources)
+
# The source difference patch is applied to get the latest changes again to allow short build times.
+
preBuild = (old.preBuild or "") + ''
+
set +e
+
diff -ur ${previousBuildArtifacts}/sources ./ > sourceDifference.patch
+
set -e
+
shopt -s extglob dotglob
+
rm -r !("sourceDifference.patch")
+
${pkgs.rsync}/bin/rsync -cutU --chown=$USER:$USER --chmod=+w -r ${previousBuildArtifacts}/outputs/* .
+
patch -p 1 -i sourceDifference.patch
+
'';
+
});
+
}
+57
pkgs/test/checkpointBuild/default.nix
···
···
+
{ hello, checkpointBuildTools, runCommandNoCC, texinfo, stdenv, rsync }:
+
let
+
baseHelloArtifacts = checkpointBuildTools.prepareCheckpointBuild hello;
+
patchedHello = hello.overrideAttrs (old: {
+
buildInputs = [ texinfo ];
+
src = runCommandNoCC "patch-hello-src" { } ''
+
mkdir -p $out
+
cd $out
+
tar xf ${hello.src} --strip-components=1
+
patch -p1 < ${./hello.patch}
+
'';
+
});
+
checkpointBuiltHello = checkpointBuildTools.mkCheckpointedBuild patchedHello baseHelloArtifacts;
+
+
checkpointBuiltHelloWithCheck = checkpointBuiltHello.overrideAttrs (old: {
+
doCheck = true;
+
checkPhase = ''
+
echo "checking if unchanged source file is not recompiled"
+
[ "$(stat --format="%Y" lib/exitfail.o)" = "$(stat --format="%Y" ${baseHelloArtifacts}/outputs/lib/exitfail.o)" ]
+
'';
+
});
+
+
baseHelloRemoveFileArtifacts = checkpointBuildTools.prepareCheckpointBuild (hello.overrideAttrs (old: {
+
patches = [ ./hello-additionalFile.patch ];
+
}));
+
+
preparedHelloRemoveFileSrc = runCommandNoCC "patch-hello-src" { } ''
+
mkdir -p $out
+
cd $out
+
tar xf ${hello.src} --strip-components=1
+
patch -p1 < ${./hello-additionalFile.patch}
+
'';
+
+
patchedHelloRemoveFile = hello.overrideAttrs (old: {
+
buildInputs = [ texinfo ];
+
src = runCommandNoCC "patch-hello-src" { } ''
+
mkdir -p $out
+
cd $out
+
${rsync}/bin/rsync -cutU --chown=$USER:$USER --chmod=+w -r ${preparedHelloRemoveFileSrc}/* .
+
patch -p1 < ${./hello-removeFile.patch}
+
'';
+
});
+
+
checkpointBuiltHelloWithRemovedFile = checkpointBuildTools.mkCheckpointedBuild patchedHelloRemoveFile baseHelloRemoveFileArtifacts;
+
in
+
stdenv.mkDerivation {
+
name = "patched-hello-returns-correct-output";
+
buildCommand = ''
+
touch $out
+
+
echo "testing output of hello binary"
+
[ "$(${checkpointBuiltHelloWithCheck}/bin/hello)" = "Hello, incremental world!" ]
+
echo "testing output of hello with removed file"
+
[ "$(${checkpointBuiltHelloWithRemovedFile}/bin/hello)" = "Hello, incremental world!" ]
+
'';
+
}
+
+67
pkgs/test/checkpointBuild/hello-additionalFile.patch
···
···
+
:100644 100644 0000000 0000000 M Makefile.in
+
:000000 100644 0000000 0000000 A src/additionalFile.c
+
:100644 100644 0000000 0000000 M src/hello.c
+
:100644 100644 0000000 0000000 M src/system.h
+
+
diff --git a/Makefile.in b/Makefile.in
+
index 1597d39..f63f830 100644
+
--- a/Makefile.in
+
+++ b/Makefile.in
+
@@ -312,7 +312,7 @@ am_lib_libhello_a_OBJECTS = lib/basename-lgpl.$(OBJEXT) \
+
lib/version-etc.$(OBJEXT) lib/version-etc-fsf.$(OBJEXT) \
+
lib/wctype-h.$(OBJEXT) lib/xmalloc.$(OBJEXT) \
+
lib/xalloc-die.$(OBJEXT) lib/xstriconv.$(OBJEXT) \
+
- lib/xstrndup.$(OBJEXT)
+
+ lib/xstrndup.$(OBJEXT) src/additionalFile.$(OBJEXT)
+
lib_libhello_a_OBJECTS = $(am_lib_libhello_a_OBJECTS)
+
am_hello_OBJECTS = src/hello.$(OBJEXT)
+
hello_OBJECTS = $(am_hello_OBJECTS)
+
@@ -1842,7 +1842,7 @@ lib_libhello_a_SOURCES = lib/basename-lgpl.c lib/c-ctype.h \
+
$(am__append_4) $(am__append_5) lib/version-etc.h \
+
lib/version-etc.c lib/version-etc-fsf.c lib/wctype-h.c \
+
lib/xmalloc.c lib/xalloc-die.c lib/xstriconv.h lib/xstriconv.c \
+
- lib/xstrndup.h lib/xstrndup.c
+
+ lib/xstrndup.h lib/xstrndup.c src/additionalFile.c
+
lib_libhello_a_LIBADD = $(gl_LIBOBJS)
+
lib_libhello_a_DEPENDENCIES = $(gl_LIBOBJS)
+
EXTRA_lib_libhello_a_SOURCES = lib/close.c lib/stripslash.c lib/dup2.c \
+
diff --git a/src/additionalFile.c b/src/additionalFile.c
+
new file mode 100644
+
index 0000000..34d683d
+
--- /dev/null
+
+++ b/src/additionalFile.c
+
@@ -0,0 +1,6 @@
+
+#include "config.h"
+
+#include "system.h"
+
+
+
+int somefunc() {
+
+ return 0;
+
+}
+
diff --git a/src/hello.c b/src/hello.c
+
index 2e7d38e..a8e36dc 100644
+
--- a/src/hello.c
+
+++ b/src/hello.c
+
@@ -146,7 +146,11 @@ main (int argc, char *argv[])
+
#endif
+
+
/* Having initialized gettext, get the default message. */
+
- greeting_msg = _("Hello, world!");
+
+ if (somefunc() == 0) {
+
+ greeting_msg = _("Hello, world!");
+
+ } else {
+
+ greeting_msg = _("Hello, incremental world!");
+
+ }
+
+
/* Even exiting has subtleties. On exit, if any writes failed, change
+
the exit status. The /dev/full device on GNU/Linux can be used for
+
diff --git a/src/system.h b/src/system.h
+
index d39cdb9..dc425d2 100644
+
--- a/src/system.h
+
+++ b/src/system.h
+
@@ -59,4 +59,6 @@
+
} \
+
while (0)
+
+
+int somefunc();
+
+
+
#endif /* HELLO_SYSTEM_H */
+67
pkgs/test/checkpointBuild/hello-removeFile.patch
···
···
+
:100644 100644 0000000 0000000 M Makefile.in
+
:100644 000000 0000000 0000000 D src/additionalFile.c
+
:100644 100644 0000000 0000000 M src/hello.c
+
:100755 100755 0000000 0000000 M tests/hello-1
+
+
diff --git a/Makefile.in b/Makefile.in
+
index f63f830..1597d39 100644
+
--- a/Makefile.in
+
+++ b/Makefile.in
+
@@ -312,7 +312,7 @@ am_lib_libhello_a_OBJECTS = lib/basename-lgpl.$(OBJEXT) \
+
lib/version-etc.$(OBJEXT) lib/version-etc-fsf.$(OBJEXT) \
+
lib/wctype-h.$(OBJEXT) lib/xmalloc.$(OBJEXT) \
+
lib/xalloc-die.$(OBJEXT) lib/xstriconv.$(OBJEXT) \
+
- lib/xstrndup.$(OBJEXT) src/additionalFile.$(OBJEXT)
+
+ lib/xstrndup.$(OBJEXT)
+
lib_libhello_a_OBJECTS = $(am_lib_libhello_a_OBJECTS)
+
am_hello_OBJECTS = src/hello.$(OBJEXT)
+
hello_OBJECTS = $(am_hello_OBJECTS)
+
@@ -1842,7 +1842,7 @@ lib_libhello_a_SOURCES = lib/basename-lgpl.c lib/c-ctype.h \
+
$(am__append_4) $(am__append_5) lib/version-etc.h \
+
lib/version-etc.c lib/version-etc-fsf.c lib/wctype-h.c \
+
lib/xmalloc.c lib/xalloc-die.c lib/xstriconv.h lib/xstriconv.c \
+
- lib/xstrndup.h lib/xstrndup.c src/additionalFile.c
+
+ lib/xstrndup.h lib/xstrndup.c
+
lib_libhello_a_LIBADD = $(gl_LIBOBJS)
+
lib_libhello_a_DEPENDENCIES = $(gl_LIBOBJS)
+
EXTRA_lib_libhello_a_SOURCES = lib/close.c lib/stripslash.c lib/dup2.c \
+
diff --git a/src/additionalFile.c b/src/additionalFile.c
+
deleted file mode 100644
+
index 34d683d..0000000
+
--- a/src/additionalFile.c
+
+++ /dev/null
+
@@ -1,6 +0,0 @@
+
-#include "config.h"
+
-#include "system.h"
+
-
+
-int somefunc() {
+
- return 0;
+
-}
+
diff --git a/src/hello.c b/src/hello.c
+
index a8e36dc..53722d9 100644
+
--- a/src/hello.c
+
+++ b/src/hello.c
+
@@ -126,6 +126,10 @@ parse_options (int argc, char *argv[], const char **greeting_msg)
+
}
+
}
+
+
+int somefunc() {
+
+ return 1;
+
+}
+
+
+
int
+
main (int argc, char *argv[])
+
{
+
diff --git a/tests/hello-1 b/tests/hello-1
+
index 96ffef8..f0b9f8d 100755
+
--- a/tests/hello-1
+
+++ b/tests/hello-1
+
@@ -21,7 +21,7 @@ export LANGUAGE LC_ALL LC_MESSAGES LANG
+
+
tmpfiles="hello-test1.ok"
+
cat <<EOF > hello-test1.ok
+
-Hello, world!
+
+Hello, incremental world!
+
EOF
+
+
tmpfiles="$tmpfiles hello-test1.out"
+26
pkgs/test/checkpointBuild/hello.patch
···
···
+
diff --git a/src/hello.c b/src/hello.c
+
index 182303c..453962f 100644
+
--- a/src/hello.c
+
+++ b/src/hello.c
+
@@ -57,7 +57,7 @@ main (int argc, char *argv[])
+
#endif
+
+
/* Having initialized gettext, get the default message. */
+
- greeting_msg = _("Hello, world!");
+
+ greeting_msg = _("Hello, incremental world!");
+
+
/* Even exiting has subtleties. On exit, if any writes failed, change
+
the exit status. The /dev/full device on GNU/Linux can be used for
+
diff --git a/tests/hello-1 b/tests/hello-1
+
index 3b7a815..e15fa95 100755
+
--- a/tests/hello-1
+
+++ b/tests/hello-1
+
@@ -21,7 +21,7 @@ export LANGUAGE LC_ALL LC_MESSAGES LANG
+
+
tmpfiles="hello-test1.ok"
+
cat <<EOF > hello-test1.ok
+
-Hello, world!
+
+Hello, incremental world!
+
EOF
+
+
tmpfiles="$tmpfiles hello-test1.out"
+2
pkgs/test/default.nix
···
install-shell-files = callPackage ./install-shell-files {};
kernel-config = callPackage ./kernel.nix {};
ld-library-path = callPackage ./ld-library-path {};
···
install-shell-files = callPackage ./install-shell-files {};
+
checkpoint-build = callPackage ./checkpointBuild {};
+
kernel-config = callPackage ./kernel.nix {};
ld-library-path = callPackage ./ld-library-path {};
+2
pkgs/top-level/all-packages.nix
···
camunda-modeler = callPackage ../applications/misc/camunda-modeler { };
caroline = callPackage ../development/libraries/caroline { };
cartridges = callPackage ../applications/misc/cartridges { };
···
camunda-modeler = callPackage ../applications/misc/camunda-modeler { };
+
checkpointBuildTools = callPackage ../build-support/checkpoint-build.nix {};
+
caroline = callPackage ../development/libraries/caroline { };
cartridges = callPackage ../applications/misc/cartridges { };