Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ build/
install/

# Doxygen
html/
docs/generated/

# Testing
Testing/
Expand Down
2 changes: 1 addition & 1 deletion docs/Doxyfile
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ PROJECT_LOGO =
# entered, it will be relative to the location where doxygen was started. If
# left blank the current directory will be used.

OUTPUT_DIRECTORY =
OUTPUT_DIRECTORY = docs/generated

# If the CREATE_SUBDIRS tag is set to YES then doxygen will create up to 4096
# sub-directories (in 2 levels) under the output directory of each output format
Expand Down
23 changes: 17 additions & 6 deletions scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ set -e
# Build the project using CMake
#
# Builds in Debug mode by default and installs to the install/ directory.
# When run outside the container, delegates execution to Docker automatically.
#
# Usage:
# ./scripts/build.sh
###############################################################################

# Set build directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/env.sh"
source "$SCRIPT_DIR/docker/exec.sh"
delegate_to_container "$@"

# ---------------------------------------------------------------------------
# Build
# ---------------------------------------------------------------------------
cd "$PROJECT_ROOT"

BUILD_DIR="build"
INSTALL_DIR="install"

# Create and enter build directory
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"

# Run CMake with Debug build type by default (for development)
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="../$INSTALL_DIR" ..
log_step "Configuring CMake (Debug)..."
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="${PROJECT_ROOT}/$INSTALL_DIR" "$PROJECT_ROOT"

# Build all targets with all cores
log_step "Building with $(nproc) cores..."
make -j"$(nproc)"

# Install to ../install
log_step "Installing to ${PROJECT_ROOT}/$INSTALL_DIR"
make install

log_info "Build complete."
46 changes: 24 additions & 22 deletions scripts/coverage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,46 @@ set -e
# Build and run code coverage analysis
#
# Builds with coverage enabled, runs tests, and generates HTML coverage report.
# When run outside the container, delegates execution to Docker automatically.
#
# Usage:
# ./scripts/coverage.sh
###############################################################################

# Set build directory
BUILD_DIR="build"
INSTALL_DIR="install"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/env.sh"
source "$SCRIPT_DIR/docker/exec.sh"
delegate_to_container "$@"

echo "Building with code coverage enabled..."
# ---------------------------------------------------------------------------
# Coverage
# ---------------------------------------------------------------------------
cd "$PROJECT_ROOT"

BUILD_DIR="${PROJECT_ROOT}/build"
INSTALL_DIR="${PROJECT_ROOT}/install"

# Create and enter build directory
mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"

# Run CMake with coverage enabled
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON -DCMAKE_INSTALL_PREFIX="../$INSTALL_DIR" ..
log_step "Configuring CMake with coverage enabled..."
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_COVERAGE=ON -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" "$PROJECT_ROOT"

# Build all targets with all cores
log_step "Building with $(nproc) cores..."
make -j"$(nproc)"

# Run tests to generate coverage data
log_step "Running tests..."
make test

# Generate coverage report with lcov
echo "Generating coverage report..."
export LC_ALL=C # Fix locale warnings
log_step "Generating coverage report..."
export LC_ALL=C
lcov --capture --directory . --output-file coverage.info --ignore-errors mismatch
lcov --remove coverage.info '/usr/*' --output-file coverage.info --ignore-errors unused # Remove system files
lcov --remove coverage.info '*/build/*' --output-file coverage.info --ignore-errors unused # Remove build files
lcov --remove coverage.info '*/tests/*' --output-file coverage.info --ignore-errors unused # Remove test files
lcov --remove coverage.info '*/_deps/*' --output-file coverage.info --ignore-errors unused # Remove external deps
lcov --remove coverage.info '/usr/*' --output-file coverage.info --ignore-errors unused
lcov --remove coverage.info '*/build/*' --output-file coverage.info --ignore-errors unused
lcov --remove coverage.info '*/tests/*' --output-file coverage.info --ignore-errors unused
lcov --remove coverage.info '*/_deps/*' --output-file coverage.info --ignore-errors unused

# Generate HTML report
genhtml coverage.info --output-directory coverage_report

echo "Coverage report generated in build/coverage_report/"
echo "Open build/coverage_report/index.html in your browser to view the report"

# Optional: Install the project
# make install
log_info "Coverage report generated in ${BUILD_DIR}/coverage_report/"
log_info "Open ${BUILD_DIR}/coverage_report/index.html in your browser to view the report."
92 changes: 92 additions & 0 deletions scripts/docker/exec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash

###############################################################################
# Container delegation for project scripts
#
# When a script is invoked outside the container, this module re-executes it
# inside a disposable Docker container (docker run --rm) and exits.
# If already inside the container, it returns immediately and the calling
# script continues as normal.
#
# How it works:
#
# Host shell Container shell
# ---------- ----------------
# ./scripts/build.sh
# source env.sh
# source docker/exec.sh
# delegate_to_container
# docker run --rm ... build.sh --> ./scripts/build.sh
# exit $? source env.sh
# source docker/exec.sh
# delegate_to_container
# sees /.dockerenv
# return 0
# cmake ...
# make ...
# <-- exits (container destroyed)
#
# The host starts a subprocess (docker run) that runs the SAME script from
# scratch. The second invocation detects /.dockerenv, skips delegation,
# and executes the real work. The host waits for the exit code and forwards it.
# The container is destroyed automatically after the script finishes (--rm).
#
# Requires env.sh to be sourced first (for logging helpers and PROJECT_ROOT).
#
# Usage (from any project script, after sourcing env.sh):
# source "$SCRIPT_DIR/docker/exec.sh"
# delegate_to_container "$@"
###############################################################################

IMAGE_NAME="cpp-dev:latest"
CONTAINER_WORKDIR="/workspaces/${PROJECT_NAME}"

delegate_to_container() {
# Already inside the container -- nothing to do
if [ -f /.dockerenv ]; then
return 0
fi

# Resolve the calling script path relative to project root (portable)
local caller_script="${BASH_SOURCE[1]}"
local script_absolute
script_absolute="$(cd "$(dirname "$caller_script")" && pwd)/$(basename "$caller_script")"
local script_relative="${script_absolute#"$PROJECT_ROOT"/}"

if [ "$script_relative" = "$script_absolute" ]; then
log_error "Script '${caller_script}' is not under PROJECT_ROOT '${PROJECT_ROOT}'."
return 1
fi

# Preflight: verify Docker is installed and the daemon is reachable
if ! command -v docker &> /dev/null; then
log_error "Docker is not installed. Please install Docker to use container delegation."
return 1
fi

if ! docker version &> /dev/null; then
log_error "Docker daemon is not reachable. Is the Docker service running?"
return 1
fi

log_docker "Running outside container -- delegating to Docker..."

# Build the image if it does not exist yet
if ! docker image inspect "$IMAGE_NAME" &> /dev/null; then
log_docker "Image '${IMAGE_NAME}' not found. Building it first..."
"${PROJECT_ROOT}/scripts/docker/build_image.sh"
fi
Comment on lines +79 to +83
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When Docker isn't installed or the daemon isn't reachable, the first failing docker ... invocation will surface as a low-level error. Adding an explicit preflight check (e.g., command -v docker and a lightweight docker info/docker version probe) would let you emit a clearer log_error and exit early.

Copilot uses AI. Check for mistakes.

# Run the script inside a disposable container
docker run --rm \
--hostname cpp-devcontainer \
--env "HOST_UID=$(id -u)" \
--env "HOST_GID=$(id -g)" \
--env TERM=xterm-256color \
--volume "$PROJECT_ROOT:${CONTAINER_WORKDIR}" \
--workdir "${CONTAINER_WORKDIR}" \
"$IMAGE_NAME" \
bash "${CONTAINER_WORKDIR}/${script_relative}" "$@"

exit $?
}
19 changes: 13 additions & 6 deletions scripts/docs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,25 @@ set -e
# Generate Doxygen documentation
#
# Generates HTML documentation from source code comments.
# When run outside the container, delegates execution to Docker automatically.
#
# Usage:
# ./scripts/docs.sh
###############################################################################

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
DOCS_DIR="${PROJECT_ROOT}/docs"
source "$SCRIPT_DIR/env.sh"
source "$SCRIPT_DIR/docker/exec.sh"
delegate_to_container "$@"

echo "[INFO] Generating Doxygen documentation..."
echo "[INFO] Doxygen version: $(doxygen --version)"
# ---------------------------------------------------------------------------
# Documentation
# ---------------------------------------------------------------------------
cd "$PROJECT_ROOT"

# Run doxygen from project root
doxygen "$DOCS_DIR/Doxyfile"
log_info "Generating Doxygen documentation..."
log_info "Doxygen version: $(doxygen --version)"

doxygen docs/Doxyfile

log_info "Documentation generation complete."
67 changes: 67 additions & 0 deletions scripts/env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash

###############################################################################
# Environment setup for all project scripts
#
# Provides:
# - Colored output and logging helpers
# - Project path resolution
#
# Usage (source from other scripts):
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# source "$SCRIPT_DIR/env.sh"
###############################################################################

# ==============================================================================
# Color definitions (disabled when output is not a terminal)
# ==============================================================================
if [ -t 1 ]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
RESET='\033[0m'
else
RED=''
GREEN=''
YELLOW=''
BLUE=''
CYAN=''
BOLD=''
RESET=''
fi

# ==============================================================================
# Logging helpers
#
# - printf is used instead of echo -e for portable escape handling
# - log_warn and log_error write to stderr so pipelines stay clean
# ==============================================================================
log_info() { printf '%b %s\n' "${GREEN}[INFO]${RESET}" "$*"; }
log_warn() { printf '%b %s\n' "${YELLOW}[WARN]${RESET}" "$*" >&2; }
log_error() { printf '%b %s\n' "${RED}[ERROR]${RESET}" "$*" >&2; }
log_step() { printf '%b %s\n' "${CYAN}[STEP]${RESET}" "$*"; }
log_docker() { printf '%b %s\n' "${BLUE}[CONTAINER]${RESET}" "$*"; }

# ==============================================================================
# Project paths
#
# SCRIPT_DIR must be set by the calling script before sourcing env.sh.
# ==============================================================================
if [ -z "${SCRIPT_DIR:-}" ]; then
log_error "SCRIPT_DIR is not set. Please set SCRIPT_DIR before sourcing env.sh."
return 1
fi
Comment on lines +60 to +63
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

env.sh uses return at top-level when SCRIPT_DIR is missing. That works when sourced, but if someone accidentally executes scripts/env.sh directly it will error with “return: can only return from a function or sourced script”. Consider adding an explicit guard that enforces sourcing (exit with a clear message) before any return statements.

Copilot uses AI. Check for mistakes.

if [ ! -d "$SCRIPT_DIR" ]; then
log_error "SCRIPT_DIR '$SCRIPT_DIR' does not exist or is not a directory."
return 1
fi

if ! PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"; then
log_error "Failed to determine PROJECT_ROOT from SCRIPT_DIR '$SCRIPT_DIR'."
return 1
fi
PROJECT_NAME="$(basename "$PROJECT_ROOT")"
21 changes: 13 additions & 8 deletions scripts/format.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,27 @@ set -e
# Format all C++ source/header files using clang-format
#
# Formats all C++ files in the project according to .clang-format config.
# When run outside the container, delegates execution to Docker automatically.
#
# Usage:
# ./scripts/format.sh - Format files in-place
# ./scripts/format.sh --check - Check formatting without modifying files
###############################################################################

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
source "$SCRIPT_DIR/env.sh"
source "$SCRIPT_DIR/docker/exec.sh"
delegate_to_container "$@"

# Check if --check flag is passed
# ---------------------------------------------------------------------------
# Format
# ---------------------------------------------------------------------------
CHECK_MODE=false
if [[ "$1" == "--check" ]]; then
CHECK_MODE=true
echo "[INFO] Running clang-format in check mode (no modifications)..."
log_info "Running clang-format in check mode (no modifications)..."
else
echo "[INFO] Running clang-format on source files..."
log_info "Running clang-format on source files..."
fi

EXTENSIONS=("*.cpp" "*.hpp" "*.cc" "*.h" "*.cxx" "*.hxx")
Expand All @@ -32,17 +37,17 @@ for ext in "${EXTENSIONS[@]}"; do
for f in $FILES; do
FOUND=true
if $CHECK_MODE; then
echo "Checking $f"
log_step "Checking $f"
clang-format --dry-run --Werror "$f"
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The find output is stored in FILES and then iterated with for f in $FILES, which will break on filenames containing whitespace/newlines. Prefer iterating directly from find (e.g., -print0 + while IFS= read -r -d '') so all valid paths are handled correctly.

Copilot uses AI. Check for mistakes.
else
echo "Formatting $f"
log_step "Formatting $f"
clang-format -i "$f"
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FILES=$(find ...) followed by for f in $FILES will word-split on whitespace and can break on filenames containing spaces/tabs/newlines. Prefer iterating over find output safely (e.g., -print0 with a while IFS= read -r -d '' loop, or mapfile), instead of expanding a whitespace-separated variable.

Copilot uses AI. Check for mistakes.
fi
done
done

if ! $FOUND; then
echo "[INFO] No files found to format."
log_warn "No files found to format."
else
echo "[INFO] clang-format complete."
log_info "clang-format complete."
fi
Loading
Loading