diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44ffab3..cd15e2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,12 +6,14 @@ jobs: steps: - uses: actions/checkout@v4 - name: Install deps - run: sudo apt-get update && sudo apt-get install -y cmake clang-format clang-tidy + run: > + sudo apt-get update && + sudo apt-get install -y cmake clang-format clang-tidy - name: Format check run: ./scripts/format.sh --check - name: Build - run: mkdir -p build && cd build && cmake .. && make + run: ./scripts/build.sh - name: Lint check run: ./scripts/lint.sh - name: Test - run: cd build && ctest --output-on-failure \ No newline at end of file + run: ./scripts/test.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index 0feb08f..2fdb18f 100644 --- a/.gitignore +++ b/.gitignore @@ -36,7 +36,7 @@ build/ install/ # Doxygen -html/ +docs/generated/ # Testing Testing/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..735140a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,49 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Build & Test Commands + +```bash +./scripts/build.sh # Debug build with tests enabled +./scripts/test.sh # Run all tests +./scripts/package.sh # Release build + CPack packaging +./scripts/format.sh # Apply clang-format +./scripts/format.sh --check # Verify formatting (CI mode) +./scripts/lint.sh # Run clang-tidy (build first — needs compile_commands.json) +./scripts/coverage.sh # Generate lcov coverage report +./scripts/docs.sh # Generate Doxygen documentation +``` + +Pre-commit hooks run on **pre-push** (not pre-commit): `pre-commit install --hook-type pre-push` + +All scripts auto-delegate to Docker when run outside the container (`docker run --rm`). The delegation logic lives in `scripts/docker/exec.sh`, sourced by each script via `scripts/env.sh`. Inside the container or CI (`CI=true`), scripts run directly with no overhead. See `docs/ci-container-delegation.md` for details on the CI strategy and a GHCR upgrade path. + +## Architecture + +This is a C++ project template using **CMake >= 3.20** and **C++17**. Each build target lives in its own `src//` subdirectory with its own `CMakeLists.txt`, registered via `add_subdirectory()` in the root CMakeLists.txt. + +The template demonstrates four library patterns that build on each other: + +- **Static library** (`example_static`) — simplest case, compiled and linked at build time +- **Shared library** (`example_shared`) — uses RPATH (`$ORIGIN/../lib`) so the binary finds `.so` files relative to itself at runtime, making the install relocatable +- **Interface library** (`example_interface`) — header-only, no compiled output; uses `INTERFACE` visibility so dependents get the include paths automatically +- **Public/Private visibility** (`example_public_private`) — demonstrates how `PUBLIC` includes propagate to dependents while `PRIVATE` includes stay internal + +The **plugin system** (`example_plugin_loader` + `example_plugin_impl`) shows runtime loading via `dlopen()`. Plugins implement a C-compatible API defined in `plugin_api.hpp` and must export `create_plugin()` as `extern "C"`. The loader discovers plugin `.so` files via RPATH. + +The main executable (`src/main/`) links against all libraries and demonstrates their usage together. + +Tests use **GoogleTest v1.14.0** (fetched via `FetchContent`). Test files follow the pattern `tests/test_.cpp` and are discovered via `gtest_discover_tests()`. The `tests/test_helpers.hpp` provides an `OutputCapture` utility for testing stdout. + +## Code Style + +Enforced by `.clang-format` and `.clang-tidy` — CI rejects non-conforming code. + +- Google C++ style base, **4-space indent**, **100-char column limit**, K&R braces +- Pointer alignment: left (`int* ptr`) +- Naming: `lower_case` for variables/members, `CamelCase` for classes/structs +- Headers use `#pragma once` (not traditional include guards) +- Includes: sorted and grouped (main header, then system, then project) +- Use `target_include_directories` and `target_link_libraries` with correct CMake visibility (PUBLIC/PRIVATE/INTERFACE) — never raw compiler/linker flags +- Shared libraries must configure RPATH — never hardcode absolute paths diff --git a/docs/Doxyfile b/docs/Doxyfile index 0a802cf..99281ac 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -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 diff --git a/docs/ci-container-delegation.md b/docs/ci-container-delegation.md new file mode 100644 index 0000000..ab379d7 --- /dev/null +++ b/docs/ci-container-delegation.md @@ -0,0 +1,147 @@ +# CI and Container Delegation + +## How Script Delegation Works + +All project scripts (`build.sh`, `test.sh`, `format.sh`, `lint.sh`, etc.) auto-delegate to Docker when run outside the container. The delegation logic in `scripts/docker/exec.sh` checks three conditions in order: + +1. **Inside a container** (`/.dockerenv` exists) — run directly, no delegation +2. **CI environment** (`CI=true`) — run directly, tools provided by the runner +3. **Developer host** — delegate to Docker via `docker run --rm` + +``` +Developer host Container / CI runner +-------------- --------------------- +./scripts/build.sh + source env.sh + source docker/exec.sh + delegate_to_container + /.dockerenv? No + CI=true? No + docker run --rm ... build.sh --> ./scripts/build.sh + exit $? delegate_to_container + /.dockerenv? Yes + return 0 + cmake / make / ... + <-- exits +``` + +## Current CI Approach + +The CI workflow installs tools directly on the GitHub Actions runner and skips Docker delegation: + +```yaml +# .github/workflows/ci.yml +steps: + - uses: actions/checkout@v4 + - name: Install deps + run: > + sudo apt-get update && + sudo apt-get install -y cmake clang-format clang-tidy + - name: Format check + run: ./scripts/format.sh --check + - name: Build + run: ./scripts/build.sh + - name: Lint check + run: ./scripts/lint.sh + - name: Test + run: ./scripts/test.sh +``` + +GitHub Actions sets `CI=true` automatically, so `delegate_to_container` returns immediately and scripts run directly on the runner. This is fast, requires no Docker setup, and works out-of-the-box for anyone who forks the template. + +### Why not Docker in CI? + +An earlier approach ran some CI steps directly on the host and others via Docker delegation. This caused path mismatches: `compile_commands.json` generated on the host contained runner paths (`/home/runner/work/...`), but `clang-tidy` ran inside a Docker container with different paths (`/workspaces/...`), causing crashes. The current approach avoids this by running everything in the same context. + +## Alternative: GHCR Container Image + +For production projects that require identical toolchains in CI and local development, you can publish the dev container image to GitHub Container Registry (GHCR) and use it as the CI job container. + +GHCR is free for public repositories (unlimited storage and bandwidth). + +### Setup + +**1. Add a workflow to build and push the image** (`.github/workflows/docker-image.yml`): + +```yaml +name: Docker Image +on: + push: + branches: [main] + paths: + - 'Dockerfile' + - 'scripts/docker/entrypoint.sh' + - '.github/workflows/docker-image.yml' + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/cpp-dev + +jobs: + build-and-push: + runs-on: ubuntu-24.04 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: docker/Dockerfile + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} +``` + +**2. Update the CI workflow** to use the published image: + +```yaml +name: CI +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-24.04 + container: + image: ghcr.io/${{ github.repository }}/cpp-dev:latest + steps: + - uses: actions/checkout@v4 + - name: Format check + run: ./scripts/format.sh --check + - name: Build + run: ./scripts/build.sh + - name: Lint check + run: ./scripts/lint.sh + - name: Test + run: ./scripts/test.sh +``` + +### Why this works without code changes + +When GitHub Actions runs a job with `container:`, it creates `/.dockerenv` inside the container. The first check in `delegate_to_container` detects this and skips delegation. The `CI=true` check is never reached, so both guards coexist without conflict. + +### Trade-offs + +| | apt-get (current) | GHCR container | +|---|---|---| +| CI speed | Fast | Fast (pre-built image) | +| Tool consistency | Runner versions (minor drift possible) | Identical to local dev | +| Fork setup | Zero — works immediately | Must trigger image build first | +| Maintenance | 1 workflow | 2 workflows | + +### First-time setup for GHCR + +1. Push the `docker-image.yml` workflow to `main` +2. Go to Actions tab and manually trigger "Docker Image" (`workflow_dispatch`) +3. Go to Packages tab and ensure the image visibility matches the repo (public/private) +4. Update `ci.yml` to use `container:` as shown above +5. Remove the `apt-get install` step (tools come from the image) diff --git a/scripts/build.sh b/scripts/build.sh index d5c03a9..dcca1c9 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -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 -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 "$@" + +# --------------------------------------------------------------------------- +# Build +# --------------------------------------------------------------------------- +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 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="$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 $INSTALL_DIR" make install + +log_info "Build complete." diff --git a/scripts/coverage.sh b/scripts/coverage.sh index f6b1a81..75b2bed 100755 --- a/scripts/coverage.sh +++ b/scripts/coverage.sh @@ -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." diff --git a/scripts/docker/exec.sh b/scripts/docker/exec.sh new file mode 100644 index 0000000..374a266 --- /dev/null +++ b/scripts/docker/exec.sh @@ -0,0 +1,97 @@ +#!/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 + + # CI environments provide their own toolchain -- skip delegation + if [ "${CI:-}" = "true" ]; 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 + + # 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 $? +} diff --git a/scripts/docs.sh b/scripts/docs.sh index 5104282..db80cde 100755 --- a/scripts/docs.sh +++ b/scripts/docs.sh @@ -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." diff --git a/scripts/env.sh b/scripts/env.sh new file mode 100644 index 0000000..9089cea --- /dev/null +++ b/scripts/env.sh @@ -0,0 +1,74 @@ +#!/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" +############################################################################### + +# Guard: this file must be sourced, not executed directly +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + echo "[ERROR] env.sh must be sourced, not executed directly." >&2 + echo "Usage: source \"$0\"" >&2 + exit 1 +fi + +# ============================================================================== +# 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 + +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")" diff --git a/scripts/format.sh b/scripts/format.sh index 06f12f0..05e0167 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -5,6 +5,7 @@ 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 @@ -12,37 +13,38 @@ set -e ############################################################################### 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") FOUND=false -for ext in "${EXTENSIONS[@]}"; do - FILES=$(find "$PROJECT_ROOT/src" "$PROJECT_ROOT/tests" -type f -name "$ext" 2>/dev/null) - - for f in $FILES; do - FOUND=true - if $CHECK_MODE; then - echo "Checking $f" - clang-format --dry-run --Werror "$f" - else - echo "Formatting $f" - clang-format -i "$f" - fi - done -done +while IFS= read -r -d '' f; do + FOUND=true + if $CHECK_MODE; then + log_step "Checking $f" + clang-format --dry-run --Werror "$f" + else + log_step "Formatting $f" + clang-format -i "$f" + fi +done < <(find "$PROJECT_ROOT/src" "$PROJECT_ROOT/tests" -type f \ + \( -name '*.cpp' -o -name '*.hpp' -o -name '*.cc' -o -name '*.h' -o -name '*.cxx' -o -name '*.hxx' \) \ + -print0 2>/dev/null) 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 diff --git a/scripts/lint.sh b/scripts/lint.sh index af6c86f..133080b 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,24 +5,38 @@ 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 ############################################################################### -BUILD_DIR="build" -TIDY_BIN=$(command -v clang-tidy || true) +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/env.sh" +source "$SCRIPT_DIR/docker/exec.sh" +delegate_to_container "$@" -if [ -z "$TIDY_BIN" ]; then - echo "[ERROR] clang-tidy not found. Install it first." - exit 1 -fi +# --------------------------------------------------------------------------- +# Lint +# --------------------------------------------------------------------------- +cd "$PROJECT_ROOT" + +BUILD_DIR="${PROJECT_ROOT}/build" + +log_info "Running clang-tidy over source files..." -echo "[INFO] Running clang-tidy over source files..." +LINT_FAILED=0 -find src/ tests/ -type f \( -name '*.cpp' -o -name '*.cxx' -o -name '*.cc' \) | while read -r file; do - echo "[TIDY] $file" - clang-tidy "$file" -p "$BUILD_DIR" || true -done +while IFS= read -r file; do + log_step "$file" + if ! clang-tidy "$file" -p "$BUILD_DIR"; then + LINT_FAILED=1 + fi +done < <(find "${PROJECT_ROOT}/src" "${PROJECT_ROOT}/tests" -type f \( -name '*.cpp' -o -name '*.cxx' -o -name '*.cc' \)) + +if [ "$LINT_FAILED" -ne 0 ]; then + log_error "clang-tidy found issues." + exit 1 +fi -echo "[INFO] clang-tidy lint complete." +log_info "clang-tidy lint complete." diff --git a/scripts/package.sh b/scripts/package.sh index b1304c3..19998c0 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -5,35 +5,40 @@ set -e # Package the project for distribution # # Builds in Release mode and creates distributable packages. +# When run outside the container, delegates execution to Docker automatically. # # Usage: # ./scripts/package.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 "[INFO] Building project..." +# --------------------------------------------------------------------------- +# Package +# --------------------------------------------------------------------------- +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 Release build type by default (disable coverage for packaging) -cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_COVERAGE=OFF -DCMAKE_INSTALL_PREFIX="../$INSTALL_DIR" .. +log_step "Configuring CMake (Release)..." +cmake -DCMAKE_BUILD_TYPE=Release -DENABLE_COVERAGE=OFF -DCMAKE_INSTALL_PREFIX="$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 $INSTALL_DIR" make install -echo "[INFO] Creating packages..." - -# Create packages using CPack +log_step "Creating packages with CPack..." cpack -echo "[INFO] Build and packaging complete!" -echo "[INFO] Packages created:" -ls -la *.tar.gz *.zip *.deb 2>/dev/null || echo "No packages found" +log_info "Build and packaging complete." +log_info "Packages created:" +ls -la *.tar.gz *.zip *.deb 2>/dev/null || log_warn "No packages found." diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 0000000..6ff4d0c --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +set -e + +############################################################################### +# Run project tests +# +# Runs ctest on the build directory with verbose output on failure. +# When run outside the container, delegates execution to Docker automatically. +# +# Usage: +# ./scripts/test.sh +############################################################################### + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/env.sh" +source "$SCRIPT_DIR/docker/exec.sh" +delegate_to_container "$@" + +# --------------------------------------------------------------------------- +# Test +# --------------------------------------------------------------------------- +BUILD_DIR="${PROJECT_ROOT}/build" + +if [ ! -d "$BUILD_DIR" ]; then + log_error "Build directory not found. Run ./scripts/build.sh first." + exit 1 +fi + +cd "$BUILD_DIR" + +log_step "Running tests..." +ctest --output-on-failure + +log_info "All tests passed."