#!/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 "$@"