1{ pkgs
2, lib
3
4, # The NixOS configuration to be installed onto the disk image.
5 config
6
7, # The size of the disk, in megabytes.
8 diskSize
9
10 # The files and directories to be placed in the target file system.
11 # This is a list of attribute sets {source, target} where `source'
12 # is the file system object (regular file or directory) to be
13 # grafted in the file system at path `target'.
14, contents ? []
15
16, # Type of partition table to use; either "legacy", "efi", or "none".
17 # For "efi" images, the GPT partition table is used and a mandatory ESP
18 # partition of reasonable size is created in addition to the root partition.
19 # If `installBootLoader` is true, GRUB will be installed in EFI mode.
20 # For "legacy", the msdos partition table is used and a single large root
21 # partition is created. If `installBootLoader` is true, GRUB will be
22 # installed in legacy mode.
23 # For "none", no partition table is created. Enabling `installBootLoader`
24 # most likely fails as GRUB will probably refuse to install.
25 partitionTableType ? "legacy"
26
27 # Whether to invoke switch-to-configuration boot during image creation
28, installBootLoader ? true
29
30, # The root file system type.
31 fsType ? "ext4"
32
33, # The initial NixOS configuration file to be copied to
34 # /etc/nixos/configuration.nix.
35 configFile ? null
36
37, # Shell code executed after the VM has finished.
38 postVM ? ""
39
40, name ? "nixos-disk-image"
41
42, # Disk image format, one of qcow2, qcow2-compressed, vpc, raw.
43 format ? "raw"
44}:
45
46assert partitionTableType == "legacy" || partitionTableType == "efi" || partitionTableType == "none";
47# We use -E offset=X below, which is only supported by e2fsprogs
48assert partitionTableType != "none" -> fsType == "ext4";
49
50with lib;
51
52let format' = format; in let
53
54 format = if format' == "qcow2-compressed" then "qcow2" else format';
55
56 compress = optionalString (format' == "qcow2-compressed") "-c";
57
58 filename = "nixos." + {
59 qcow2 = "qcow2";
60 vpc = "vhd";
61 raw = "img";
62 }.${format};
63
64 rootPartition = { # switch-case
65 legacy = "1";
66 efi = "2";
67 }.${partitionTableType};
68
69 partitionDiskScript = { # switch-case
70 legacy = ''
71 parted --script $diskImage -- \
72 mklabel msdos \
73 mkpart primary ext4 1MiB -1
74 '';
75 efi = ''
76 parted --script $diskImage -- \
77 mklabel gpt \
78 mkpart ESP fat32 8MiB 256MiB \
79 set 1 boot on \
80 mkpart primary ext4 256MiB -1
81 '';
82 none = "";
83 }.${partitionTableType};
84
85 nixpkgs = cleanSource pkgs.path;
86
87 # FIXME: merge with channel.nix / make-channel.nix.
88 channelSources = pkgs.runCommand "nixos-${config.system.nixos.version}" {} ''
89 mkdir -p $out
90 cp -prd ${nixpkgs} $out/nixos
91 chmod -R u+w $out/nixos
92 if [ ! -e $out/nixos/nixpkgs ]; then
93 ln -s . $out/nixos/nixpkgs
94 fi
95 rm -rf $out/nixos/.git
96 echo -n ${config.system.nixos.versionSuffix} > $out/nixos/.version-suffix
97 '';
98
99 binPath = with pkgs; makeBinPath (
100 [ rsync
101 utillinux
102 parted
103 e2fsprogs
104 lkl
105 config.system.build.nixos-install
106 config.system.build.nixos-enter
107 nix
108 ] ++ stdenv.initialPath);
109
110 # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate
111 # image building logic. The comment right below this now appears in 4 different places in nixpkgs :)
112 # !!! should use XML.
113 sources = map (x: x.source) contents;
114 targets = map (x: x.target) contents;
115
116 closureInfo = pkgs.closureInfo { rootPaths = [ config.system.build.toplevel channelSources ]; };
117
118 prepareImage = ''
119 export PATH=${binPath}
120
121 # Yes, mkfs.ext4 takes different units in different contexts. Fun.
122 sectorsToKilobytes() {
123 echo $(( ( "$1" * 512 ) / 1024 ))
124 }
125
126 sectorsToBytes() {
127 echo $(( "$1" * 512 ))
128 }
129
130 mkdir $out
131 diskImage=nixos.raw
132 truncate -s ${toString diskSize}M $diskImage
133
134 ${partitionDiskScript}
135
136 ${if partitionTableType != "none" then ''
137 # Get start & length of the root partition in sectors to $START and $SECTORS.
138 eval $(partx $diskImage -o START,SECTORS --nr ${rootPartition} --pairs)
139
140 mkfs.${fsType} -F -L nixos $diskImage -E offset=$(sectorsToBytes $START) $(sectorsToKilobytes $SECTORS)K
141 '' else ''
142 mkfs.${fsType} -F -L nixos $diskImage
143 ''}
144
145 root="$PWD/root"
146 mkdir -p $root
147
148 # Copy arbitrary other files into the image
149 # Semi-shamelessly copied from make-etc.sh. I (@copumpkin) shall factor this stuff out as part of
150 # https://github.com/NixOS/nixpkgs/issues/23052.
151 set -f
152 sources_=(${concatStringsSep " " sources})
153 targets_=(${concatStringsSep " " targets})
154 set +f
155
156 for ((i = 0; i < ''${#targets_[@]}; i++)); do
157 source="''${sources_[$i]}"
158 target="''${targets_[$i]}"
159
160 if [[ "$source" =~ '*' ]]; then
161 # If the source name contains '*', perform globbing.
162 mkdir -p $root/$target
163 for fn in $source; do
164 rsync -a --no-o --no-g "$fn" $root/$target/
165 done
166 else
167 mkdir -p $root/$(dirname $target)
168 if ! [ -e $root/$target ]; then
169 rsync -a --no-o --no-g $source $root/$target
170 else
171 echo "duplicate entry $target -> $source"
172 exit 1
173 fi
174 fi
175 done
176
177 export HOME=$TMPDIR
178
179 # Provide a Nix database so that nixos-install can copy closures.
180 export NIX_STATE_DIR=$TMPDIR/state
181 nix-store --load-db < ${closureInfo}/registration
182
183 echo "running nixos-install..."
184 nixos-install --root $root --no-bootloader --no-root-passwd \
185 --system ${config.system.build.toplevel} --channel ${channelSources} --substituters ""
186
187 echo "copying staging root to image..."
188 cptofs -p ${optionalString (partitionTableType != "none") "-P ${rootPartition}"} -t ${fsType} -i $diskImage $root/* /
189 '';
190in pkgs.vmTools.runInLinuxVM (
191 pkgs.runCommand name
192 { preVM = prepareImage;
193 buildInputs = with pkgs; [ utillinux e2fsprogs dosfstools ];
194 postVM = ''
195 ${if format == "raw" then ''
196 mv $diskImage $out/${filename}
197 '' else ''
198 ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${format} ${compress} $diskImage $out/${filename}
199 ''}
200 diskImage=$out/${filename}
201 ${postVM}
202 '';
203 memSize = 1024;
204 }
205 ''
206 export PATH=${binPath}:$PATH
207
208 rootDisk=${if partitionTableType != "none" then "/dev/vda${rootPartition}" else "/dev/vda"}
209
210 # Some tools assume these exist
211 ln -s vda /dev/xvda
212 ln -s vda /dev/sda
213
214 mountPoint=/mnt
215 mkdir $mountPoint
216 mount $rootDisk $mountPoint
217
218 # Create the ESP and mount it. Unlike e2fsprogs, mkfs.vfat doesn't support an
219 # '-E offset=X' option, so we can't do this outside the VM.
220 ${optionalString (partitionTableType == "efi") ''
221 mkdir -p /mnt/boot
222 mkfs.vfat -n ESP /dev/vda1
223 mount /dev/vda1 /mnt/boot
224 ''}
225
226 # Install a configuration.nix
227 mkdir -p /mnt/etc/nixos
228 ${optionalString (configFile != null) ''
229 cp ${configFile} /mnt/etc/nixos/configuration.nix
230 ''}
231
232 # Set up core system link, GRUB, etc.
233 NIXOS_INSTALL_BOOTLOADER=1 nixos-enter --root $mountPoint -- /nix/var/nix/profiles/system/bin/switch-to-configuration boot
234
235 # The above scripts will generate a random machine-id and we don't want to bake a single ID into all our images
236 rm -f $mountPoint/etc/machine-id
237
238 umount -R /mnt
239
240 # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal
241 # mount, so the `-c 0` and `-i 0` don't affect it. Setting it to `now` doesn't produce deterministic
242 # output, of course, but we can fix that when/if we start making images deterministic.
243 ${optionalString (fsType == "ext4") ''
244 tune2fs -T now -c 0 -i 0 $rootDisk
245 ''}
246 ''
247)