summary refs log tree commit diff
path: root/nixos/lib/make-disk-image.nix
blob: f1cbd0b9c08d1d985c56631316a99db99c4e9e9b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
{ pkgs
, lib

, # The NixOS configuration to be installed onto the disk image.
  config

, # The size of the disk, in megabytes.
  diskSize

  # The files and directories to be placed in the target file system.
  # This is a list of attribute sets {source, target} where `source'
  # is the file system object (regular file or directory) to be
  # grafted in the file system at path `target'.
, contents ? []

, # Whether the disk should be partitioned (with a single partition
  # containing the root filesystem) or contain the root filesystem
  # directly.
  partitioned ? true

  # Whether to invoke switch-to-configuration boot during image creation
, installBootLoader ? true

, # The root file system type.
  fsType ? "ext4"

, # The initial NixOS configuration file to be copied to
  # /etc/nixos/configuration.nix.
  configFile ? null

, # Shell code executed after the VM has finished.
  postVM ? ""

, name ? "nixos-disk-image"

, # Disk image format, one of qcow2, vpc, raw.
  format ? "raw"
}:

with lib;

let
  extensions = {
    qcow2 = "qcow2";
    vpc   = "vhd";
    raw   = "img";
  };

  nixpkgs = cleanSource pkgs.path;

  channelSources = pkgs.runCommand "nixos-${config.system.nixosVersion}" {} ''
    mkdir -p $out
    cp -prd ${nixpkgs} $out/nixos
    chmod -R u+w $out/nixos
    if [ ! -e $out/nixos/nixpkgs ]; then
      ln -s . $out/nixos/nixpkgs
    fi
    rm -rf $out/nixos/.git
    echo -n ${config.system.nixosVersionSuffix} > $out/nixos/.version-suffix
  '';

  metaClosure = pkgs.writeText "meta" ''
    ${config.system.build.toplevel}
    ${config.nix.package.out}
    ${channelSources}
  '';

  prepareImageInputs = with pkgs; [ rsync utillinux parted e2fsprogs lkl fakeroot libfaketime config.system.build.nixos-prepare-root ] ++ stdenv.initialPath;

  # I'm preserving the line below because I'm going to search for it across nixpkgs to consolidate
  # image building logic. The comment right below this now appears in 4 different places in nixpkgs :)
  # !!! should use XML.
  sources = map (x: x.source) contents;
  targets = map (x: x.target) contents;

  prepareImage = ''
    export PATH=${makeSearchPathOutput "bin" "bin" prepareImageInputs}

    mkdir $out
    diskImage=nixos.raw
    truncate -s ${toString diskSize}M $diskImage

    ${if partitioned then ''
      parted --script $diskImage -- mklabel msdos mkpart primary ext4 1M -1s
      offset=$((2048*512))
    '' else ''
      offset=0
    ''}

    faketime -f "1970-01-01 00:00:01" mkfs.${fsType} -F -L nixos -E offset=$offset $diskImage

    root="$PWD/root"
    mkdir -p $root

    # Copy arbitrary other files into the image
    # Semi-shamelessly copied from make-etc.sh. I (@copumpkin) shall factor this stuff out as part of
    # https://github.com/NixOS/nixpkgs/issues/23052.
    set -f
    sources_=(${concatStringsSep " " sources})
    targets_=(${concatStringsSep " " targets})
    set +f

    for ((i = 0; i < ''${#targets_[@]}; i++)); do
      source="''${sources_[$i]}"
      target="''${targets_[$i]}"

      if [[ "$source" =~ '*' ]]; then
        # If the source name contains '*', perform globbing.
        mkdir -p $root/$target
        for fn in $source; do
          rsync -a --no-o --no-g "$fn" $root/$target/
        done
      else
        mkdir -p $root/$(dirname $target)
        if ! [ -e $root/$target ]; then
          rsync -a --no-o --no-g $source $root/$target
        else
          echo "duplicate entry $target -> $source"
          exit 1
        fi
      fi
    done

    # TODO: Nix really likes to chown things it creates to its current user...
    fakeroot nixos-prepare-root $root ${channelSources} ${config.system.build.toplevel} closure

    echo "copying staging root to image..."
    # If we don't faketime, we can end up with timestamps other than 1 on the nix store, which
    # will confuse Nix in some situations (e.g., breaking image builds in the target image)
    # N.B: I use 0 here, which results in timestamp = 1 in the image. It's weird but see
    # https://github.com/lkl/linux/issues/393. Also, running under faketime makes `cptofs` super
    # noisy and it prints out that it can't find a bunch of files, and then works anyway. We'll
    # shut it up someday but trying to do a stderr filter through grep is running into some nasty
    # bug in some eval nonsense we have in runInLinuxVM and I'm sick of trying to fix it.
    faketime -f "1970-01-01 00:00:00" \
      cptofs ${optionalString partitioned "-P 1"} -t ${fsType} -i $diskImage $root/* /
  '';
in pkgs.vmTools.runInLinuxVM (
  pkgs.runCommand name
    { preVM = prepareImage;
      buildInputs = with pkgs; [ utillinux e2fsprogs ];
      exportReferencesGraph = [ "closure" metaClosure ];
      postVM = ''
        ${if format == "raw" then ''
          mv $diskImage $out/nixos.img
          diskImage=$out/nixos.img
        '' else ''
          ${pkgs.qemu}/bin/qemu-img convert -f raw -O ${format} $diskImage $out/nixos.${extensions.${format}}
          diskImage=$out/nixos.${extensions.${format}}
        ''}
        ${postVM}
      '';
      memSize = 1024;
    }
    ''
      ${if partitioned then ''
        . /sys/class/block/vda1/uevent
        mknod /dev/vda1 b $MAJOR $MINOR
        rootDisk=/dev/vda1
      '' else ''
        rootDisk=/dev/vda
      ''}

      # Some tools assume these exist
      ln -s vda /dev/xvda
      ln -s vda /dev/sda

      mountPoint=/mnt
      mkdir $mountPoint
      mount $rootDisk $mountPoint

      # Install a configuration.nix
      mkdir -p /mnt/etc/nixos
      ${optionalString (configFile != null) ''
        cp ${configFile} /mnt/etc/nixos/configuration.nix
      ''}

      mount --rbind /dev  $mountPoint/dev
      mount --rbind /proc $mountPoint/proc
      mount --rbind /sys  $mountPoint/sys

      # Set up core system link, GRUB, etc.
      NIXOS_INSTALL_BOOTLOADER=1 chroot $mountPoint /nix/var/nix/profiles/system/bin/switch-to-configuration boot

      # TODO: figure out if I should activate, but for now I won't
      # chroot $mountPoint /nix/var/nix/profiles/system/activate

      # The above scripts will generate a random machine-id and we don't want to bake a single ID into all our images
      rm -f $mountPoint/etc/machine-id

      umount -R /mnt

      # Make sure resize2fs works. Note that resize2fs has stricter criteria for resizing than a normal
      # mount, so the `-c 0` and `-i 0` don't affect it. Setting it to `now` doesn't produce deterministic
      # output, of course, but we can fix that when/if we start making images deterministic.
      ${optionalString (fsType == "ext4") ''
        tune2fs -T now -c 0 -i 0 $rootDisk
      ''}
    ''
)