feat: add reproducible dev environment setup script
A freshly-cloned repo had no path to a working environment: building the dev image, wiring git hooks, and installing VM tooling were all manual, undocumented steps. This makes onboarding reproducible across machines. scripts/setup-dev-environment.sh is idempotent and safe to re-run: - Verifies OS, ensures Docker + docker group, starts daemon if needed - Builds the knel-football-dev image - Configures git hooks (SDLC enforcement) - Creates output/ and tmp/ working dirs - Installs VM tooling (libvirt/qemu/ovmf/swtpm) by default, since the ultimate artifact is an ISO that must be booted in a VM with serial capture to verify it works. --skip-vm opts out for build-only envs. - Runs a smoke test (lint + unit tests) and prints a status summary README "First-Time Setup" now points at this script instead of the bare setup-githooks.sh step. 💘 Generated with Crush Assisted-by: GLM-4.7 via Crush <crush@charm.land>
This commit is contained in:
Executable
+392
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# KNEL-Football Secure OS - Development Environment Setup
|
||||
#
|
||||
# Reproducibly prepares a fresh Debian/Ubuntu host for hacking on this project.
|
||||
#
|
||||
# The core development loop (build / test / lint / iso) runs entirely inside
|
||||
# Docker, so the ONLY hard host requirement is Docker itself plus the built
|
||||
# dev image. This script installs/verifies that, builds the image, wires up
|
||||
# git hooks, and runs a smoke test.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/setup-dev-environment.sh # full setup (core + VM tooling)
|
||||
# ./scripts/setup-dev-environment.sh --skip-vm # core only, no libvirt/qemu
|
||||
# ./scripts/setup-dev-environment.sh --smoke-test-only # skip installs, just verify
|
||||
# ./scripts/setup-dev-environment.sh --help
|
||||
#
|
||||
# The ultimate artifact is an ISO that must be booted in a VM (libvirt/qEMU)
|
||||
# with serial port capture to verify it actually works, so VM tooling is
|
||||
# installed by default. --skip-vm disables it for environments that only need
|
||||
# the build/test loop.
|
||||
#
|
||||
# Safe to re-run; every step is idempotent.
|
||||
#
|
||||
# Copyright (c) 2026 Known Element Enterprises LLC
|
||||
# License: GNU Affero General Public License v3.0 only
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Constants & paths
|
||||
# ---------------------------------------------------------------------------
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
readonly SCRIPT_DIR REPO_ROOT
|
||||
|
||||
readonly RUN_SH="${REPO_ROOT}/run.sh"
|
||||
readonly OUTPUT_DIR="${REPO_ROOT}/output"
|
||||
readonly TMP_DIR="${REPO_ROOT}/tmp"
|
||||
readonly DOCKER_IMAGE="knel-football-dev:latest"
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logging
|
||||
# ---------------------------------------------------------------------------
|
||||
log_info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
|
||||
log_step() { echo -e "\n${BLUE}=== $* ===${NC}"; }
|
||||
log_ok() { echo -e "${GREEN}OK${NC}"; }
|
||||
|
||||
# Track overall result so we can fail at the end with a useful summary.
|
||||
FAILED=0
|
||||
note_failure() { FAILED=1; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Option parsing
|
||||
# ---------------------------------------------------------------------------
|
||||
WITH_VM=1
|
||||
SMOKE_ONLY=0
|
||||
SHOW_HELP=0
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--skip-vm) WITH_VM=0 ;;
|
||||
--with-vm) WITH_VM=1 ;;
|
||||
--smoke-test-only) SMOKE_ONLY=1 ;;
|
||||
-h|--help) SHOW_HELP=1 ;;
|
||||
*)
|
||||
log_error "Unknown option: $arg"
|
||||
echo "Run with --help for usage."
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ "$SHOW_HELP" -eq 1 ]]; then
|
||||
sed -n '3,20p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Print the OS "family" (debian|ubuntu|other) and id, or "other".
|
||||
detect_os() {
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
. /etc/os-release
|
||||
case "${ID:-}" in
|
||||
debian) echo "debian" ;;
|
||||
ubuntu|linuxmint) echo "ubuntu" ;;
|
||||
*) echo "other (${ID:-unknown})" ;;
|
||||
esac
|
||||
else
|
||||
echo "other"
|
||||
fi
|
||||
}
|
||||
|
||||
# True if a command exists.
|
||||
have() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# Run a command with sudo if we are not root, else directly.
|
||||
as_root() {
|
||||
if [[ "$(id -u)" -eq 0 ]]; then
|
||||
"$@"
|
||||
elif have sudo; then
|
||||
sudo "$@"
|
||||
else
|
||||
log_error "This step requires root privileges but 'sudo' is unavailable."
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Install apt packages (Debian/Ubuntu only). Arguments: package names.
|
||||
# Skips packages already installed; no-ops if the list is empty after filtering.
|
||||
apt_install() {
|
||||
local to_install=()
|
||||
local pkg
|
||||
for pkg in "$@"; do
|
||||
if ! dpkg -s "$pkg" >/dev/null 2>&1; then
|
||||
to_install+=("$pkg")
|
||||
fi
|
||||
done
|
||||
if [[ ${#to_install[@]} -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
log_info "Installing: ${to_install[*]}"
|
||||
as_root apt-get update -qq
|
||||
as_root apt-get install -y --no-install-recommends "${to_install[@]}"
|
||||
}
|
||||
|
||||
# Ensure the current user is in a group (idempotent).
|
||||
ensure_group() {
|
||||
local group="$1"
|
||||
if ! getent group "$group" >/dev/null 2>&1; then
|
||||
as_root groupadd "$group"
|
||||
fi
|
||||
if ! id -nG | tr ' ' '\n' | grep -qx "$group"; then
|
||||
log_info "Adding user '$USER' to group '$group'..."
|
||||
as_root usermod -aG "$group" "$USER"
|
||||
log_warn "You were added to the '$group' group. Log out and back in (or reboot) for it to take effect."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase: OS check
|
||||
# ---------------------------------------------------------------------------
|
||||
phase_check_os() {
|
||||
log_step "Checking operating system"
|
||||
local os
|
||||
os="$(detect_os)"
|
||||
log_info "Detected: $os"
|
||||
case "$os" in
|
||||
debian|ubuntu)
|
||||
log_ok
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
log_warn "This project targets Debian/Ubuntu. Docker builds use Debian 13 (trixie) with apt-pinned versions."
|
||||
log_warn "Continuing anyway, but Docker image builds may fail on other distros."
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase: Docker (hard requirement)
|
||||
# ---------------------------------------------------------------------------
|
||||
phase_docker() {
|
||||
log_step "Verifying Docker"
|
||||
|
||||
if have docker; then
|
||||
log_info "Docker CLI present: $(docker --version)"
|
||||
else
|
||||
log_info "Docker not found; installing via apt (docker.io)..."
|
||||
apt_install docker.io
|
||||
# Some installs name the socket-activating package; ensure a daemon exists.
|
||||
apt_install docker-ce 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Make sure the user can talk to the daemon without sudo.
|
||||
ensure_group docker
|
||||
|
||||
# Verify daemon connectivity. If it fails, try to start it.
|
||||
if ! docker info >/dev/null 2>&1; then
|
||||
log_warn "Cannot reach Docker daemon. Attempting to start it..."
|
||||
if have systemctl; then
|
||||
as_root systemctl start docker || true
|
||||
as_root systemctl enable docker || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if docker info >/dev/null 2>&1; then
|
||||
log_info "Docker daemon reachable."
|
||||
if ! docker ps >/dev/null 2>&1; then
|
||||
log_warn "'docker ps' failed. If you were just added to the 'docker' group, log out and back in, then re-run this script."
|
||||
note_failure
|
||||
else
|
||||
log_ok
|
||||
fi
|
||||
else
|
||||
log_error "Docker daemon is not running and could not be started."
|
||||
log_error "Start it manually, then re-run: sudo systemctl start docker"
|
||||
note_failure
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase: Build the dev Docker image
|
||||
# ---------------------------------------------------------------------------
|
||||
phase_build_image() {
|
||||
log_step "Building dev Docker image (${DOCKER_IMAGE})"
|
||||
|
||||
if docker image inspect "$DOCKER_IMAGE" >/dev/null 2>&1; then
|
||||
log_info "Image already exists. Use './run.sh build' to force a rebuild."
|
||||
log_ok
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Building image (this downloads Debian packages and may take several minutes)..."
|
||||
if "${RUN_SH}" build; then
|
||||
log_ok
|
||||
else
|
||||
log_error "Docker image build failed."
|
||||
log_error "If apt version pins fail, the pinned versions in Dockerfile may need updating."
|
||||
note_failure
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase: Git hooks
|
||||
# ---------------------------------------------------------------------------
|
||||
phase_git_hooks() {
|
||||
log_step "Configuring git hooks (SDLC enforcement)"
|
||||
if "${REPO_ROOT}/scripts/setup-githooks.sh"; then
|
||||
log_ok
|
||||
else
|
||||
log_error "Git hook setup failed."
|
||||
note_failure
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase: Working directories
|
||||
# ---------------------------------------------------------------------------
|
||||
phase_workdirs() {
|
||||
log_step "Creating working directories"
|
||||
mkdir -p "$OUTPUT_DIR" "$TMP_DIR"
|
||||
log_info "Created: output/ tmp/ (gitignored build artifacts)"
|
||||
log_ok
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase: Optional VM testing tooling (libvirt/qemu)
|
||||
# ---------------------------------------------------------------------------
|
||||
phase_vm_tooling() {
|
||||
log_step "Installing VM testing tooling (libvirt/qemu/ovmf/swtpm)"
|
||||
|
||||
apt_install \
|
||||
libvirt-clients \
|
||||
libvirt-daemon-system \
|
||||
qemu-system-x86 \
|
||||
qemu-utils \
|
||||
ovmf \
|
||||
swtpm \
|
||||
swtpm-tools \
|
||||
virt-manager
|
||||
|
||||
ensure_group libvirt
|
||||
|
||||
# Start/enable libvirtd if possible.
|
||||
if have systemctl; then
|
||||
as_root systemctl start libvirtd 2>/dev/null || true
|
||||
as_root systemctl enable libvirtd 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Permanent swtpm permissions fix (idempotent; safe to re-run).
|
||||
if [[ -f "${REPO_ROOT}/scripts/fix-swtpm-permissions.sh" ]]; then
|
||||
log_info "Applying swtpm default-ACL fix..."
|
||||
if [[ -d /var/lib/libvirt/swtpm ]]; then
|
||||
as_root bash "${REPO_ROOT}/scripts/fix-swtpm-permissions.sh" || \
|
||||
log_warn "swtpm permission fix did not complete (non-fatal)."
|
||||
else
|
||||
log_warn "/var/lib/libvirt/swtpm does not exist yet; run the fix after your first 'test:iso create'."
|
||||
fi
|
||||
fi
|
||||
|
||||
log_ok
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase: Smoke test (lint + unit tests, both run inside Docker)
|
||||
# ---------------------------------------------------------------------------
|
||||
phase_smoke_test() {
|
||||
log_step "Smoke test: lint + unit tests (runs inside Docker)"
|
||||
|
||||
if ! docker image inspect "$DOCKER_IMAGE" >/dev/null 2>&1; then
|
||||
log_error "Dev image not built; cannot smoke test. Fix the build step first."
|
||||
note_failure
|
||||
return 0
|
||||
fi
|
||||
|
||||
log_info "Running lint (shellcheck)..."
|
||||
if "${RUN_SH}" lint; then
|
||||
log_info "Lint passed."
|
||||
else
|
||||
log_error "Lint failed."
|
||||
note_failure
|
||||
fi
|
||||
|
||||
log_info "Running unit tests (bats)..."
|
||||
if "${RUN_SH}" test:unit; then
|
||||
log_info "Unit tests passed."
|
||||
else
|
||||
log_error "Unit tests failed."
|
||||
note_failure
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
print_summary() {
|
||||
log_step "Setup summary"
|
||||
|
||||
local checks=(
|
||||
"Docker CLI:$(have docker && echo yes || echo no)"
|
||||
"Docker daemon reachable:$(docker info >/dev/null 2>&1 && echo yes || echo no)"
|
||||
"Dev image built:$(docker image inspect "$DOCKER_IMAGE" >/dev/null 2>&1 && echo yes || echo no)"
|
||||
"Git hooks configured:$(git -C "$REPO_ROOT" config --get core.hooksPath >/dev/null && echo yes || echo no)"
|
||||
)
|
||||
if [[ "$WITH_VM" -eq 1 ]]; then
|
||||
checks+=(
|
||||
"virsh available:$(have virsh && echo yes || echo no)"
|
||||
"qemu-img available:$(have qemu-img && echo yes || echo no)"
|
||||
"swtpm_setup available:$(have swtpm_setup && echo yes || echo no)"
|
||||
)
|
||||
fi
|
||||
|
||||
for c in "${checks[@]}"; do
|
||||
printf " %-30s %s\n" "${c%%:*}" "${c#*:}"
|
||||
done
|
||||
|
||||
echo
|
||||
if [[ "$FAILED" -eq 0 ]]; then
|
||||
echo -e "${GREEN}Environment ready.${NC}"
|
||||
echo
|
||||
echo "Next steps:"
|
||||
echo " ./run.sh test # full test suite"
|
||||
echo " ./run.sh iso # build the ISO (60-90 min)"
|
||||
[[ "$WITH_VM" -eq 1 ]] && echo " ./run.sh test:iso create # boot the ISO in a VM"
|
||||
else
|
||||
echo -e "${RED}Setup completed with issues (see above).${NC}"
|
||||
echo "Address the errors, then re-run this script."
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main
|
||||
# ---------------------------------------------------------------------------
|
||||
main() {
|
||||
echo -e "${BLUE}KNEL-Football development environment setup${NC}"
|
||||
echo "Repository: $REPO_ROOT"
|
||||
|
||||
phase_check_os
|
||||
|
||||
if [[ "$SMOKE_ONLY" -eq 1 ]]; then
|
||||
phase_smoke_test
|
||||
print_summary
|
||||
exit "$FAILED"
|
||||
fi
|
||||
|
||||
phase_docker
|
||||
phase_build_image
|
||||
phase_git_hooks
|
||||
phase_workdirs
|
||||
[[ "$WITH_VM" -eq 1 ]] && phase_vm_tooling
|
||||
phase_smoke_test
|
||||
|
||||
print_summary
|
||||
exit "$FAILED"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user