Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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."
44 changes: 23 additions & 21 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
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/env.sh"
source "$SCRIPT_DIR/docker/exec.sh"
delegate_to_container "$@"

# ---------------------------------------------------------------------------
# Coverage
# ---------------------------------------------------------------------------
cd "$PROJECT_ROOT"

BUILD_DIR="build"
INSTALL_DIR="install"

echo "Building with code coverage enabled..."

# 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="${PROJECT_ROOT}/$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/coverage_report/"
log_info "Open build/coverage_report/index.html in your browser to view the report."
81 changes: 81 additions & 0 deletions scripts/docker/exec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!/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

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."
56 changes: 56 additions & 0 deletions scripts/env.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/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
# ==============================================================================
log_info() { echo -e "${GREEN}[INFO]${RESET} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
log_error() { echo -e "${RED}[ERROR]${RESET} $*"; }
log_step() { echo -e "${CYAN}[STEP]${RESET} $*"; }
log_docker() { echo -e "${BLUE}[CONTAINER]${RESET} $*"; }
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 logging helpers use echo -e and $*, which can produce inconsistent behavior (escape processing varies) and can mangle messages that contain backslashes or leading flags. Consider switching these helpers to printf and using "$@"/"$*" safely, and also route log_warn/log_error output to stderr so pipelines can distinguish normal output from warnings/errors.

Suggested change
log_info() { echo -e "${GREEN}[INFO]${RESET} $*"; }
log_warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; }
log_error() { echo -e "${RED}[ERROR]${RESET} $*"; }
log_step() { echo -e "${CYAN}[STEP]${RESET} $*"; }
log_docker() { echo -e "${BLUE}[CONTAINER]${RESET} $*"; }
# Logging helpers
# ==============================================================================
log_info() {
printf '%b' "${GREEN}[INFO]${RESET}"
printf ' %s' "$@"
printf '\n'
}
log_warn() {
{
printf '%b' "${YELLOW}[WARN]${RESET}"
printf ' %s' "$@"
printf '\n'
} >&2
}
log_error() {
{
printf '%b' "${RED}[ERROR]${RESET}"
printf ' %s' "$@"
printf '\n'
} >&2
}
log_step() {
printf '%b' "${CYAN}[STEP]${RESET}"
printf ' %s' "$@"
printf '\n'
}
log_docker() {
printf '%b' "${BLUE}[CONTAINER]${RESET}"
printf ' %s' "$@"
printf '\n'
}

Copilot uses AI. Check for mistakes.

# ==============================================================================
# Project paths
#
# SCRIPT_DIR must be set by the calling script before sourcing env.sh.
# ==============================================================================
if [ -z "${SCRIPT_DIR:-}" ] || [ ! -d "$SCRIPT_DIR" ]; then
echo "[ERROR] SCRIPT_DIR is not set or does not exist. Set it before sourcing env.sh." >&2
return 1
fi

PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
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 assumes SCRIPT_DIR is set by the caller; if a script forgets to set it (or it’s empty), PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." ...)" will fail with a confusing error. Consider adding an explicit guard that checks SCRIPT_DIR is non-empty and points to an existing directory, and print a clear log_error message before returning non-zero.

Suggested change
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if [ -z "${SCRIPT_DIR:-}" ]; then
log_error "SCRIPT_DIR is not set. Please set SCRIPT_DIR before sourcing env.sh."
return 1
fi
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

Copilot uses AI. Check for mistakes.
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
19 changes: 15 additions & 4 deletions scripts/lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,35 @@ set -e
# Run clang-tidy static analysis
#
# Runs clang-tidy over all C++ source files using compile_commands.json.
# When run outside the container, delegates execution to Docker automatically.
#
# Usage:
# ./scripts/lint.sh
###############################################################################

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

# ---------------------------------------------------------------------------
# Lint
# ---------------------------------------------------------------------------
cd "$PROJECT_ROOT"

BUILD_DIR="build"
TIDY_BIN=$(command -v clang-tidy || true)

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.

TIDY_BIN is computed but never used, which is confusing and can drift from the actual binary invoked. Either remove TIDY_BIN entirely or use it consistently when running clang-tidy (and when checking availability).

Copilot uses AI. Check for mistakes.
if [ -z "$TIDY_BIN" ]; then
echo "[ERROR] clang-tidy not found. Install it first."
log_error "clang-tidy not found. Install it first."
exit 1
fi

echo "[INFO] Running clang-tidy over source files..."
log_info "Running clang-tidy over source files..."

find src/ tests/ -type f \( -name '*.cpp' -o -name '*.cxx' -o -name '*.cc' \) | while read -r file; do
echo "[TIDY] $file"
log_step "$file"
clang-tidy "$file" -p "$BUILD_DIR" || true
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.

find src/ tests/ and BUILD_DIR="build" are relative to the current working directory, unlike format.sh which uses "$PROJECT_ROOT"/.... If lint.sh is run from outside the repo root (including inside the container), it may scan the wrong paths and point clang-tidy at the wrong build directory. Consider using ${PROJECT_ROOT}/src, ${PROJECT_ROOT}/tests, and ${PROJECT_ROOT}/build consistently.

Copilot uses AI. Check for mistakes.
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 loop invokes clang-tidy directly even though the script already resolved the tool path in TIDY_BIN. Using the resolved path avoids accidentally running a different clang-tidy from $PATH (or failing if $PATH changes).

Suggested change
clang-tidy "$file" -p "$BUILD_DIR" || true
"$TIDY_BIN" "$file" -p "$BUILD_DIR" || true

Copilot uses AI. Check for mistakes.
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.

TIDY_BIN is used to check that clang-tidy exists, but the script then invokes clang-tidy directly instead of using the resolved path. This makes the preflight check ineffective if a different clang-tidy is later picked up via PATH changes/aliases; use "$TIDY_BIN" for the invocation (or drop TIDY_BIN and rely on command -v failing naturally).

Suggested change
clang-tidy "$file" -p "$BUILD_DIR" || true
"$TIDY_BIN" "$file" -p "$BUILD_DIR" || true

Copilot uses AI. Check for mistakes.
done
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.

clang-tidy failures are currently ignored (|| true), so this script will almost always exit 0 and CI / pre-push hooks won’t fail on lint issues. Consider capturing the exit status (e.g., accumulate a failure flag) and exiting non-zero when any invocation reports problems (and/or add -warnings-as-errors=* depending on the intended policy).

Copilot uses AI. Check for mistakes.

echo "[INFO] clang-tidy lint complete."
log_info "clang-tidy lint complete."
Loading
Loading