1# Builds an ext4 image containing a populated /nix/store with the closure
2# of store paths passed in the storePaths parameter. The generated image
3# is sized to only fit its contents, with the expectation that a script
4# resizes the filesystem at boot time.
5{ pkgs
6, storePaths
7, volumeLabel
8, uuid ? "44444444-4444-4444-8888-888888888888"
9, e2fsprogs
10, libfaketime
11, perl
12}:
13
14let
15 sdClosureInfo = pkgs.buildPackages.closureInfo { rootPaths = storePaths; };
16in
17
18pkgs.stdenv.mkDerivation {
19 name = "ext4-fs.img";
20
21 nativeBuildInputs = [e2fsprogs.bin libfaketime perl];
22
23 buildCommand =
24 ''
25 # Add the closures of the top-level store objects.
26 storePaths=$(cat ${sdClosureInfo}/store-paths)
27
28 # Also include a manifest of the closures in a format suitable for nix-store --load-db.
29 cp ${sdClosureInfo}/registration nix-path-registration
30
31 # Make a crude approximation of the size of the target image.
32 # If the script starts failing, increase the fudge factors here.
33 numInodes=$(find $storePaths | wc -l)
34 numDataBlocks=$(du -c -B 4096 --apparent-size $storePaths | awk '$2 == "total" { print int($1 * 1.03) }')
35 bytes=$((2 * 4096 * $numInodes + 4096 * $numDataBlocks))
36 echo "Creating an EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks)"
37
38 truncate -s $bytes $out
39 faketime -f "1970-01-01 00:00:01" mkfs.ext4 -L ${volumeLabel} -U ${uuid} $out
40
41 # Populate the image contents by piping a bunch of commands to the `debugfs` tool from e2fsprogs.
42 # For example, to copy /nix/store/abcd...efg-coreutils-8.23/bin/sleep:
43 # cd /nix/store/abcd...efg-coreutils-8.23/bin
44 # write /nix/store/abcd...efg-coreutils-8.23/bin/sleep sleep
45 # sif sleep mode 040555
46 # sif sleep gid 30000
47 # In particular, debugfs doesn't handle absolute target paths; you have to 'cd' in the virtual
48 # filesystem first. Likewise the intermediate directories must already exist (using `find`
49 # handles that for us). And when setting the file's permissions, the inode type flags (__S_IFDIR,
50 # __S_IFREG) need to be set as well.
51 (
52 echo write nix-path-registration nix-path-registration
53 echo mkdir nix
54 echo cd /nix
55 echo mkdir store
56
57 # XXX: This explodes in exciting ways if anything in /nix/store has a space in it.
58 find $storePaths -printf '%y %f %h %m\n'| while read -r type file dir perms; do
59 # echo "TYPE=$type DIR=$dir FILE=$file PERMS=$perms" >&2
60
61 echo "cd $dir"
62 case $type in
63 d)
64 echo "mkdir $file"
65 echo sif $file mode $((040000 | 0$perms)) # magic constant is __S_IFDIR
66 ;;
67 f)
68 echo "write $dir/$file $file"
69 echo sif $file mode $((0100000 | 0$perms)) # magic constant is __S_IFREG
70 ;;
71 l)
72 echo "symlink $file $(readlink "$dir/$file")"
73 ;;
74 *)
75 echo "Unknown entry: $type $dir $file $perms" >&2
76 exit 1
77 ;;
78 esac
79
80 echo sif $file gid 30000 # chgrp to nixbld
81 done
82 ) | faketime -f "1970-01-01 00:00:01" debugfs -w $out -f /dev/stdin > errorlog 2>&1
83
84 # The debugfs tool doesn't terminate on error nor exit with a non-zero status. Check manually.
85 if egrep -q 'Could not allocate|File not found' errorlog; then
86 cat errorlog
87 echo "--- Failed to create EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---"
88 return 1
89 fi
90
91 # I have ended up with corrupted images sometimes, I suspect that happens when the build machine's disk gets full during the build.
92 if ! fsck.ext4 -n -f $out; then
93 echo "--- Fsck failed for EXT4 image of $bytes bytes (numInodes=$numInodes, numDataBlocks=$numDataBlocks) ---"
94 cat errorlog
95 return 1
96 fi
97 '';
98}