ISO/.gitlab/ci/build-host.sh
David Runge cde7296e6a
ci: Consolidate build-host script
.gitlab/ci/build-host.sh:
Change all script-local variables to lower-case and make some of them overridable using environment variables (by
providing defaults).
Break down overly long commands by splitting them into a list of strings.
Use local variables where possible.
Change `main()` to use rsync instead of cp to copy the project to the build location more generically.
Change `main()` to use rsync instead of cp to copy the build artifacts on the VM from the project's build directory to
the output.
Remove all unnecessary `function` keywords for function declarations.
Replace the dependency on libisoburn's `xorriso` with libarchive's `bsdtar` and util-linux's `blkid` in
`prepare_boot()`.
Add `print_section_start()` and `print_section_end()` to reduce code duplication and error potential when printing lines
for gitlab's collapsible sections (https://docs.gitlab.com/ee/ci/jobs/#custom-collapsible-sections).
Document the script's behavior and expectations.
Document the understood environment variables and add links to documentation on understood units (in case of size
units).
2021-05-13 18:32:10 +02:00

280 lines
8.6 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# This script runs a build script in a QEMU VM using the latest Arch Linux installation medium.
# The build script is expected to create an './output' directory in the project's directory (when running in the VM) and
# place any build artifacts there.
# After the build script has finished this script will copy all artifacts to a (local) './output' directory and shutdown
# the VM.
#
# Dependencies:
# - coreutils
# - curl
# - libarchive
# - qemu-headless
# - util-linux
#
# Considered environment variables:
# ARCHISO_COW_SPACE_SIZE: The amount of RAM to allocate for the copy-on-write space used by archiso (defaults to 1g -
# see https://man.archlinux.org/man/tmpfs.5 for understood units)
# ARCHITECTURE: A string to set the CPU architecture (defaults to x86_64)
# BUILD_SCRIPT: A script that will be called on the host (defaults to ./build-inside-vm.sh)
# BUILD_SCRIPT_ARGS: The arguments to BUILD_SCRIPT (as a space delimited list)
# PACKAGE_LIST: A space delimited list of packages to install to the virtual machine
# PACMAN_MIRROR: The pacman mirror to use (defaults to "https://mirror.pkgbuild.com")
# QEMU_DISK_SIZE: A string given to fallocate to create a scratch disk to build in (defaults to 8G - see
# https://man.archlinux.org/man/fallocate.1 for understood units)
# QEMU_VM_MEMORY: The amount of RAM (in MiB) allocated for the QEMU virtual machine (defaults to 1024)
# QEMU_LOGIN_TIMEOUT: The maximum time (in seconds) to wait for the initial prompt in the VM to appear (defaults to 60)
# QEMU_PACKAGES_TIMEOUT: The maximum time (in seconds) to wait for output from pacman when installing packages (defaults
# to 120)
# QEMU_BUILD_TIMEOUT: The maximum time (in seconds) to wait for output from the build script (defaults to 1800)
# QEMU_COPY_ARTIFACTS_TIMEOUT: The maximum time (in seconds) to wait for output from the action of copying the build
# artifacts from the VM to a local directory (defaults to 60)
set -euo pipefail
readonly orig_pwd="${PWD}"
readonly output="${PWD}/output"
# variables with presets/ environmental overrides
arch="${ARCHITECTURE:-x86_64}"
script="${BUILD_SCRIPT:-./build-inside-vm.sh}"
script_args="${BUILD_SCRIPT_ARGS:-}"
mirror="${PACMAN_MIRROR:-https://mirror.pkgbuild.com}"
disk_size="${QEMU_DISK_SIZE:-8G}"
vm_memory="${QEMU_VM_MEMORY:-1024}"
login_timeout="${QEMU_LOGIN_TIMEOUT:-60}"
packages_timeout="${QEMU_PACKAGES_TIMEOUT:-120}"
build_timeout="${QEMU_BUILD_TIMEOUT:-1800}"
copy_artifacts_timeout="${QEMU_COPY_ARTIFACTS_TIMEOUT:-60}"
cow_space_size="${ARCHISO_COW_SPACE_SIZE:-1g}"
packages="${PACKAGE_LIST:-}"
# variables without presets/ environmental overrides
iso=""
iso_volume_id=""
tmpdir=""
tmpdir="$(mktemp --dry-run --directory --tmpdir="${PWD}/tmp")"
print_section_start() {
# gitlab collapsible sections start: https://docs.gitlab.com/ee/ci/jobs/#custom-collapsible-sections
local _section _title
_section="${1}"
_title="${2}"
printf "\e[0Ksection_start:%(%s)T:%s\r\e[0K%s\n" '-1' "${_section}" "${_title}"
}
print_section_end() {
# gitlab collapsible sections end: https://docs.gitlab.com/ee/ci/jobs/#custom-collapsible-sections
local _section
_section="${1}"
printf "\e[0Ksection_end:%(%s)T:%s\r\e[0K\n" '-1' "${_section}"
}
init() {
print_section_start "create_dirs" "Create required directories"
mkdir -p "${output}" "${tmpdir}"
cd "${tmpdir}"
print_section_end "create_dirs"
}
# Do some cleanup when the script exits
cleanup() {
print_section_start "cleanup" "Cleaning up"
rm -rf -- "${tmpdir}"
jobs -p | xargs --no-run-if-empty kill
print_section_end "cleanup"
}
trap cleanup EXIT
# Use local Arch iso or download the latest iso and extract the relevant files
prepare_boot() {
local _latest_iso _iso
local _isos=()
print_section_start "prepare_boot" "Prepare boot media"
# retrieve any local images and sort them
for _iso in "${orig_pwd}/"archlinux-*-"${arch}.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}-${arch}.iso" | \
head -n 1
)"
if [[ -z "${_latest_iso}" ]]; then
echo "Error: Could not find latest iso"
exit 1
fi
curl -fO "${mirror}/iso/latest/${_latest_iso}"
iso="${PWD}/${_latest_iso}"
fi
# Extract the kernel and initrd so that a custom kernel cmdline can be set:
# console=ttyS0, so that the kernel and systemd send output to the serial.
bsdtar -x -f "${iso}" -C . "arch/boot/${arch}"
iso_volume_id="$(blkid -s LABEL -o value "${iso}")"
print_section_end "prepare_boot"
}
start_qemu() {
local _kernel_params=(
"archisobasedir=arch"
"archisolabel=${iso_volume_id}"
"cow_spacesize=${cow_space_size}"
"ip=dhcp"
"net.ifnames=0"
"console=ttyS0"
"mirror=${mirror}"
)
print_section_start "start_qemu" "Start VM using 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 "${disk_size}" scratch-disk.img
{ qemu-system-x86_64 \
-machine accel=kvm:tcg \
-smp "$(nproc)" \
-m "${vm_memory}" \
-device virtio-net-pci,romfile=,netdev=net0 \
-netdev user,id=net0 \
-kernel "arch/boot/${arch}/vmlinuz-linux" \
-initrd "arch/boot/${arch}/initramfs-linux.img" \
-append "${_kernel_params[*]}" \
-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)
print_section_end "start_qemu"
}
# Wait for a specific string from qemu
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
send() {
echo -en "${1}" >guest.in
}
main() {
local _pacman_command=(
"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 ${packages}\n"
)
init
prepare_boot
start_qemu
print_section_start "init_build_environment" "Initialize build environment"
# Login
expect "archiso login:" "${login_timeout}"
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 "rsync -a --exclude tmp --exclude .git -- /mnt/project/ .\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 "# "
print_section_end "init_build_environment"
print_section_start "install_packages" "Install packages"
if [[ -n "${packages}" ]]; then
# Install required packages
send "${_pacman_command[*]}"
expect "# " "${packages_timeout}"
fi
print_section_end "install_packages"
## Start build and copy output to local disk
send "bash -x ${script} ${script_args}\n "
expect "# " "${build_timeout}"
print_section_start "move_artifacts" "Move artifacts to output directory"
send "rsync -av -- output /mnt/project/tmp/$(basename "${tmpdir}")/\n"
expect "# " "${copy_artifacts_timeout}"
mv -- output/* "${output}/"
print_section_end "move_artifacts"
print_section_start "shutdown" "Shutdown the VM"
# Shutdown the VM
send "systemctl poweroff -i\n"
wait
print_section_end "shutdown"
}
main