first commit

This commit is contained in:
2026-11-04 09:43:05 -07:00
commit c33736775b
25 changed files with 2056 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
#
# Purpose : coredesktop-dev check: lint and test the working copy, including
# scripts not committed yet, which CoreDesktop's make lint skips
check_usage() {
cat << EOF
Usage: coredesktop-dev check [--fast]
Runs make lint and make test in the working copy, then shellcheck on the
scripts git doesn't track yet. --fast runs only the shellcheck steps.
EOF
}
# Print each file named on stdin (NUL-separated) that starts with a bash or sh
# shebang, as the Makefile picks them, or is a .sh file
shell_scripts() {
local file
while IFS= read -r -d '' file; do
[ -f "$file" ] && [ ! -L "$file" ] || continue
if [[ "$file" == *.sh ]] || head -n 1 "$file" | grep -qE '^#!.*[/ ](ba)?sh'; then
echo "$file"
fi
done
}
cmd_check() {
local fast=0
case "${1:-}" in
-h | --help) check_usage; return 0 ;;
--fast) fast=1 ;;
"") ;;
*) check_usage >&2; return 1 ;;
esac
require_working_copy
cd "$WORKING_COPY" || return 1
local status=0 tracked untracked lint="ok" bats="skipped (--fast)"
mapfile -t tracked < <(git ls-files -z | shell_scripts)
mapfile -t untracked < <(git ls-files -z --others --exclude-standard | shell_scripts)
echo "== make lint"
make --no-print-directory lint || { lint="FAILED"; status=1; }
if (( ${#untracked[@]} > 0 )); then
echo "== shellcheck on untracked scripts: ${untracked[*]}"
shellcheck --severity=warning "${untracked[@]}" || { lint="FAILED"; status=1; }
fi
if (( fast == 0 )); then
echo "== make test"
if make --no-print-directory test; then
bats="ok ($(bats --count tests 2> /dev/null) tests)"
else
bats="FAILED"
status=1
fi
fi
echo
echo "shellcheck: $lint (${#tracked[@]} files, ${#untracked[@]} untracked) bats: $bats"
return $status
}
+67
View File
@@ -0,0 +1,67 @@
#!/bin/bash
#
# Purpose : Paths, settings and helpers shared by coredesktop-dev's scripts
# The deployed copy: the machine's running CoreDesktop, which update pulls
COREDESKTOP="$HOME/.local/share/coredesktop"
CONFIG_DIR="$HOME/.config/coredesktop-dev"
CONFIG="$CONFIG_DIR/config"
# First line after the shebang of the pre-push hook this project installs, so it
# never overwrites or removes a hook someone else wrote
HOOK_MARKER="# Installed by coredesktop-dev; remove with: coredesktop-dev hooks remove"
die() {
echo "$*" >&2
exit 1
}
confirm() {
local answer
read -rp "$1 [y/N] " answer
[[ "$answer" == [yY]* ]]
}
# Sets WORKING_COPY from the settings file (empty if there is none)
load_config() {
WORKING_COPY=""
# shellcheck source=/dev/null
[ -f "$CONFIG" ] && source "$CONFIG"
return 0
}
save_config() {
mkdir -p "$CONFIG_DIR"
printf 'WORKING_COPY=%q\n' "$1" > "$CONFIG"
}
# True if DIR is the deployed copy, however it's spelled
is_deployed_copy() {
[ -e "$1" ] && [ "$(realpath "$1")" = "$(realpath "$COREDESKTOP" 2> /dev/null)" ]
}
# True if DIR is the top of a git clone of CoreDesktop
is_coredesktop_clone() {
[ -d "$1" ] \
&& [ "$(git -C "$1" rev-parse --show-toplevel 2> /dev/null)" = "$(realpath "$1")" ] \
&& [ -f "$1/update" ] && [ -d "$1/updates.d" ]
}
# Load the settings and stop unless they point at a CoreDesktop clone
require_working_copy() {
load_config
[ -n "$WORKING_COPY" ] \
|| die "No working copy is set. Run $(sibling_dir)/install.sh, or: sibling add <coredesktop-dev url>"
is_coredesktop_clone "$WORKING_COPY" \
|| die "The working copy, $WORKING_COPY, isn't a clone of CoreDesktop (set in $CONFIG)."
}
in_flatpak() {
[ -f /.flatpak-info ] || [ -n "${FLATPAK_ID:-}" ]
}
# This project's folder
sibling_dir() {
cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd
}
+79
View File
@@ -0,0 +1,79 @@
#!/bin/bash
#
# Purpose : coredesktop-dev deployed: show the deployed copy of CoreDesktop
# (~/.local/share/coredesktop), or put back its tracked files so
# update can pull again
deployed_usage() {
cat << EOF
Usage: coredesktop-dev deployed [status|restore]
status Branch, commit, how far behind upstream, and local changes (default)
restore Undo local changes to tracked files, after asking. Untracked files
are listed and left alone.
EOF
}
cmd_deployed() {
case "${1:-status}" in
-h | --help) deployed_usage; return 0 ;;
status | restore) ;;
*) deployed_usage >&2; return 1 ;;
esac
git -C "$COREDESKTOP" rev-parse --git-dir &> /dev/null \
|| die "CoreDesktop isn't installed in $COREDESKTOP."
"deployed_${1:-status}"
}
deployed_status() {
local behind changes
echo "Deployed copy: $COREDESKTOP"
echo "Branch: $(git -C "$COREDESKTOP" rev-parse --abbrev-ref HEAD)"
echo "Commit: $(git -C "$COREDESKTOP" log -1 --format='%cd (%h) %s' --date=format:%Y.%m.%d)"
if GIT_TERMINAL_PROMPT=0 timeout 30 git -C "$COREDESKTOP" fetch --quiet &> /dev/null \
&& behind="$(git -C "$COREDESKTOP" rev-list --count 'HEAD..@{upstream}' 2> /dev/null)"; then
echo "Behind: $behind commits (update pulls them)"
else
echo "Behind: unknown (couldn't reach upstream)"
fi
changes="$(git -C "$COREDESKTOP" status --porcelain --untracked-files=no)"
if [ -z "$changes" ]; then
echo "Changes: none"
else
echo "Changes: these stop update from pulling (coredesktop-dev deployed restore):"
echo "$changes" | sed 's/^/ /'
fi
}
deployed_restore() {
local untracked
if [ -z "$(git -C "$COREDESKTOP" status --porcelain --untracked-files=no)" ]; then
echo "The deployed copy has no local changes."
return 0
fi
git -C "$COREDESKTOP" diff HEAD --stat
untracked="$(git -C "$COREDESKTOP" ls-files --others --exclude-standard)"
if [ -n "$untracked" ]; then
echo
echo "Untracked files, which are left alone:"
echo "$untracked" | sed 's/^/ /'
fi
echo
if ! confirm "Undo the changes above in $COREDESKTOP?"; then
echo "Nothing changed."
return 1
fi
git -C "$COREDESKTOP" checkout HEAD -- . || return 1
if [ -n "$(git -C "$COREDESKTOP" status --porcelain --untracked-files=no)" ]; then
echo "Restored, but some changes remain (new files added to git's index?):"
git -C "$COREDESKTOP" status --short --untracked-files=no
return 1
fi
echo "Restored. update can pull again."
}
+113
View File
@@ -0,0 +1,113 @@
#!/bin/bash
#
# Purpose : coredesktop-dev doctor: check this machine is set up to work on
# CoreDesktop. Read-only.
doctor_usage() {
cat << EOF
Usage: coredesktop-dev doctor
Checks the tools are installed, the working copy is set up, and the deployed
copy (~/.local/share/coredesktop) can still update. Exits 1 if anything needs
fixing.
EOF
}
PROBLEMS=0
ok() {
echo "ok $1"
}
problem() {
echo "PROBLEM $1"
shift
local line
for line in "$@"; do
echo " $line"
done
PROBLEMS=1
}
note() {
echo "note $1"
}
cmd_doctor() {
case "${1:-}" in
-h | --help) doctor_usage; return 0 ;;
"") ;;
*) doctor_usage >&2; return 1 ;;
esac
local tool missing=()
for tool in shellcheck bats tea qemu-system-x86_64 qemu-img xorriso; do
command -v "$tool" &> /dev/null || missing+=("$tool")
done
if (( ${#missing[@]} == 0 )); then
ok "shellcheck, bats, tea, QEMU and xorriso are installed"
else
problem "not installed: ${missing[*]}" "fix: bash $(sibling_dir)/update.sh"
fi
if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
ok "KVM is available for the smoketest's virtual machines"
else
problem "can't use /dev/kvm, so the smoketest can't run VMs" \
"fix: turn on virtualization (VT-x/AMD-V) in the firmware settings"
fi
load_config
if [ -z "$WORKING_COPY" ]; then
problem "no working copy is set" "fix: bash $(sibling_dir)/install.sh"
elif is_coredesktop_clone "$WORKING_COPY"; then
ok "working copy: $WORKING_COPY"
else
problem "the working copy, $WORKING_COPY, isn't a clone of CoreDesktop" \
"fix: bash $(sibling_dir)/install.sh"
fi
doctor_deployed
if [ -n "${COREDESKTOP_DEV_VIA_FLATPAK:-}" ] || in_flatpak; then
note "you ran this in a Flatpak, which has no pacman, systemctl or host /tmp;
coredesktop-dev runs itself on the host for you (flatpak-spawn --host)"
fi
if [ -n "$WORKING_COPY" ] && hook_is_ours "$(hook_path)"; then
ok "the pre-push hook is installed"
else
note "the pre-push hook isn't installed (coredesktop-dev hooks install)"
fi
return $PROBLEMS
}
doctor_deployed() {
if ! git -C "$COREDESKTOP" rev-parse --git-dir &> /dev/null; then
problem "CoreDesktop isn't installed in $COREDESKTOP"
return
fi
local changes
changes="$(git -C "$COREDESKTOP" status --porcelain --untracked-files=no)"
if [ -z "$changes" ]; then
ok "the deployed copy has no local changes"
else
problem "the deployed copy has local changes, so update can't pull:" \
"${changes//$'\n'/$'\n' }" \
"fix: coredesktop-dev deployed restore"
fi
if ! GIT_TERMINAL_PROMPT=0 timeout 30 git -C "$COREDESKTOP" fetch --quiet &> /dev/null; then
problem "couldn't reach the deployed copy's upstream to check it can update"
elif git -C "$COREDESKTOP" merge-base --is-ancestor HEAD '@{upstream}' 2> /dev/null; then
ok "the deployed copy can fast-forward to its upstream"
else
problem "the deployed copy has commits its upstream doesn't, so update's pull will fail" \
"see: git -C $COREDESKTOP log @{upstream}..HEAD"
fi
}
# hooks.sh has hook_path and hook_is_ours
source "$(dirname "${BASH_SOURCE[0]}")/hooks.sh"
+70
View File
@@ -0,0 +1,70 @@
#!/bin/bash
#
# Purpose : coredesktop-dev hooks: install or remove the working copy's pre-push
# hook, which runs coredesktop-dev check before each push
hooks_usage() {
cat << EOF
Usage: coredesktop-dev hooks [status|install|remove]
status Show whether the pre-push hook is installed (the default)
install Run coredesktop-dev check before each git push from the working copy
remove Remove that hook
git push --no-verify skips the hook for one push. A pre-push hook that
coredesktop-dev didn't install is never replaced or removed.
EOF
}
# Where git looks for the working copy's pre-push hook (core.hooksPath aware)
hook_path() {
local path
path="$(git -C "$WORKING_COPY" rev-parse --git-path hooks/pre-push)" || return 1
[[ "$path" == /* ]] || path="$WORKING_COPY/$path"
echo "$path"
}
hook_is_ours() {
[ -f "$1" ] && grep -qxF "$HOOK_MARKER" "$1"
}
cmd_hooks() {
local hook
case "${1:-status}" in
-h | --help) hooks_usage; return 0 ;;
status | install | remove) ;;
*) hooks_usage >&2; return 1 ;;
esac
require_working_copy
hook="$(hook_path)" || die "Couldn't find the working copy's git hooks folder."
case "${1:-status}" in
status)
if hook_is_ours "$hook"; then
echo "The pre-push hook is installed: $hook"
elif [ -e "$hook" ]; then
echo "$hook is someone else's hook; coredesktop-dev leaves it alone."
else
echo "The pre-push hook isn't installed. To install it: coredesktop-dev hooks install"
fi
;;
install)
if [ -e "$hook" ] && ! hook_is_ours "$hook"; then
die "$hook already exists and coredesktop-dev didn't write it, so it's left alone."
fi
mkdir -p "$(dirname "$hook")"
install -m 755 "$(sibling_dir)/hooks/pre-push" "$hook"
echo "Installed $hook: each git push now runs coredesktop-dev check first."
;;
remove)
if hook_is_ours "$hook"; then
rm "$hook"
echo "Removed $hook."
elif [ -e "$hook" ]; then
die "$hook isn't coredesktop-dev's, so it's left alone."
else
echo "The pre-push hook isn't installed."
fi
;;
esac
}
+353
View File
@@ -0,0 +1,353 @@
#!/bin/bash
#
# Purpose : coredesktop-dev smoketest: run CoreDesktop's smoketest scenarios
# (smoketest/ in the working copy) on QEMU virtual machines
source "$(dirname "${BASH_SOURCE[0]}")/vm.sh"
CLOUD_IMAGE_URL="${SMOKETEST_CLOUD_IMAGE_URL:-https://geo.mirror.pkgbuild.com/images/latest/Arch-Linux-x86_64-cloudimg.qcow2}"
DISK_SIZE=120G
RUNS="$SMOKETEST_STATE/runs"
smoketest_usage() {
cat << EOF
Usage: coredesktop-dev smoketest <command>
list Show the scenarios
run [options] [scenario...]
Run scenarios against the working copy's HEAD
--tier pr|all pr (the default): the quick ones; all: everything,
including a fresh install and upgrades from older
commits, which take hours
--ref REF Test REF instead of HEAD
--keep After a failure, leave its VM running to look
around in, and stop
images Show the VM images
images base (Re)build the base image: Arch + GNOME
images installed [REF] (Re)build the installed image: base + install.sh at
REF (default: where HEAD branched from upstream)
shell [base|installed] Boot a throwaway copy of an image and SSH in
clean [--all] Delete old runs' logs (--all: images and the
package cache too)
Scenarios are named by file (40-failing-migration) or number (40). Each run
keeps its logs in $RUNS.
Set SMOKETEST_VNC=1 to watch the VM's screen at VNC display 127.0.0.1:1.
EOF
}
cmd_smoketest() {
local sub="${1:-}"
shift || true
case "$sub" in
-h | --help | "") smoketest_usage; [ -n "$sub" ]; return ;;
list | run | images | shell | clean) ;;
*) smoketest_usage >&2; return 1 ;;
esac
require_working_copy
SCENARIOS="$WORKING_COPY/smoketest/scenarios"
[ -d "$SCENARIOS" ] || die "$WORKING_COPY has no smoketest/scenarios; is it an older CoreDesktop?"
"smoketest_$sub" "$@"
}
require_vm_tools() {
local tool
for tool in qemu-system-x86_64 qemu-img xorriso curl ssh scp ssh-keygen; do
command -v "$tool" &> /dev/null \
|| die "$tool isn't installed. Install coredesktop-dev's packages: bash $(sibling_dir)/update.sh"
done
[ -r "$KVM_DEVICE" ] && [ -w "$KVM_DEVICE" ] \
|| die "Can't use $KVM_DEVICE. Turn on virtualization (VT-x/AMD-V) in the firmware settings."
}
# scenario_info FILE: print IMAGE, TIER, DESCRIPTION and the VARIANTS (one per
# line after the first three)
scenario_info() {
(
source "$WORKING_COPY/smoketest/lib/scenario.sh"
source "$1"
printf '%s\n' "$IMAGE" "$TIER" "$DESCRIPTION"
(( ${#VARIANTS[@]} == 0 )) || printf '%s\n' "${VARIANTS[@]}"
)
}
smoketest_list() {
local file info
for file in "$SCENARIOS"/*.sh; do
mapfile -t info < <(scenario_info "$file")
printf '%-28s %-8s %-10s %s' "$(basename "$file" .sh)" "${info[1]}" "${info[0]}" "${info[2]}"
(( ${#info[@]} > 3 )) && printf ' (%d variants)' $(( ${#info[@]} - 3 ))
echo
done
}
# Set SELECTED to the scenario files to run: those named, or those in the tier
select_scenarios() {
local tier="$1" name file found info
shift
SELECTED=()
if (( $# > 0 )); then
for name in "$@"; do
found=""
for file in "$SCENARIOS"/*.sh; do
case "$(basename "$file" .sh)" in
"$name" | "$name"-*) found="$file"; break ;;
esac
done
[ -n "$found" ] || die "No scenario called $name (see: coredesktop-dev smoketest list)"
SELECTED+=("$found")
done
return
fi
for file in "$SCENARIOS"/*.sh; do
mapfile -t info < <(scenario_info "$file")
[[ "$tier" == all || "${info[1]}" == "$tier" ]] && SELECTED+=("$file")
done
return 0
}
smoketest_run() {
local tier=pr ref=HEAD keep=0 names=()
while (( $# > 0 )); do
case "$1" in
--tier) tier="${2:-}"; shift 2 || die "--tier needs pr or all" ;;
--ref) ref="${2:-}"; shift 2 || die "--ref needs a commit" ;;
--keep) keep=1; shift ;;
-*) die "Unknown option: $1" ;;
*) names+=("$1"); shift ;;
esac
done
[[ "$tier" == pr || "$tier" == all ]] || die "--tier is pr or all, not $tier"
require_vm_tools
local sha
sha="$(git -C "$WORKING_COPY" rev-parse --verify --quiet "$ref^{commit}")" || die "No such commit: $ref"
if [ "$ref" = HEAD ] && [ -n "$(git -C "$WORKING_COPY" status --porcelain --untracked-files=no)" ]; then
echo "Note: the working copy has uncommitted changes. Only HEAD is installed and"
echo "updated in the VM; the scenarios themselves are read from the working copy."
fi
select_scenarios "$tier" "${names[@]}"
local files=("${SELECTED[@]}")
(( ${#files[@]} > 0 )) || die "No scenarios to run."
# Build the images the chosen scenarios need, if they aren't there yet
local file info need_installed=0
for file in "${files[@]}"; do
mapfile -t info < <(scenario_info "$file")
[ "${info[0]}" = installed ] && need_installed=1
done
[ -f "$IMAGES/base.qcow2" ] || build_base_image
if (( need_installed )) && [ ! -f "$IMAGES/installed.qcow2" ]; then
build_installed_image "$(default_installed_ref)"
fi
RUN="$RUNS/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$RUN/shared"
echo "Testing $(git -C "$WORKING_COPY" log -1 --format='%h %s' "$sha"); logs in $RUN"
local results=() failed=0 variants variant status
for file in "${files[@]}"; do
mapfile -t info < <(scenario_info "$file")
variants=("${info[@]:3}")
(( ${#variants[@]} > 0 )) || variants=("")
for variant in "${variants[@]}"; do
run_scenario "$file" "$variant" "$sha" "$keep"
status=$?
local label
label="$(basename "$file" .sh)${variant:+ @ $variant}"
case $status in
0) results+=("PASS $label") ;;
3) results+=("SKIP $label") ;;
*) results+=("FAIL $label"); failed=1 ;;
esac
if (( failed && keep )); then
break 2
fi
done
done
echo
printf '%s\n' "${results[@]}" | tee "$RUN/summary"
echo "Logs: $RUN"
return $failed
}
# run_scenario FILE VARIANT SHA KEEP: boot a throwaway copy of the scenario's
# image and run it. Returns 0 for pass, 3 for skip, anything else for fail.
run_scenario() {
local file="$1" variant="$2" sha="$3" keep="$4"
local name dir
name="$(basename "$file" .sh)${variant:+@$variant}"
dir="$RUN/$name"
mkdir -p "$dir"
echo
echo "=== $name"
(
export SMOKETEST_RESULTS="$dir" SMOKETEST_SHARED="$RUN/shared" SMOKETEST_REF="$sha" VARIANT="$variant"
IMAGE_REF="$(cat "$IMAGES/installed.ref" 2> /dev/null)"
export IMAGE_REF
source "$WORKING_COPY/smoketest/lib/scenario.sh"
source "$file"
qemu-img create -q -f qcow2 -b "$IMAGES/$IMAGE.qcow2" -F qcow2 "$dir/disk.qcow2" \
|| die "Couldn't make the VM's disk."
vm_start "$dir" "$dir/disk.qcow2"
trap 'finish_scenario $? '"$keep" EXIT
vm_wait_up
smoketest_prepare
set -e
scenario
) 2>&1 | tee "$dir/scenario.log"
return "${PIPESTATUS[0]}"
}
# Runs as the scenario's subshell exits: stop the VM and delete its disk,
# unless it failed and --keep was given
finish_scenario() {
local status="$1" keep="$2"
if (( status != 0 && status != SKIP_STATUS && keep )); then
echo
echo "The VM is still running. To look around: $(vm_ssh_command)"
echo "To stop it: kill $(vm_pid) Its disk: $VM_DIR/disk.qcow2"
return
fi
vm_kill
rm -f "$VM_DIR/disk.qcow2"
}
# Where the installed image is built by default: the commit HEAD branched
# from upstream, so updating it to HEAD exercises the branch's changes
default_installed_ref() {
git -C "$WORKING_COPY" merge-base HEAD '@{upstream}' 2> /dev/null \
|| git -C "$WORKING_COPY" merge-base HEAD origin/main 2> /dev/null \
|| git -C "$WORKING_COPY" rev-parse HEAD
}
download_cloud_image() {
local image="$IMAGES/cloudimg.qcow2" expected actual
[ -f "$image" ] && return 0
mkdir -p "$IMAGES"
echo "Downloading Arch's cloud image..."
curl -fL --progress-bar -o "$image.part" "$CLOUD_IMAGE_URL" || die "Couldn't download $CLOUD_IMAGE_URL"
expected="$(curl -fsSL "$CLOUD_IMAGE_URL.SHA256" | awk '{print $1}')" \
|| die "Couldn't download $CLOUD_IMAGE_URL.SHA256"
actual="$(sha256sum "$image.part" | awk '{print $1}')"
[ -n "$expected" ] && [ "$expected" = "$actual" ] \
|| die "The cloud image's checksum doesn't match; try again."
mv "$image.part" "$image"
}
build_base_image() {
require_vm_tools
download_cloud_image
local build="$SMOKETEST_STATE/build-base"
rm -rf "$build"
mkdir -p "$build/seed"
echo "Building the base image (Arch + GNOME); log: $build/build.log"
cp --reflink=auto "$IMAGES/cloudimg.qcow2" "$build/disk.qcow2"
qemu-img resize -q "$build/disk.qcow2" "$DISK_SIZE"
ensure_ssh_key
sed "s|@SSH_KEY@|$(cat "$SSH_KEY.pub")|" "$(sibling_dir)/vm/user-data" > "$build/seed/user-data"
printf 'instance-id: coredesktop-smoketest\nlocal-hostname: coredesktop-smoketest\n' \
> "$build/seed/meta-data"
xorriso -as mkisofs -quiet -output "$build/seed.iso" -volid cidata -joliet -rock \
"$build/seed/user-data" "$build/seed/meta-data" || die "Couldn't make the cloud-init disk."
(
vm_start "$build" "$build/disk.qcow2" -cdrom "$build/seed.iso"
trap vm_kill EXIT
wait_for_ssh
vm_ssh 'cloud-init status --wait' < /dev/null > /dev/null || true
vm_put "$(sibling_dir)/vm/provision.sh" /tmp/provision.sh || die "Couldn't copy provision.sh to the VM."
vm_exec 'sudo bash /tmp/provision.sh' || die "Provisioning failed; see $build/build.log"
vm_stop
) 2>&1 | tee "$build/build.log"
(( PIPESTATUS[0] == 0 )) || die "Building the base image failed; see $build/build.log"
mv "$build/disk.qcow2" "$IMAGES/base.qcow2"
# Built on the old base image
rm -f "$IMAGES/installed.qcow2" "$IMAGES/installed.ref"
rm -rf "$build"
echo "Built $IMAGES/base.qcow2"
}
# build_installed_image REF: the base image after install.sh at REF
build_installed_image() {
local ref sha build="$SMOKETEST_STATE/build-installed"
ref="$1"
sha="$(git -C "$WORKING_COPY" rev-parse --verify --quiet "$ref^{commit}")" || die "No such commit: $ref"
require_vm_tools
[ -f "$IMAGES/base.qcow2" ] || build_base_image
rm -rf "$build"
mkdir -p "$build"
echo "Building the installed image at $(git -C "$WORKING_COPY" log -1 --format='%h %s' "$sha");" \
"this takes an hour or more. Log: $build/build.log"
qemu-img create -q -f qcow2 -b "$IMAGES/base.qcow2" -F qcow2 "$build/disk.qcow2"
(
export SMOKETEST_RESULTS="$build" SMOKETEST_SHARED="$build" SMOKETEST_REF="$sha"
IMAGE=base
source "$WORKING_COPY/smoketest/lib/scenario.sh"
vm_start "$build" "$build/disk.qcow2"
trap vm_kill EXIT
vm_wait_up
smoketest_install_image "$sha"
# The README says to reboot after installing
vm_reboot
vm_stop
) 2>&1 | tee "$build/build.log"
(( PIPESTATUS[0] == 0 )) || die "Building the installed image failed; logs in $build"
mv "$build/disk.qcow2" "$IMAGES/installed.qcow2"
echo "$sha" > "$IMAGES/installed.ref"
echo "Built $IMAGES/installed.qcow2"
}
smoketest_images() {
case "${1:-}" in
"")
local image
for image in base installed; do
if [ -f "$IMAGES/$image.qcow2" ]; then
printf '%-10s built %s' "$image" "$(date -r "$IMAGES/$image.qcow2" '+%Y-%m-%d %H:%M')"
[ "$image" = installed ] \
&& printf ', at %s' "$(git -C "$WORKING_COPY" log -1 --format='%h %s' "$(cat "$IMAGES/installed.ref")" 2> /dev/null)"
echo
else
printf '%-10s not built\n' "$image"
fi
done
;;
base) build_base_image ;;
installed) build_installed_image "${2:-$(default_installed_ref)}" ;;
*) smoketest_usage >&2; return 1 ;;
esac
}
smoketest_shell() {
local image="${1:-installed}" dir
[[ "$image" == base || "$image" == installed ]] || die "The image is base or installed, not $image."
require_vm_tools
[ -f "$IMAGES/$image.qcow2" ] || die "The $image image isn't built: coredesktop-dev smoketest images $image"
dir="$SMOKETEST_STATE/shell"
rm -rf "$dir"
mkdir -p "$dir"
qemu-img create -q -f qcow2 -b "$IMAGES/$image.qcow2" -F qcow2 "$dir/disk.qcow2"
vm_start "$dir" "$dir/disk.qcow2"
trap 'vm_kill; rm -rf "$dir"' EXIT
echo "Starting a throwaway copy of the $image image..."
vm_wait_up
vm_ssh -t
}
smoketest_clean() {
case "${1:-}" in
"") rm -rf "$RUNS" && echo "Deleted $RUNS" ;;
--all) rm -rf "$SMOKETEST_STATE" && echo "Deleted $SMOKETEST_STATE" ;;
*) smoketest_usage >&2; return 1 ;;
esac
}
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
#
# Purpose : coredesktop-dev try: run the working copy's code as if it were the
# deployed CoreDesktop, in a throwaway home with its own session
try_usage() {
cat << EOF
Usage: coredesktop-dev try [--keep] [command...]
Runs command (or a shell) with a throwaway HOME where
~/.local/share/coredesktop is the working copy, and update, configure, version
etc. in ~/.local/bin are the working copy's. It gets its own XDG_RUNTIME_DIR
and D-Bus session, and no display, so it can't touch your desktop session.
sudo, yay, pacman and systemctl still change this machine.
--keep Keep the throwaway folder afterwards, and print where it is
Example: coredesktop-dev try configure --list
EOF
}
TRY_COMMANDS=(update refresh sibling version configure audit)
cmd_try() {
local keep=0
while [ $# -gt 0 ]; do
case "$1" in
-h | --help) try_usage; return 0 ;;
--keep) keep=1; shift ;;
--) shift; break ;;
*) break ;;
esac
done
require_working_copy
local tmp home runtime cmd status
tmp="$(mktemp -d "${TMPDIR:-/tmp}/coredesktop-dev-try.XXXXXX")" || return 1
home="$tmp/home"
runtime="$tmp/runtime"
mkdir -p "$home/.local/share" "$home/.local/bin" "$home/.local/state" \
"$home/.config" "$home/.cache"
mkdir -m 700 "$runtime"
ln -s "$WORKING_COPY" "$home/.local/share/coredesktop"
for cmd in "${TRY_COMMANDS[@]}"; do
ln -s "$WORKING_COPY/$cmd" "$home/.local/bin/$cmd"
done
echo "coredesktop-dev try: HOME is $home, with the working copy as CoreDesktop." >&2
echo "Careful: sudo, yay, pacman and systemctl still change this machine." >&2
(( $# > 0 )) || set -- bash
env -u DISPLAY -u WAYLAND_DISPLAY -u DBUS_SESSION_BUS_ADDRESS -u SSH_AUTH_SOCK \
HOME="$home" \
XDG_CONFIG_HOME="$home/.config" \
XDG_DATA_HOME="$home/.local/share" \
XDG_STATE_HOME="$home/.local/state" \
XDG_CACHE_HOME="$home/.cache" \
XDG_RUNTIME_DIR="$runtime" \
PATH="$home/.local/bin:$PATH" \
dbus-run-session -- "$@"
status=$?
if (( keep )); then
echo "Kept $tmp" >&2
else
rm -rf "$tmp"
fi
return $status
}
+182
View File
@@ -0,0 +1,182 @@
#!/bin/bash
#
# Purpose : Start, reach and stop the smoketest's QEMU virtual machines. Defines
# what CoreDesktop's smoketest/lib/scenario.sh expects of a driver:
# vm_exec, vm_put, vm_reboot, VM_GIT_SSH and VM_GIT_URL.
SMOKETEST_STATE="${XDG_CACHE_HOME:-$HOME/.cache}/coredesktop-dev/smoketest"
IMAGES="$SMOKETEST_STATE/images"
# Shared with every VM as its /var/cache/pacman/pkg, so packages download once
PKG_CACHE="$SMOKETEST_STATE/pkg"
SSH_KEY="$SMOKETEST_STATE/id_ed25519"
KVM_DEVICE="${KVM_DEVICE:-/dev/kvm}"
VM_USER=tester
VM_MEMORY="${SMOKETEST_MEMORY:-8G}"
VM_CPUS="${SMOKETEST_CPUS:-4}"
# Longest a single command in the VM may run (an install takes an hour or more)
VM_EXEC_TIMEOUT="${SMOKETEST_EXEC_TIMEOUT:-4h}"
# Seconds to wait for the VM to answer on SSH, and for GNOME to start
VM_SSH_TIMEOUT="${SMOKETEST_SSH_TIMEOUT:-900}"
VM_DESKTOP_TIMEOUT="${SMOKETEST_DESKTOP_TIMEOUT:-300}"
SSH_OPTS=(
-i "$SSH_KEY"
-o IdentitiesOnly=yes
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o LogLevel=ERROR
-o BatchMode=yes
-o ConnectTimeout=10
-o ServerAliveInterval=15
-o ServerAliveCountMax=8
)
# Commands in the VM run as they would in a GNOME Terminal: with the GNOME
# session's environment, in an interactive bash that has read ~/.bashrc
VM_SESSION_ENV='eval "$(systemctl --user show-environment 2> /dev/null | sed "s/^/export /")"
export TERM="${TERM:-dumb}"'
ensure_ssh_key() {
mkdir -p "$SMOKETEST_STATE"
[ -f "$SSH_KEY" ] || ssh-keygen -q -t ed25519 -N '' -C coredesktop-smoketest -f "$SSH_KEY"
}
free_port() {
local port i
for i in $(seq 50); do
port=$((20000 + RANDOM % 20000))
[ -z "$(ss -Hltn "( sport = :$port )" 2> /dev/null)" ] && { echo "$port"; return 0; }
done
return 1
}
# vm_start DIR DISK [QEMU ARGS...]: boot DISK in the background. DIR gets the
# pid file and the serial console's log.
vm_start() {
VM_DIR="$1"
local disk="$2"
shift 2
local display=(-display none)
[ -n "${SMOKETEST_VNC:-}" ] && display+=(-vnc "127.0.0.1:$SMOKETEST_VNC")
ensure_ssh_key
mkdir -p "$VM_DIR" "$PKG_CACHE"
VM_PORT="$(free_port)" || die "Couldn't find a free port for the VM's SSH."
qemu-system-x86_64 \
-name coredesktop-smoketest \
-machine q35,accel=kvm \
-cpu host \
-smp "$VM_CPUS" \
-m "$VM_MEMORY" \
-drive "file=$disk,if=virtio,format=qcow2,discard=unmap" \
-nic "user,model=virtio-net-pci,hostfwd=tcp:127.0.0.1:$VM_PORT-:22" \
-virtfs "local,path=$PKG_CACHE,mount_tag=pkgcache,security_model=mapped-xattr" \
-vga virtio \
"${display[@]}" \
-serial "file:$VM_DIR/serial.log" \
-pidfile "$VM_DIR/qemu.pid" \
-daemonize \
"$@" || die "QEMU didn't start."
VM_GIT_SSH="ssh$(printf ' %q' "${SSH_OPTS[@]}")"
VM_GIT_URL="ssh://$VM_USER@127.0.0.1:$VM_PORT"
export VM_GIT_SSH VM_GIT_URL
}
vm_pid() {
cat "$VM_DIR/qemu.pid" 2> /dev/null
}
vm_running() {
local pid
pid="$(vm_pid)" && [ -n "$pid" ] && kill -0 "$pid" 2> /dev/null
}
vm_ssh() {
ssh "${SSH_OPTS[@]}" -p "$VM_PORT" "$VM_USER@127.0.0.1" "$@"
}
# The command to SSH in by hand
vm_ssh_command() {
echo "ssh -i $SSH_KEY -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p $VM_PORT $VM_USER@127.0.0.1"
}
wait_for_ssh() {
local deadline=$((SECONDS + VM_SSH_TIMEOUT))
until vm_ssh true < /dev/null 2> /dev/null; do
vm_running || die "The VM stopped. Its console log: $VM_DIR/serial.log"
(( SECONDS < deadline )) || die "The VM didn't answer on SSH. Its console log: $VM_DIR/serial.log"
sleep 5
done
}
# Wait for the automatic GNOME login. Not fatal: the health checks catch a
# desktop that didn't start.
wait_for_desktop() {
local deadline=$((SECONDS + VM_DESKTOP_TIMEOUT))
until vm_ssh "pgrep -u $VM_USER -x gnome-shell" < /dev/null &> /dev/null; do
if (( SECONDS >= deadline )); then
echo "Warning: GNOME didn't start in the VM within ${VM_DESKTOP_TIMEOUT}s."
return 0
fi
sleep 5
done
}
vm_wait_up() {
wait_for_ssh
wait_for_desktop
}
# vm_exec SCRIPT: run SCRIPT with bash in the VM, as tester, in the GNOME
# session's environment. Interactive bash's complaints about having no
# terminal are dropped.
vm_exec() {
timeout "$VM_EXEC_TIMEOUT" ssh "${SSH_OPTS[@]}" -p "$VM_PORT" -T "$VM_USER@127.0.0.1" \
"bash -ic $(printf '%q' "$VM_SESSION_ENV
$1")" < /dev/null \
2> >(grep -v -e '^bash: cannot set terminal process group' -e '^bash: no job control in this shell' >&2)
}
# vm_put LOCAL REMOTE
vm_put() {
scp -q "${SSH_OPTS[@]}" -P "$VM_PORT" "$1" "$VM_USER@127.0.0.1:$2"
}
vm_boot_id() {
vm_ssh 'cat /proc/sys/kernel/random/boot_id' < /dev/null 2> /dev/null
}
vm_reboot() {
local before deadline=$((SECONDS + VM_SSH_TIMEOUT))
echo "== reboot"
before="$(vm_boot_id)"
vm_ssh 'sudo systemctl reboot' < /dev/null &> /dev/null || true
until [ "$(vm_boot_id)" != "$before" ] && [ -n "$(vm_boot_id)" ]; do
vm_running || die "The VM stopped while rebooting. Its console log: $VM_DIR/serial.log"
(( SECONDS < deadline )) || die "The VM didn't come back after rebooting. Its console log: $VM_DIR/serial.log"
sleep 5
done
wait_for_desktop
}
# Shut the VM down cleanly (so its disk can be kept), killing it if it hangs
vm_stop() {
vm_running || return 0
vm_ssh 'sudo systemctl poweroff' < /dev/null &> /dev/null || true
local deadline=$((SECONDS + 120))
while vm_running && (( SECONDS < deadline )); do
sleep 2
done
vm_kill
}
vm_kill() {
local pid
pid="$(vm_pid)" || return 0
[ -n "$pid" ] && kill "$pid" 2> /dev/null
rm -f "$VM_DIR/qemu.pid"
return 0
}