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
|
#!/usr/bin/env bash
# build-host.sh runs build-inside-vm.sh in a qemu VM running the latest Arch installer iso
#
# nounset: "Treat unset variables and parameters [...] as an error when performing parameter expansion."
# errexit: "Exit immediately if [...] command exits with a non-zero status."
set -o nounset -o errexit
readonly MIRROR="https://mirror.pkgbuild.com"
tmpdir=""
function init() {
readonly ORIG_PWD="${PWD}"
readonly OUTPUT="${PWD}/output"
tmpdir="$(mktemp --dry-run --directory --tmpdir="${PWD}/tmp")"
mkdir -p "${OUTPUT}" "${tmpdir}"
cd "${tmpdir}"
}
# Do some cleanup when the script exits
function cleanup() {
rm -rf -- "${tmpdir}"
jobs -p | xargs --no-run-if-empty kill
}
trap cleanup EXIT
# Use local Arch iso or download the latest iso and extract the relevant files
function prepare_boot() {
local iso
local isos=()
# retrieve any local images and sort them
for iso in "${ORIG_PWD}/"archlinux-*-x86_64.iso; do
if [[ -f "$iso" ]]; then
isos+=("${iso}")
fi
done
if (( ${#isos[@]} >= 1 )); then
ISO="$(printf '%s\n' "${isos[@]}" | sort -r | head -n1)"
printf "Using local iso: %s\n" "$ISO"
fi
if (( ${#isos[@]} < 1 )); then
LATEST_ISO="$(curl -fs "${MIRROR}/iso/latest/" | grep -Eo 'archlinux-[0-9]{4}\.[0-9]{2}\.[0-9]{2}-x86_64.iso' | head -n 1)"
if [[ -z "${LATEST_ISO}" ]]; then
echo "Error: Couldn't find latest iso'"
exit 1
fi
curl -fO "${MIRROR}/iso/latest/${LATEST_ISO}"
ISO="${PWD}/${LATEST_ISO}"
fi
# We need to extract the kernel and initrd so we can set a custom cmdline:
# console=ttyS0, so the kernel and systemd sends output to the serial.
xorriso -osirrox on -indev "${ISO}" -extract arch/boot/x86_64 .
ISO_VOLUME_ID="$(xorriso -indev "${ISO}" |& awk -F : '$1 ~ "Volume id" {print $2}' | tr -d "' ")"
}
function start_qemu() {
# Used to communicate with qemu
mkfifo guest.out guest.in
# We could use a sparse file but we want to fail early
fallocate -l 8G scratch-disk.img
{ qemu-system-x86_64 \
-machine accel=kvm:tcg \
-smp "$(nproc)" \
-m 4096 \
-device virtio-net-pci,romfile=,netdev=net0 -netdev user,id=net0 \
-kernel vmlinuz-linux \
-initrd initramfs-linux.img \
-append "archisobasedir=arch archisolabel=${ISO_VOLUME_ID} cow_spacesize=4G ip=dhcp net.ifnames=0 console=ttyS0 mirror=${MIRROR}" \
-drive file=scratch-disk.img,format=raw,if=virtio \
-drive file="${ISO}",format=raw,if=virtio,media=cdrom,read-only=on \
-virtfs "local,path=${ORIG_PWD},mount_tag=host,security_model=none" \
-monitor none \
-serial pipe:guest \
-nographic || kill "${$}"; } &
# We want to send the output to both stdout (fd1) and fd10 (used by the expect function)
exec 3>&1 10< <(tee /dev/fd/3 <guest.out)
}
# Wait for a specific string from qemu
function expect() {
local length="${#1}"
local i=0
local timeout="${2:-30}"
# We can't use ex: grep as we could end blocking forever, if the string isn't followed by a newline
while true; do
# read should never exit with a non-zero exit code,
# but it can happen if the fd is EOF or it times out
IFS= read -r -u 10 -n 1 -t "${timeout}" c
if [[ "${1:${i}:1}" = "${c}" ]]; then
i="$((i + 1))"
if [[ "${length}" -eq "${i}" ]]; then
break
fi
else
i=0
fi
done
}
# Send string to qemu
function send() {
echo -en "${1}" >guest.in
}
function main() {
init
prepare_boot
start_qemu
# Login
expect "archiso login:" 60
send "root\n"
expect "# "
# Switch to bash and shutdown on error
send "bash\n"
expect "# "
send "trap \"shutdown now\" ERR\n"
expect "# "
# Prepare environment
send "mkdir /mnt/project && mount -t 9p -o trans=virtio host /mnt/project -oversion=9p2000.L\n"
expect "# "
send "mkfs.ext4 /dev/vda && mkdir /mnt/scratch-disk/ && mount /dev/vda /mnt/scratch-disk && cd /mnt/scratch-disk\n"
expect "# "
send "cp -a -- /mnt/project/{.gitlab,archiso,configs,scripts} .\n"
expect "# "
send "mkdir pkg && mount --bind pkg /var/cache/pacman/pkg\n"
expect "# "
# Wait for pacman-init
send "until systemctl is-active pacman-init; do sleep 1; done\n"
expect "# "
# Explicitly lookup mirror address as we'd get random failures otherwise during pacman
send "curl -sSo /dev/null ${MIRROR}\n"
expect "# "
# Install required packages
send "pacman -Fy && pacman -Syu --ignore \$(pacman -Fq --machinereadable /usr/lib/modules/ | awk 'BEGIN { FS = \"\\\0\";ORS=\",\" }; { print \$2 } ' | sort -ut , | head -c -2) --noconfirm --needed qemu-headless jq dosfstools erofs-utils e2fsprogs libisoburn mtools squashfs-tools zsync\n"
expect "# " 120
## Start build and copy output to local disk
send "bash -x ./.gitlab/ci/build-inside-vm.sh ${PROFILE}\n "
expect "# " 2400 # mksquashfs can take a very long time
send "cp -r --preserve=mode,timestamps -- output /mnt/project/tmp/$(basename "${tmpdir}")/\n"
expect "# " 60
mv output/* "${OUTPUT}/"
# Shutdown the VM
send "systemctl poweroff -i\n"
wait
}
main
|