build-inside-vm.sh 7.88 KB
Newer Older
1
#!/bin/bash
2
# build-inside-vm.sh builds the images (cloud image, vagrant boxes)
Kristian Klausen's avatar
Kristian Klausen committed
3
4
5

# nounset: "Treat unset variables and parameters [...] as an error when performing parameter expansion."
# errexit: "Exit immediately if [...] command exits with a non-zero status."
6
set -o nounset -o errexit
7
readonly DISK_SIZE="20G"
8
readonly IMAGE="image.img"
9
# shellcheck disable=SC2016
10
readonly MIRROR='https://mirror.pkgbuild.com/$repo/os/$arch'
11

12
13
14
15
16
17
18
19
20
function init() {
  readonly ORIG_PWD="${PWD}"
  readonly OUTPUT="${PWD}/output"
  readonly TMPDIR="$(mktemp --dry-run --directory --tmpdir="${PWD}/tmp")"
  mkdir -p "${OUTPUT}" "${TMPDIR}"
  if [ -n "${SUDO_UID:-}" ]; then
    chown "${SUDO_UID}:${SUDO_GID}" "${OUTPUT}" "${TMPDIR}"
  fi
  cd "${TMPDIR}"
21

22
23
24
  readonly MOUNT="${PWD}/mount"
  mkdir "${MOUNT}"
}
25

Kristian Klausen's avatar
Kristian Klausen committed
26
# Do some cleanup when the script exits
27
function cleanup() {
Kristian Klausen's avatar
Kristian Klausen committed
28
  # We want all the commands to run, even if one of them fails.
29
30
31
32
33
34
35
36
37
38
39
40
41
42
  set +o errexit
  if [ -n "${LOOPDEV:-}" ]; then
    losetup -d "${LOOPDEV}"
  fi
  if [ -n "${MOUNT:-}" ] && mountpoint -q "${MOUNT}"; then
    # We do not want risking deleting ex: the package cache
    umount --recursive "${MOUNT}" || exit 1
  fi
  if [ -n "${TMPDIR:-}" ]; then
    rm -rf "${TMPDIR}"
  fi
}
trap cleanup EXIT

Kristian Klausen's avatar
Kristian Klausen committed
43
# Create the disk, partitions it, format the partition and mount the filesystem
44
45
46
47
48
49
50
51
function setup_disk() {
  truncate -s "${DISK_SIZE}" "${IMAGE}"
  sgdisk --clear \
    --new 1::+1M --typecode=1:ef02 \
    --new 2::-0 --typecode=2:8300 \
    "${IMAGE}"

  LOOPDEV=$(losetup --find --partscan --show "${IMAGE}")
52
  # Partscan is racy
53
  wait_until_settled "${LOOPDEV}"
54
55
56
57
  mkfs.btrfs "${LOOPDEV}p2"
  mount -o compress-force=zstd "${LOOPDEV}p2" "${MOUNT}"
}

Kristian Klausen's avatar
Kristian Klausen committed
58
# Install Arch Linux to the filesystem (bootstrap)
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function bootstrap() {
  cat <<EOF >pacman.conf
[options]
Architecture = auto

[core]
Include = mirrorlist

[extra]
Include = mirrorlist

[community]
Include = mirrorlist
EOF
  echo "Server = ${MIRROR}" >mirrorlist

  # We use the hosts package cache
76
  pacstrap -c -C pacman.conf -M "${MOUNT}" base linux grub openssh sudo polkit haveged btrfs-progs reflector
77
78
79
  cp mirrorlist "${MOUNT}/etc/pacman.d/"
}

Kristian Klausen's avatar
Kristian Klausen committed
80
# Misc "tweaks" done after bootstrapping
81
function postinstall() {
82
83
84
85
86
  # Remove machine-id see:
  # https://gitlab.archlinux.org/archlinux/arch-boxes/-/issues/25
  # https://gitlab.archlinux.org/archlinux/arch-boxes/-/issues/117
  rm "${MOUNT}/etc/machine-id"

Kristian Klausen's avatar
Kristian Klausen committed
87
88
89
90
91
92
93
  arch-chroot "${MOUNT}" /usr/bin/btrfs subvolume create /swap
  chattr +C "${MOUNT}/swap"
  chmod 0700 "${MOUNT}/swap"
  fallocate -l 512M "${MOUNT}/swap/swapfile"
  mkswap "${MOUNT}/swap/swapfile"
  echo -e "/swap/swapfile none swap defaults 0 0" >>"${MOUNT}/etc/fstab"

94
95
96
  echo "COMPRESSION=\"xz\"" >>"${MOUNT}/etc/mkinitcpio.conf"
  arch-chroot "${MOUNT}" /usr/bin/mkinitcpio -p linux

97
98
99
  sed -i -e 's/^#\(en_US.UTF-8\)/\1/' "${MOUNT}/etc/locale.gen"
  arch-chroot "${MOUNT}" /usr/bin/locale-gen
  arch-chroot "${MOUNT}" /usr/bin/systemd-firstboot --locale=en_US.UTF-8 --timezone=UTC --hostname=archlinux --keymap=us
100
101
102
  ln -sf /var/run/systemd/resolve/resolv.conf "${MOUNT}/etc/resolv.conf"
}

Kristian Klausen's avatar
Kristian Klausen committed
103
# Cleanup the image and trim it
104
105
106
107
108
109
110
111
function image_cleanup() {
  # Remove pacman key ring for re-initialization
  rm -rf "${MOUNT}/etc/pacman.d/gnupg/"

  sync -f "${MOUNT}/etc/os-release"
  fstrim --verbose "${MOUNT}"
}

112
113
114
115
116
117
118
119
120
121
122
# Helper function: wait until a given loop device has settled
# ${1} - loop device
function wait_until_settled() {
  udevadm settle
  blockdev --flushbufs --rereadpt ${1}
  until test -e "${1}p2"; do
      echo "${1}p2 doesn't exist yet..."
      sleep 1
  done
}

Kristian Klausen's avatar
Kristian Klausen committed
123
# Mount image helper (loop device + mount)
124
125
function mount_image() {
  LOOPDEV=$(losetup --find --partscan --show "${1:-${IMAGE}}")
126
  # Partscan is racy
127
  wait_until_settled ${LOOPDEV}
128
129
130
131
132
  mount -o compress-force=zstd "${LOOPDEV}p2" "${MOUNT}"
  # Setup bind mount to package cache
  mount --bind "/var/cache/pacman/pkg" "${MOUNT}/var/cache/pacman/pkg"
}

Kristian Klausen's avatar
Kristian Klausen committed
133
# Unmount image helper (umount + detach loop device)
134
135
136
137
138
139
function unmount_image() {
  umount --recursive "${MOUNT}"
  losetup -d "${LOOPDEV}"
  LOOPDEV=""
}

Kristian Klausen's avatar
Kristian Klausen committed
140
# Copy image and mount the copied image
141
142
143
144
145
function copy_and_mount_image() {
  cp -a "${IMAGE}" "${1}"
  mount_image "${1}"
}

Kristian Klausen's avatar
Kristian Klausen committed
146
# Compute SHA256, adjust owner to $SUDO_UID:$SUDO_UID and move to output/
147
148
149
150
151
152
153
154
function mv_to_output() {
  sha256sum "${1}" >"${1}.SHA256"
  if [ -n "${SUDO_UID:-}" ]; then
    chown "${SUDO_UID}:${SUDO_GID}" "${1}"{,.SHA256}
  fi
  mv "${1}"{,.SHA256} "${OUTPUT}/"
}

Kristian Klausen's avatar
Kristian Klausen committed
155
# Helper function: create a new image from the "base" image
Kristian Klausen's avatar
Kristian Klausen committed
156
157
158
# ${1} - final file
# ${2} - pre
# ${3} - post
159
function create_image() {
Kristian Klausen's avatar
Kristian Klausen committed
160
161
162
  local tmp_image="$(basename "$(mktemp -u)")"
  copy_and_mount_image "${tmp_image}"
  "${2}"
163
164
  image_cleanup
  unmount_image
Kristian Klausen's avatar
Kristian Klausen committed
165
166
  "${3}" "${tmp_image}" "${1}"
  mv_to_output "${1}"
167
168
169
170
}

function cloud_image() {
  arch-chroot "${MOUNT}" /bin/bash < <(cat "${ORIG_PWD}"/http/install-{cloud,common}.sh)
171
  arch-chroot "${MOUNT}" /usr/bin/pacman -S --noconfirm qemu-guest-agent cloud-init
172
173
174
175
  arch-chroot "${MOUNT}" /usr/bin/systemctl enable cloud-init-local.service cloud-init.service cloud-config.service cloud-final.service
}

function cloud_image_post() {
Kristian Klausen's avatar
Kristian Klausen committed
176
  qemu-img convert -c -f raw -O qcow2 "${1}" "${2}"
177
178
179
  rm "${1}"
}

180
181
function vagrant_qemu() {
  arch-chroot "${MOUNT}" /bin/bash < <(cat "${ORIG_PWD}"/http/install-{chroot,common}.sh)
182
  arch-chroot "${MOUNT}" /usr/bin/pacman -S --noconfirm netctl qemu-guest-agent
183
184
185
186
187
}

function vagrant_qemu_post() {
  # Create vagrant box
  cat <<EOF >Vagrantfile
188
189
190
191
192
193
Vagrant.configure("2") do |config|
  config.vm.provider :libvirt do |libvirt|
    libvirt.driver = "kvm"
  end
end
EOF
194
195
196
  local virtual_size
  virtual_size="$(grep -o "^[0-9]*" <<<"${DISK_SIZE}")"
  echo '{"format":"qcow2","provider":"libvirt","virtual_size":'"${virtual_size}"'}' >metadata.json
197
198
  qemu-img convert -f raw -O qcow2 "${1}" box.img
  rm "${1}"
199

200
201
  tar -czf "${2}" Vagrantfile metadata.json box.img
  rm Vagrantfile metadata.json box.img
202
203
204
205
}

function vagrant_virtualbox() {
  arch-chroot "${MOUNT}" /bin/bash < <(cat "${ORIG_PWD}"/http/install-{chroot,common}.sh)
206
  arch-chroot "${MOUNT}" /usr/bin/pacman -S --noconfirm netctl virtualbox-guest-utils-nox
207
208
209
210
  arch-chroot "${MOUNT}" /usr/bin/systemctl enable vboxservice
}

function vagrant_virtualbox_post() {
211
212
  # Create vagrant box
  # VirtualBox-6.1.12 src/VBox/NetworkServices/Dhcpd/Config.cpp line 276
213
214
  local mac_address
  mac_address="080027$(openssl rand -hex 3 | tr '[:lower:]' '[:upper:]')"
215
216
  cat <<EOF >Vagrantfile
Vagrant.configure("2") do |config|
217
  config.vm.base_mac = "${mac_address}"
218
219
220
221
222
223
224
225
226
end
EOF
  echo '{"provider":"virtualbox"}' >metadata.json
  qemu-img convert -f raw -O vmdk "${1}" "packer-virtualbox.vmdk"
  rm "${1}"

  cp "${ORIG_PWD}/box.ovf" .
  sed -e "s/MACHINE_UUID/$(uuidgen)/" \
    -e "s/DISK_UUID/$(uuidgen)/" \
227
    -e "s/DISK_CAPACITY/$(qemu-img info --output=json "packer-virtualbox.vmdk" | jq '."virtual-size"')/" \
228
    -e "s/UNIX/$(date +%s)/" \
229
    -e "s/MAC_ADDRESS/${mac_address}/" \
230
231
232
233
    -i box.ovf

  tar -czf "${2}" Vagrantfile metadata.json packer-virtualbox.vmdk box.ovf
  rm Vagrantfile metadata.json packer-virtualbox.vmdk box.ovf
234
235
}

236
# ${1} - Optional build version. If not set, will generate a default based on date.
237
238
239
240
241
function main() {
  if [ "$(id -u)" -ne 0 ]; then
    echo "root is required"
    exit 1
  fi
Kristian Klausen's avatar
Kristian Klausen committed
242
  init
243

244
245
246
247
248
249
250
  setup_disk
  bootstrap
  postinstall
  # We run it here as it is the easiest solution and we do not want anything to go wrong!
  arch-chroot "${MOUNT}" grub-install --target=i386-pc "${LOOPDEV}"
  unmount_image

251
252
  local build_version
  if [ -z "${1:-}" ]; then
253
    build_version="$(date +%Y%m%d).0"
254
255
256
257
    echo "WARNING: BUILD_VERSION wasn't set!"
    echo "Falling back to $build_version"
  else
    build_version="${1}"
258
  fi
Kristian Klausen's avatar
Kristian Klausen committed
259
260
  create_image "Arch-Linux-x86_64-cloudimg-${build_version}.qcow2" cloud_image cloud_image_post
  create_image "Arch-Linux-x86_64-libvirt-${build_version}.box" vagrant_qemu vagrant_qemu_post
261
  create_image "Arch-Linux-x86_64-virtualbox-${build_version}.box" vagrant_virtualbox vagrant_virtualbox_post
262
}
263
main "$@"