cde7296e6a
.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).
280 lines
8.6 KiB
Bash
Executable File
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
|