From b0553c6e7073290a7a900733010219ac76b97e92 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Thu, 30 Jul 2026 12:00:50 -0500 Subject: [PATCH] feat: add reproducible dev environment setup script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 8 +- scripts/setup-dev-environment.sh | 392 +++++++++++++++++++++++++++++++ 2 files changed, 398 insertions(+), 2 deletions(-) create mode 100755 scripts/setup-dev-environment.sh diff --git a/README.md b/README.md index 2cbd8b3..d16e340 100644 --- a/README.md +++ b/README.md @@ -96,8 +96,12 @@ ls -lh output/ ### First-Time Setup (After Cloning) ```bash -# Configure git hooks (required for SDLC enforcement) -./scripts/setup-githooks.sh +# Full reproducible dev environment setup (Docker image, git hooks, smoke test) +./scripts/setup-dev-environment.sh + +# VM testing tooling (libvirt/qemu/ovmf/swtpm) is installed by default so the +# built ISO can be booted and verified. Skip it with --skip-vm if you only need +# the build/test loop. Requires sudo for apt installs. ``` ### SDLC Workflow (MANDATORY) diff --git a/scripts/setup-dev-environment.sh b/scripts/setup-dev-environment.sh new file mode 100755 index 0000000..88a88c1 --- /dev/null +++ b/scripts/setup-dev-environment.sh @@ -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 "$@"