backend-infra-engineer: Release v0.3.3 snapshot
This commit is contained in:
@@ -115,3 +115,291 @@ cmake --build build --target build_cleaner
|
||||
- Other: `YAZE_AGENT_SOURCES`, `YAZE_TEST_SOURCES`
|
||||
|
||||
The script intelligently preserves conditional blocks (if/endif) and excludes conditional files from the main source list.
|
||||
|
||||
## verify-build-environment.\*
|
||||
|
||||
`verify-build-environment.ps1` (Windows) and `verify-build-environment.sh` (macOS/Linux) are the primary diagnostics for contributors. They now:
|
||||
|
||||
- Check for `clang-cl`, Ninja, NASM, Visual Studio workloads, and VS Code (optional).
|
||||
- Validate vcpkg bootstrap status plus `vcpkg/installed` cache contents.
|
||||
- Warn about missing ROM assets (`zelda3.sfc`, `assets/zelda3.sfc`, etc.).
|
||||
- Offer `-FixIssues` and `-CleanCache` switches to repair Git config, resync submodules, and wipe stale build directories.
|
||||
|
||||
Run the script once per machine (and rerun after major toolchain updates) to ensure presets such as `win-dbg`, `win-ai`, `mac-ai`, and `ci-windows-ai` have everything they need.
|
||||
|
||||
## setup-vcpkg-windows.ps1
|
||||
|
||||
Automates the vcpkg bootstrap flow on Windows:
|
||||
|
||||
1. Clones and bootstraps vcpkg (if not already present).
|
||||
2. Verifies that `git`, `clang-cl`, and Ninja are available, printing friendly instructions when they are missing.
|
||||
3. Installs the default triplet (`x64-windows` or `arm64-windows` when detected) and confirms that `vcpkg/installed/<triplet>` is populated.
|
||||
4. Reminds you to rerun `.\scripts\verify-build-environment.ps1 -FixIssues` to double-check the environment.
|
||||
|
||||
Use it immediately after cloning the repository or whenever you need to refresh your local dependency cache before running `win-ai` or `ci-windows-ai` presets.
|
||||
|
||||
## CMake Validation Tools
|
||||
|
||||
A comprehensive toolkit for validating CMake configuration and catching dependency issues early. These tools help prevent build failures by detecting configuration problems before compilation.
|
||||
|
||||
### validate-cmake-config.cmake
|
||||
|
||||
Validates CMake configuration by checking targets, flags, and platform-specific settings.
|
||||
|
||||
```bash
|
||||
# Validate default build directory
|
||||
cmake -P scripts/validate-cmake-config.cmake
|
||||
|
||||
# Validate specific build directory
|
||||
cmake -P scripts/validate-cmake-config.cmake build_ai
|
||||
```
|
||||
|
||||
**What it checks:**
|
||||
- Required targets exist
|
||||
- Feature flag consistency (AI requires gRPC, etc.)
|
||||
- Compiler settings (C++23, MSVC runtime on Windows)
|
||||
- Abseil configuration on Windows (prevents missing include issues)
|
||||
- Output directories
|
||||
- Common configuration mistakes
|
||||
|
||||
### check-include-paths.sh
|
||||
|
||||
Validates include paths in compile_commands.json to catch missing includes before build.
|
||||
|
||||
```bash
|
||||
# Check default build directory
|
||||
./scripts/check-include-paths.sh
|
||||
|
||||
# Check specific build
|
||||
./scripts/check-include-paths.sh build_ai
|
||||
|
||||
# Verbose mode (show all include dirs)
|
||||
VERBOSE=1 ./scripts/check-include-paths.sh build
|
||||
```
|
||||
|
||||
**Requires:** `jq` for better parsing (optional but recommended): `brew install jq`
|
||||
|
||||
**What it checks:**
|
||||
- Common dependencies (SDL2, ImGui, yaml-cpp)
|
||||
- Platform-specific includes
|
||||
- Abseil includes from gRPC build (critical on Windows)
|
||||
- Suspicious configurations (missing -I flags, relative paths)
|
||||
|
||||
### visualize-deps.py
|
||||
|
||||
Generates dependency graphs and detects circular dependencies.
|
||||
|
||||
```bash
|
||||
# Generate GraphViz diagram
|
||||
python3 scripts/visualize-deps.py build --format graphviz -o deps.dot
|
||||
dot -Tpng deps.dot -o deps.png
|
||||
|
||||
# Generate Mermaid diagram
|
||||
python3 scripts/visualize-deps.py build --format mermaid -o deps.mmd
|
||||
|
||||
# Show statistics
|
||||
python3 scripts/visualize-deps.py build --stats
|
||||
```
|
||||
|
||||
**Formats:**
|
||||
- **graphviz**: DOT format for rendering with `dot` command
|
||||
- **mermaid**: For embedding in Markdown/documentation
|
||||
- **text**: Simple text tree for quick overview
|
||||
|
||||
**Features:**
|
||||
- Detects circular dependencies (highlighted in red)
|
||||
- Shows dependency statistics
|
||||
- Color-coded targets (executables, libraries, etc.)
|
||||
|
||||
### test-cmake-presets.sh
|
||||
|
||||
Tests that all CMake presets can configure successfully.
|
||||
|
||||
```bash
|
||||
# Test all presets for current platform
|
||||
./scripts/test-cmake-presets.sh
|
||||
|
||||
# Test specific preset
|
||||
./scripts/test-cmake-presets.sh --preset mac-ai
|
||||
|
||||
# Test in parallel (faster)
|
||||
./scripts/test-cmake-presets.sh --platform mac --parallel 4
|
||||
|
||||
# Quick mode (don't clean between tests)
|
||||
./scripts/test-cmake-presets.sh --quick
|
||||
```
|
||||
|
||||
**Options:**
|
||||
- `--parallel N`: Test N presets in parallel (default: 4)
|
||||
- `--preset NAME`: Test only specific preset
|
||||
- `--platform PLATFORM`: Test only mac/win/lin presets
|
||||
- `--quick`: Skip cleaning between tests
|
||||
- `--verbose`: Show full CMake output
|
||||
|
||||
### Usage in Development Workflow
|
||||
|
||||
**After configuring CMake:**
|
||||
```bash
|
||||
cmake --preset mac-ai
|
||||
cmake -P scripts/validate-cmake-config.cmake build
|
||||
./scripts/check-include-paths.sh build
|
||||
```
|
||||
|
||||
**Before committing:**
|
||||
```bash
|
||||
# Test all platform presets configure successfully
|
||||
./scripts/test-cmake-presets.sh --platform mac
|
||||
```
|
||||
|
||||
**When adding new targets:**
|
||||
```bash
|
||||
# Check for circular dependencies
|
||||
python3 scripts/visualize-deps.py build --stats
|
||||
```
|
||||
|
||||
**For full details**, see [docs/internal/testing/cmake-validation.md](../docs/internal/testing/cmake-validation.md)
|
||||
|
||||
## Symbol Conflict Detection
|
||||
|
||||
Tools to detect One Definition Rule (ODR) violations and duplicate symbol definitions **before linking fails**.
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Extract symbols from object files
|
||||
./scripts/extract-symbols.sh
|
||||
|
||||
# Check for conflicts
|
||||
./scripts/check-duplicate-symbols.sh
|
||||
|
||||
# Run tests
|
||||
./scripts/test-symbol-detection.sh
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
#### extract-symbols.sh
|
||||
|
||||
Scans compiled object files and creates a JSON database of all symbols and their locations.
|
||||
|
||||
**Features:**
|
||||
- Cross-platform: macOS/Linux (nm), Windows (dumpbin)
|
||||
- Fast: ~2-3 seconds for typical builds
|
||||
- Identifies duplicate definitions across object files
|
||||
- Tracks symbol type (text, data, read-only, etc.)
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Extract from build directory
|
||||
./scripts/extract-symbols.sh build
|
||||
|
||||
# Custom output file
|
||||
./scripts/extract-symbols.sh build symbols.json
|
||||
```
|
||||
|
||||
**Output:** `build/symbol_database.json` - JSON with symbol conflicts listed
|
||||
|
||||
#### check-duplicate-symbols.sh
|
||||
|
||||
Analyzes symbol database and reports conflicts in developer-friendly format.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
# Check default database
|
||||
./scripts/check-duplicate-symbols.sh
|
||||
|
||||
# Verbose output
|
||||
./scripts/check-duplicate-symbols.sh --verbose
|
||||
|
||||
# Include fix suggestions
|
||||
./scripts/check-duplicate-symbols.sh --fix-suggestions
|
||||
```
|
||||
|
||||
**Exit codes:**
|
||||
- `0` = No conflicts found
|
||||
- `1` = Conflicts detected (fails in CI/pre-commit)
|
||||
|
||||
#### test-symbol-detection.sh
|
||||
|
||||
Integration test suite for symbol detection system.
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
./scripts/test-symbol-detection.sh
|
||||
```
|
||||
|
||||
**Validates:**
|
||||
- Scripts are executable
|
||||
- Build directory and object files exist
|
||||
- Symbol extraction works correctly
|
||||
- JSON database is valid
|
||||
- Duplicate checker runs successfully
|
||||
- Pre-commit hook is configured
|
||||
|
||||
### Git Hook Integration
|
||||
|
||||
**First-time setup:**
|
||||
```bash
|
||||
git config core.hooksPath .githooks
|
||||
chmod +x .githooks/pre-commit
|
||||
```
|
||||
|
||||
The pre-commit hook automatically runs symbol checks on changed files:
|
||||
- Fast: ~1-2 seconds
|
||||
- Only checks affected objects
|
||||
- Warns about conflicts
|
||||
- Can skip with `--no-verify` if needed
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
The `symbol-detection.yml` GitHub Actions workflow runs on:
|
||||
- All pushes to `master` and `develop`
|
||||
- All pull requests affecting C++ files
|
||||
- Workflows can be triggered manually
|
||||
|
||||
**What it does:**
|
||||
1. Builds project
|
||||
2. Extracts symbols from all object files
|
||||
3. Checks for conflicts
|
||||
4. Uploads symbol database as artifact
|
||||
5. Fails job if conflicts found
|
||||
|
||||
### Common Fixes
|
||||
|
||||
**Duplicate global variable:**
|
||||
```cpp
|
||||
// Bad - defined in both files
|
||||
ABSL_FLAG(std::string, rom, "", "ROM path");
|
||||
|
||||
// Fix 1: Use static (internal linkage)
|
||||
static ABSL_FLAG(std::string, rom, "", "ROM path");
|
||||
|
||||
// Fix 2: Use anonymous namespace
|
||||
namespace {
|
||||
ABSL_FLAG(std::string, rom, "", "ROM path");
|
||||
}
|
||||
```
|
||||
|
||||
**Duplicate function:**
|
||||
```cpp
|
||||
// Bad - defined in both files
|
||||
void ProcessData() { /* ... */ }
|
||||
|
||||
// Fix: Make inline or use static
|
||||
inline void ProcessData() { /* ... */ }
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
| Operation | Time |
|
||||
|-----------|------|
|
||||
| Extract (4000 objects, macOS) | ~3s |
|
||||
| Extract (4000 objects, Windows) | ~5-7s |
|
||||
| Check duplicates | <100ms |
|
||||
| Pre-commit hook | ~1-2s |
|
||||
|
||||
### Documentation
|
||||
|
||||
Full documentation available in:
|
||||
- [docs/internal/testing/symbol-conflict-detection.md](../docs/internal/testing/symbol-conflict-detection.md)
|
||||
- [docs/internal/testing/sample-symbol-database.json](../docs/internal/testing/sample-symbol-database.json)
|
||||
|
||||
@@ -12,7 +12,7 @@ NC='\033[0m' # No Color
|
||||
Z3ED="./build_test/bin/z3ed"
|
||||
RESULTS_FILE="/tmp/z3ed_ai_test_results.txt"
|
||||
USE_MOCK_ROM=true # Set to false if you want to test with a real ROM
|
||||
OLLAMA_MODEL="${OLLAMA_MODEL:-qwen2.5-coder:latest}"
|
||||
OLLAMA_MODEL="${OLLAMA_MODEL:-qwen2.5-coder:0.5b}"
|
||||
OLLAMA_PID=""
|
||||
|
||||
echo "=========================================="
|
||||
@@ -124,7 +124,7 @@ if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <ollama|gemini|mock>"
|
||||
echo ""
|
||||
echo "Environment Variables:"
|
||||
echo " OLLAMA_MODEL - Ollama model to use (default: qwen2.5-coder:latest)"
|
||||
echo " OLLAMA_MODEL - Ollama model to use (default: qwen2.5-coder:0.5b)"
|
||||
echo " GEMINI_API_KEY - Required for Gemini provider"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
@@ -228,7 +228,11 @@ run_test() {
|
||||
echo "Query: $query"
|
||||
echo ""
|
||||
|
||||
local cmd="$Z3ED agent simple-chat \"$query\" $ROM_FLAGS --ai_provider=$provider $extra_args"
|
||||
local provider_args="$extra_args"
|
||||
if [ "$provider" == "ollama" ]; then
|
||||
provider_args="--ai_model=\"$OLLAMA_MODEL\" $provider_args"
|
||||
fi
|
||||
local cmd="$Z3ED agent simple-chat \"$query\" $ROM_FLAGS --ai_provider=$provider $provider_args"
|
||||
echo "Running: $cmd"
|
||||
echo ""
|
||||
|
||||
|
||||
101
scripts/agents/README.md
Normal file
101
scripts/agents/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Agent Helper Scripts
|
||||
|
||||
| Script | Description |
|
||||
|--------|-------------|
|
||||
| `run-gh-workflow.sh` | Wrapper for `gh workflow run`, prints the run URL for easy tracking. |
|
||||
| `get-gh-workflow-status.sh` | Checks the status of a GitHub Actions workflow run using `gh run view`. |
|
||||
| `smoke-build.sh` | Runs `cmake --preset` configure/build in place and reports timing. |
|
||||
| `run-tests.sh` | Configures the preset (if needed), builds `yaze_test`, and runs `ctest` with optional args. |
|
||||
| `test-http-api.sh` | Polls the HTTP API `/api/v1/health` endpoint using curl (defaults to localhost:8080). |
|
||||
| `windows-smoke-build.ps1` | PowerShell variant of the smoke build helper for Visual Studio/Ninja presets on Windows. |
|
||||
|
||||
Usage examples:
|
||||
```bash
|
||||
# Trigger CI workflow with artifacts and HTTP API tests enabled
|
||||
scripts/agents/run-gh-workflow.sh ci.yml --ref develop upload_artifacts=true enable_http_api_tests=true
|
||||
|
||||
# Get the status of a workflow run (using either a URL or just the ID)
|
||||
scripts/agents/get-gh-workflow-status.sh https://github.com/scawful/yaze/actions/runs/19529930066
|
||||
scripts/agents/get-gh-workflow-status.sh 19529930066
|
||||
|
||||
# Smoke build mac-ai preset
|
||||
scripts/agents/smoke-build.sh mac-ai
|
||||
|
||||
# Build & run tests for mac-dbg preset with verbose ctest output
|
||||
scripts/agents/run-tests.sh mac-dbg --output-on-failure
|
||||
|
||||
# Check HTTP API health (defaults to localhost:8080)
|
||||
scripts/agents/test-http-api.sh
|
||||
|
||||
# Windows smoke build using PowerShell
|
||||
pwsh -File scripts/agents/windows-smoke-build.ps1 -Preset win-ai -Target z3ed
|
||||
```
|
||||
|
||||
When invoking these scripts, log the results on the coordination board so other agents know which
|
||||
workflows/builds were triggered and where to find artifacts/logs.
|
||||
|
||||
## Reducing Build Times
|
||||
|
||||
Local builds can take 10-15+ minutes from scratch. Follow these practices to minimize rebuild time:
|
||||
|
||||
### Use Dedicated Build Directories
|
||||
Always use a dedicated build directory like `build_ai` or `build_agent` to avoid interfering with the user's `build` directory:
|
||||
```bash
|
||||
cmake --preset mac-dbg -B build_ai
|
||||
cmake --build build_ai -j8 --target yaze
|
||||
```
|
||||
|
||||
### Incremental Builds
|
||||
Once configured, only rebuild—don't reconfigure unless CMakeLists.txt changed:
|
||||
```bash
|
||||
# GOOD: Just rebuild (fast, only recompiles changed files)
|
||||
cmake --build build_ai -j8 --target yaze
|
||||
|
||||
# AVOID: Reconfiguring when unnecessary (triggers full dependency resolution)
|
||||
cmake --preset mac-dbg -B build_ai && cmake --build build_ai
|
||||
```
|
||||
|
||||
### Build Specific Targets
|
||||
Don't build everything when you only need to verify a specific component:
|
||||
```bash
|
||||
# Build only the main editor (skips CLI, tests, etc.)
|
||||
cmake --build build_ai -j8 --target yaze
|
||||
|
||||
# Build only the CLI tool
|
||||
cmake --build build_ai -j8 --target z3ed
|
||||
|
||||
# Build only tests
|
||||
cmake --build build_ai -j8 --target yaze_test
|
||||
```
|
||||
|
||||
### Parallel Compilation
|
||||
Always use `-j8` or higher based on CPU cores:
|
||||
```bash
|
||||
cmake --build build_ai -j$(sysctl -n hw.ncpu) # macOS
|
||||
cmake --build build_ai -j$(nproc) # Linux
|
||||
```
|
||||
|
||||
### Quick Syntax Check
|
||||
For rapid iteration on compile errors, build just the affected library:
|
||||
```bash
|
||||
# If fixing errors in src/app/editor/dungeon/, build just the editor lib
|
||||
cmake --build build_ai -j8 --target yaze_editor
|
||||
```
|
||||
|
||||
### Verifying Changes Before CI
|
||||
Before pushing to trigger CI builds (which take 15-20 minutes each):
|
||||
1. Run an incremental local build to catch obvious errors
|
||||
2. If you modified a specific component, build just that target
|
||||
3. Only push when local build succeeds
|
||||
|
||||
### ccache/sccache (Advanced)
|
||||
If available, these tools cache compilation results across rebuilds:
|
||||
```bash
|
||||
# Check if ccache is installed
|
||||
which ccache
|
||||
|
||||
# View cache statistics
|
||||
ccache -s
|
||||
```
|
||||
|
||||
The project's CMake configuration automatically uses ccache when available.
|
||||
35
scripts/agents/get-gh-workflow-status.sh
Executable file
35
scripts/agents/get-gh-workflow-status.sh
Executable file
@@ -0,0 +1,35 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# A script to check the status of a GitHub Actions workflow run.
|
||||
#
|
||||
# Usage: ./get-gh-workflow-status.sh <run_url>
|
||||
#
|
||||
# Requires `gh` (GitHub CLI) and `jq` to be installed and authenticated.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 <run_url_or_run_id>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
RUN_ID_OR_URL="$1"
|
||||
|
||||
# Extract run ID from URL if a URL is provided
|
||||
if [[ "$RUN_ID_OR_URL" == *"github.com"* ]]; then
|
||||
RUN_ID=$(basename "$RUN_ID_OR_URL")
|
||||
else
|
||||
RUN_ID="$RUN_ID_OR_URL"
|
||||
fi
|
||||
|
||||
echo "Fetching status for workflow run ID: $RUN_ID..."
|
||||
|
||||
# Use GitHub CLI to get the run and its jobs, then format with jq
|
||||
gh run view "$RUN_ID" --json jobs,status,conclusion,name,url --jq '
|
||||
"Run: " + .name + " (" + .status + "/" + (.conclusion // "in_progress") + ")",
|
||||
"URL: " + .url,
|
||||
"",
|
||||
"Jobs:",
|
||||
"----",
|
||||
(.jobs[] | " - " + .name + ": " + .conclusion + " (" + (.status // "unknown") + ")")
|
||||
'
|
||||
50
scripts/agents/run-gh-workflow.sh
Normal file
50
scripts/agents/run-gh-workflow.sh
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wrapper for triggering GitHub Actions workflows via gh CLI.
|
||||
# Usage: scripts/agents/run-gh-workflow.sh <workflow_file> [--ref <ref>] [key=value ...]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "error: gh CLI is required (https://cli.github.com/)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <workflow_file> [--ref <ref>] [key=value ...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORKFLOW="$1"
|
||||
shift
|
||||
|
||||
REF=""
|
||||
INPUT_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ref)
|
||||
REF="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
INPUT_ARGS+=("-f" "$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
CMD=(gh workflow run "$WORKFLOW")
|
||||
if [[ -n "$REF" ]]; then
|
||||
CMD+=("--ref" "$REF")
|
||||
fi
|
||||
if [[ ${#INPUT_ARGS[@]} -gt 0 ]]; then
|
||||
CMD+=("${INPUT_ARGS[@]}")
|
||||
fi
|
||||
|
||||
echo "+ ${CMD[*]}"
|
||||
"${CMD[@]}"
|
||||
|
||||
RUN_URL=$(gh run list --workflow "$WORKFLOW" --limit 1 --json url -q '.[0].url')
|
||||
if [[ -n "$RUN_URL" ]]; then
|
||||
echo "Triggered workflow. Track progress at: $RUN_URL"
|
||||
fi
|
||||
81
scripts/agents/run-tests.sh
Executable file
81
scripts/agents/run-tests.sh
Executable file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Helper script to configure, build, and run tests for a given CMake preset.
|
||||
# Usage: scripts/agents/run-tests.sh <preset> [ctest-args...]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <preset> [ctest-args...]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PRESET="$1"
|
||||
shift
|
||||
|
||||
echo "Configuring preset: $PRESET"
|
||||
cmake --preset "$PRESET" || { echo "Configure failed for preset: $PRESET"; exit 1; }
|
||||
|
||||
ROOT_DIR=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
read -r GENERATOR BUILD_CONFIG <<EOF
|
||||
$(python - <<'PY' "$PRESET" "$ROOT_DIR"
|
||||
import json, sys, os
|
||||
preset = sys.argv[1]
|
||||
root = sys.argv[2]
|
||||
with open(os.path.join(root, "CMakePresets.json")) as f:
|
||||
data = json.load(f)
|
||||
configure = {p["name"]: p for p in data.get("configurePresets", [])}
|
||||
build = {p["name"]: p for p in data.get("buildPresets", [])}
|
||||
|
||||
def parents(entry):
|
||||
inherits = entry.get("inherits", [])
|
||||
if isinstance(inherits, str):
|
||||
inherits = [inherits]
|
||||
return inherits
|
||||
|
||||
def resolve_generator(name, seen=None):
|
||||
if seen is None:
|
||||
seen = set()
|
||||
if name in seen:
|
||||
return None
|
||||
seen.add(name)
|
||||
entry = configure.get(name)
|
||||
if not entry:
|
||||
return None
|
||||
gen = entry.get("generator")
|
||||
if gen:
|
||||
return gen
|
||||
for parent in parents(entry):
|
||||
gen = resolve_generator(parent, seen)
|
||||
if gen:
|
||||
return gen
|
||||
return None
|
||||
|
||||
generator = resolve_generator(preset)
|
||||
build_preset = build.get(preset, {})
|
||||
config = build_preset.get("configuration")
|
||||
if not config:
|
||||
entry = configure.get(preset, {})
|
||||
cache = entry.get("cacheVariables", {})
|
||||
config = cache.get("CMAKE_BUILD_TYPE", "Debug")
|
||||
|
||||
print(generator or "")
|
||||
print(config or "")
|
||||
PY)
|
||||
EOF
|
||||
|
||||
echo "Building tests for preset: $PRESET"
|
||||
BUILD_CMD=(cmake --build --preset "$PRESET")
|
||||
if [[ "$GENERATOR" == *"Visual Studio"* && -n "$BUILD_CONFIG" ]]; then
|
||||
BUILD_CMD+=(--config "$BUILD_CONFIG")
|
||||
fi
|
||||
"${BUILD_CMD[@]}" || { echo "Build failed for preset: $PRESET"; exit 1; }
|
||||
|
||||
if ctest --preset "$PRESET" --show-only >/dev/null 2>&1; then
|
||||
echo "Running tests for preset: $PRESET"
|
||||
ctest --preset "$PRESET" "$@"
|
||||
else
|
||||
echo "Test preset '$PRESET' not found, falling back to 'all' tests."
|
||||
ctest --preset all "$@"
|
||||
fi
|
||||
|
||||
echo "All tests passed for preset: $PRESET"
|
||||
19
scripts/agents/smoke-build.sh
Normal file
19
scripts/agents/smoke-build.sh
Normal file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# Quick smoke build for a given preset in an isolated directory with timing info.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ $# -lt 1 ]]; then
|
||||
echo "Usage: $0 <preset> [build_dir]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PRESET="$1"
|
||||
|
||||
START=$(date +%s)
|
||||
cmake --preset "$PRESET"
|
||||
cmake --build --preset "$PRESET"
|
||||
END=$(date +%s)
|
||||
|
||||
ELAPSED=$((END - START))
|
||||
echo "Smoke build '$PRESET' completed in ${ELAPSED}s"
|
||||
28
scripts/agents/test-http-api.sh
Executable file
28
scripts/agents/test-http-api.sh
Executable file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# Basic health check for the HTTP API server.
|
||||
# Usage: scripts/agents/test-http-api.sh [host] [port]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${1:-127.0.0.1}"
|
||||
PORT="${2:-8080}"
|
||||
URL="http://${HOST}:${PORT}/api/v1/health"
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "error: curl is required to test the HTTP API" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Checking HTTP API health endpoint at ${URL}"
|
||||
|
||||
for attempt in {1..10}; do
|
||||
if curl -fsS "${URL}" >/dev/null; then
|
||||
echo "HTTP API responded successfully (attempt ${attempt})"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt ${attempt} failed; retrying..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "error: HTTP API did not respond at ${URL}" >&2
|
||||
exit 1
|
||||
70
scripts/agents/windows-smoke-build.ps1
Normal file
70
scripts/agents/windows-smoke-build.ps1
Normal file
@@ -0,0 +1,70 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Preset,
|
||||
[string]$Target = ""
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Resolve-Path "$PSScriptRoot/.."
|
||||
Set-Location $repoRoot
|
||||
|
||||
function Get-GeneratorAndConfig {
|
||||
param([string]$PresetName)
|
||||
|
||||
$jsonPath = Join-Path $repoRoot "CMakePresets.json"
|
||||
$data = Get-Content $jsonPath -Raw | ConvertFrom-Json
|
||||
$configurePresets = @{}
|
||||
foreach ($preset in $data.configurePresets) {
|
||||
$configurePresets[$preset.name] = $preset
|
||||
}
|
||||
|
||||
$buildPresets = @{}
|
||||
foreach ($preset in $data.buildPresets) {
|
||||
$buildPresets[$preset.name] = $preset
|
||||
}
|
||||
|
||||
function Resolve-Generator([string]$name, [hashtable]$seen) {
|
||||
if ($seen.ContainsKey($name)) { return $null }
|
||||
$seen[$name] = $true
|
||||
if (-not $configurePresets.ContainsKey($name)) { return $null }
|
||||
$entry = $configurePresets[$name]
|
||||
if ($entry.generator) { return $entry.generator }
|
||||
$inherits = $entry.inherits
|
||||
if ($inherits -is [string]) { $inherits = @($inherits) }
|
||||
foreach ($parent in $inherits) {
|
||||
$gen = Resolve-Generator $parent $seen
|
||||
if ($gen) { return $gen }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
$generator = Resolve-Generator $PresetName @{}
|
||||
|
||||
$config = $null
|
||||
if ($buildPresets.ContainsKey($PresetName) -and $buildPresets[$PresetName].configuration) {
|
||||
$config = $buildPresets[$PresetName].configuration
|
||||
} elseif ($configurePresets.ContainsKey($PresetName)) {
|
||||
$cache = $configurePresets[$PresetName].cacheVariables
|
||||
if ($cache.CMAKE_BUILD_TYPE) { $config = $cache.CMAKE_BUILD_TYPE }
|
||||
}
|
||||
|
||||
return @{ Generator = $generator; Configuration = $config }
|
||||
}
|
||||
|
||||
Write-Host "Configuring preset: $Preset"
|
||||
cmake --preset $Preset
|
||||
|
||||
$info = Get-GeneratorAndConfig -PresetName $Preset
|
||||
$buildCmd = @("cmake", "--build", "--preset", $Preset)
|
||||
if ($Target) { $buildCmd += @("--target", $Target) }
|
||||
if ($info.Generator -like "*Visual Studio*" -and $info.Configuration) {
|
||||
$buildCmd += @("--config", $info.Configuration)
|
||||
}
|
||||
|
||||
Write-Host "Building preset: $Preset"
|
||||
Write-Host "+ $($buildCmd -join ' ')"
|
||||
& $buildCmd
|
||||
|
||||
Write-Host "Smoke build completed for preset: $Preset"
|
||||
140
scripts/check-duplicate-symbols.sh
Executable file
140
scripts/check-duplicate-symbols.sh
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/bin/bash
|
||||
# Duplicate Symbol Checker - Analyze symbol database for conflicts
|
||||
#
|
||||
# Usage: ./scripts/check-duplicate-symbols.sh [SYMBOL_DB] [--verbose] [--fix-suggestions]
|
||||
# SYMBOL_DB: Path to symbol database JSON (default: build/symbol_database.json)
|
||||
# --verbose: Show all symbols (not just conflicts)
|
||||
# --fix-suggestions: Include suggested fixes for conflicts
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Configuration
|
||||
SYMBOL_DB="${1:-.}"
|
||||
VERBOSE=false
|
||||
FIX_SUGGESTIONS=false
|
||||
|
||||
# If first arg is a flag, use default database
|
||||
if [[ "${SYMBOL_DB}" == --* ]]; then
|
||||
SYMBOL_DB="."
|
||||
fi
|
||||
|
||||
# Handle case where SYMBOL_DB is a directory
|
||||
if [[ -d "${SYMBOL_DB}" ]]; then
|
||||
SYMBOL_DB="${SYMBOL_DB}/symbol_database.json"
|
||||
fi
|
||||
|
||||
# Parse additional arguments
|
||||
for arg in "$@"; do
|
||||
case "${arg}" in
|
||||
--verbose) VERBOSE=true ;;
|
||||
--fix-suggestions) FIX_SUGGESTIONS=true ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validation
|
||||
if [[ ! -f "${SYMBOL_DB}" ]]; then
|
||||
echo -e "${RED}Error: Symbol database not found: ${SYMBOL_DB}${NC}"
|
||||
echo "Generate it first with: ./scripts/extract-symbols.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to show a symbol conflict with details
|
||||
show_conflict() {
|
||||
local symbol="$1"
|
||||
local count="$2"
|
||||
local definitions_json="$3"
|
||||
|
||||
echo -e "\n${RED}SYMBOL CONFLICT DETECTED${NC}"
|
||||
echo -e " Symbol: ${CYAN}${symbol}${NC}"
|
||||
echo -e " Defined in: ${RED}${count} object files${NC}"
|
||||
|
||||
# Parse JSON and show each definition
|
||||
python3 << PYTHON_EOF
|
||||
import json
|
||||
import sys
|
||||
|
||||
definitions = json.loads('''${definitions_json}''')
|
||||
|
||||
for i, defn in enumerate(definitions, 1):
|
||||
obj_file = defn.get('object_file', '?')
|
||||
sym_type = defn.get('type', '?')
|
||||
print(f" {i}. {obj_file} (type: {sym_type})")
|
||||
PYTHON_EOF
|
||||
|
||||
# Show fix suggestions if requested
|
||||
if ${FIX_SUGGESTIONS}; then
|
||||
echo -e "\n ${YELLOW}Suggested fixes:${NC}"
|
||||
echo " 1. Add 'static' or 'inline' to make the symbol have internal linkage"
|
||||
echo " 2. Move definition to a header file with inline/constexpr"
|
||||
echo " 3. Use anonymous namespace {} in .cc file"
|
||||
echo " 4. Use 'extern' keyword to declare without defining"
|
||||
echo " 5. Use ODR-friendly patterns (Meyers' singleton, etc.)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main analysis
|
||||
echo -e "${BLUE}=== Duplicate Symbol Checker ===${NC}"
|
||||
echo -e "Database: ${SYMBOL_DB}"
|
||||
echo ""
|
||||
|
||||
# Parse JSON and check for conflicts
|
||||
python3 << PYTHON_EOF
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
with open("${SYMBOL_DB}", "r") as f:
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
print(f"${RED}Error reading database: {e}${NC}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
metadata = data.get("metadata", {})
|
||||
conflicts = data.get("conflicts", [])
|
||||
symbols = data.get("symbols", {})
|
||||
|
||||
# Display metadata
|
||||
print(f"Platform: {metadata.get('platform', '?')}")
|
||||
print(f"Build directory: {metadata.get('build_dir', '?')}")
|
||||
print(f"Timestamp: {metadata.get('timestamp', '?')}")
|
||||
print(f"Object files scanned: {metadata.get('object_files_scanned', 0)}")
|
||||
print(f"Total symbols: {metadata.get('total_symbols', 0)}")
|
||||
print(f"Total conflicts: {len(conflicts)}")
|
||||
print("")
|
||||
|
||||
# Show conflicts
|
||||
if conflicts:
|
||||
print(f"${RED}CONFLICTS FOUND:${NC}\n")
|
||||
|
||||
for i, conflict in enumerate(conflicts, 1):
|
||||
symbol = conflict.get("symbol", "?")
|
||||
count = conflict.get("count", 0)
|
||||
definitions = conflict.get("definitions", [])
|
||||
|
||||
print(f"${RED}[{i}/{len(conflicts)}]${NC} {symbol} (x{count})")
|
||||
for j, defn in enumerate(definitions, 1):
|
||||
obj = defn.get("object_file", "?")
|
||||
sym_type = defn.get("type", "?")
|
||||
print(f" {j}. {obj} (type: {sym_type})")
|
||||
print("")
|
||||
|
||||
print(f"${RED}=== Summary ===${NC}")
|
||||
print(f"Total conflicts: ${RED}{len(conflicts)}${NC}")
|
||||
print(f"Fix these before linking!${NC}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"${GREEN}No conflicts found! Symbol table is clean.${NC}")
|
||||
sys.exit(0)
|
||||
|
||||
PYTHON_EOF
|
||||
|
||||
exit_code=$?
|
||||
exit ${exit_code}
|
||||
294
scripts/check-include-paths.sh
Executable file
294
scripts/check-include-paths.sh
Executable file
@@ -0,0 +1,294 @@
|
||||
#!/bin/bash
|
||||
# Include Path Checker
|
||||
# Validates that all required include paths are present in compile commands
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/check-include-paths.sh [build_directory]
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 - All checks passed
|
||||
# 1 - Validation failed (missing includes detected)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Determine build directory
|
||||
BUILD_DIR="${1:-build}"
|
||||
|
||||
if [ ! -d "$BUILD_DIR" ]; then
|
||||
echo -e "${RED}✗ Build directory not found: $BUILD_DIR${NC}"
|
||||
echo "Run cmake configure first: cmake --preset <preset-name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_DIR/compile_commands.json" ]; then
|
||||
echo -e "${RED}✗ compile_commands.json not found in $BUILD_DIR${NC}"
|
||||
echo "Make sure CMAKE_EXPORT_COMPILE_COMMANDS is ON"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}=== Include Path Validation ===${NC}"
|
||||
echo "Build directory: $BUILD_DIR"
|
||||
echo ""
|
||||
|
||||
# Parse compile_commands.json using jq if available, otherwise use grep
|
||||
if command -v jq &> /dev/null; then
|
||||
USE_JQ=true
|
||||
echo -e "${GREEN}✓ Using jq for JSON parsing${NC}"
|
||||
else
|
||||
USE_JQ=false
|
||||
echo -e "${YELLOW}⚠ jq not found - using basic parsing (install jq for better results)${NC}"
|
||||
fi
|
||||
|
||||
# Counter for issues
|
||||
ERROR_COUNT=0
|
||||
WARNING_COUNT=0
|
||||
CHECK_COUNT=0
|
||||
|
||||
# Function to check if a path exists in compile commands
|
||||
check_include_path() {
|
||||
local path="$1"
|
||||
local description="$2"
|
||||
local is_required="${3:-true}"
|
||||
|
||||
CHECK_COUNT=$((CHECK_COUNT + 1))
|
||||
|
||||
if [ "$USE_JQ" = true ]; then
|
||||
# Use jq to search compile commands
|
||||
if jq -e "[.[].command | select(contains(\"$path\"))] | length > 0" "$BUILD_DIR/compile_commands.json" &> /dev/null; then
|
||||
echo -e "${GREEN}✓${NC} $description: $path"
|
||||
return 0
|
||||
else
|
||||
if [ "$is_required" = true ]; then
|
||||
echo -e "${RED}✗${NC} Missing required include: $description"
|
||||
echo " Expected path: $path"
|
||||
ERROR_COUNT=$((ERROR_COUNT + 1))
|
||||
return 1
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Optional include not found: $description"
|
||||
WARNING_COUNT=$((WARNING_COUNT + 1))
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
else
|
||||
# Basic grep-based search
|
||||
if grep -q "$path" "$BUILD_DIR/compile_commands.json"; then
|
||||
echo -e "${GREEN}✓${NC} $description: found"
|
||||
return 0
|
||||
else
|
||||
if [ "$is_required" = true ]; then
|
||||
echo -e "${RED}✗${NC} Missing required include: $description"
|
||||
ERROR_COUNT=$((ERROR_COUNT + 1))
|
||||
return 1
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} Optional include not found: $description"
|
||||
WARNING_COUNT=$((WARNING_COUNT + 1))
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to extract unique include directories from compile commands
|
||||
extract_include_dirs() {
|
||||
echo -e "\n${BLUE}=== Include Directories Found ===${NC}"
|
||||
|
||||
if [ "$USE_JQ" = true ]; then
|
||||
jq -r '.[].command' "$BUILD_DIR/compile_commands.json" | \
|
||||
grep -oE -- '-I[^ ]+' | \
|
||||
sed 's/^-I//' | \
|
||||
sort -u | \
|
||||
head -50
|
||||
else
|
||||
grep -oE -- '-I[^ ]+' "$BUILD_DIR/compile_commands.json" | \
|
||||
sed 's/^-I//' | \
|
||||
sort -u | \
|
||||
head -50
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check for Abseil includes (Windows issue)
|
||||
check_abseil_includes() {
|
||||
echo -e "\n${BLUE}=== Checking Abseil Includes (Windows Issue) ===${NC}"
|
||||
|
||||
# Check if gRPC is enabled
|
||||
if [ -d "$BUILD_DIR/_deps/grpc-build" ]; then
|
||||
echo "gRPC build detected - checking Abseil paths..."
|
||||
|
||||
# Check for the problematic missing include
|
||||
if [ -d "$BUILD_DIR/_deps/grpc-build/third_party/abseil-cpp" ]; then
|
||||
local absl_dir="$BUILD_DIR/_deps/grpc-build/third_party/abseil-cpp"
|
||||
check_include_path "$absl_dir" "Abseil from gRPC build" true
|
||||
fi
|
||||
|
||||
# Check for generator expression variants
|
||||
if grep -q '\$<BUILD_INTERFACE:.*abseil-cpp' "$BUILD_DIR/compile_commands.json" 2>/dev/null; then
|
||||
echo -e "${YELLOW}⚠${NC} Generator expressions found in compile commands (may not be expanded)"
|
||||
WARNING_COUNT=$((WARNING_COUNT + 1))
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} gRPC build not detected - skipping Abseil checks"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check platform-specific includes
|
||||
check_platform_includes() {
|
||||
echo -e "\n${BLUE}=== Platform-Specific Includes ===${NC}"
|
||||
|
||||
# Detect platform
|
||||
case "$(uname -s)" in
|
||||
Darwin*)
|
||||
echo "Platform: macOS"
|
||||
# macOS-specific checks
|
||||
check_include_path "SDL2" "SDL2 framework/library" true
|
||||
;;
|
||||
Linux*)
|
||||
echo "Platform: Linux"
|
||||
# Linux-specific checks
|
||||
check_include_path "SDL2" "SDL2 library" true
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
echo "Platform: Windows"
|
||||
# Windows-specific checks
|
||||
check_include_path "SDL2" "SDL2 library" true
|
||||
;;
|
||||
*)
|
||||
echo "Platform: Unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Function to validate common dependencies
|
||||
check_common_dependencies() {
|
||||
echo -e "\n${BLUE}=== Common Dependencies ===${NC}"
|
||||
|
||||
# SDL2
|
||||
check_include_path "SDL" "SDL2 includes" true
|
||||
|
||||
# ImGui (should be in build/_deps or ext/)
|
||||
if grep -q "imgui" "$BUILD_DIR/compile_commands.json"; then
|
||||
echo -e "${GREEN}✓${NC} ImGui includes found"
|
||||
else
|
||||
echo -e "${RED}✗${NC} ImGui includes not found"
|
||||
ERROR_COUNT=$((ERROR_COUNT + 1))
|
||||
fi
|
||||
|
||||
# yaml-cpp
|
||||
if grep -q "yaml-cpp" "$BUILD_DIR/compile_commands.json"; then
|
||||
echo -e "${GREEN}✓${NC} yaml-cpp includes found"
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} yaml-cpp includes not found (may be optional)"
|
||||
WARNING_COUNT=$((WARNING_COUNT + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to check for suspicious configurations
|
||||
check_suspicious_configs() {
|
||||
echo -e "\n${BLUE}=== Suspicious Configurations ===${NC}"
|
||||
|
||||
# Check for missing -I flags entirely
|
||||
if [ "$USE_JQ" = true ]; then
|
||||
local compile_cmds=$(jq -r '.[].command' "$BUILD_DIR/compile_commands.json" | wc -l)
|
||||
local include_cmds=$(jq -r '.[].command' "$BUILD_DIR/compile_commands.json" | grep -c -- '-I' || true)
|
||||
|
||||
if [ "$include_cmds" -eq 0 ] && [ "$compile_cmds" -gt 0 ]; then
|
||||
echo -e "${RED}✗${NC} No -I flags found in any compile command!"
|
||||
ERROR_COUNT=$((ERROR_COUNT + 1))
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} Include flags present ($include_cmds/$compile_cmds commands)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for absolute vs relative paths
|
||||
if grep -q -- '-I\.\.' "$BUILD_DIR/compile_commands.json" 2>/dev/null; then
|
||||
echo -e "${YELLOW}⚠${NC} Relative include paths found (../) - may cause issues"
|
||||
WARNING_COUNT=$((WARNING_COUNT + 1))
|
||||
fi
|
||||
|
||||
# Check for duplicate include paths
|
||||
local duplicates
|
||||
if [ "$USE_JQ" = true ]; then
|
||||
duplicates=$(jq -r '.[].command' "$BUILD_DIR/compile_commands.json" | \
|
||||
grep -oE -- '-I[^ ]+' | \
|
||||
sort | uniq -d | wc -l)
|
||||
if [ "$duplicates" -gt 0 ]; then
|
||||
echo -e "${YELLOW}⚠${NC} $duplicates duplicate include paths found (usually harmless)"
|
||||
else
|
||||
echo -e "${GREEN}✓${NC} No duplicate include paths"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to analyze a specific source file
|
||||
analyze_file_includes() {
|
||||
local source_file="${1:-}"
|
||||
|
||||
if [ -z "$source_file" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
echo -e "\n${BLUE}=== Analyzing: $source_file ===${NC}"
|
||||
|
||||
if [ "$USE_JQ" = true ]; then
|
||||
local includes=$(jq -r ".[] | select(.file | contains(\"$source_file\")) | .command" \
|
||||
"$BUILD_DIR/compile_commands.json" | \
|
||||
grep -oE -- '-I[^ ]+' | \
|
||||
sed 's/^-I//')
|
||||
|
||||
if [ -n "$includes" ]; then
|
||||
echo "Include paths for this file:"
|
||||
echo "$includes" | while read -r path; do
|
||||
echo " - $path"
|
||||
done
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} File not found in compile commands"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
echo -e "${BLUE}=== Running Include Path Checks ===${NC}\n"
|
||||
|
||||
check_common_dependencies
|
||||
check_platform_includes
|
||||
check_abseil_includes
|
||||
check_suspicious_configs
|
||||
|
||||
# Optional: Show all include directories
|
||||
if [ "${VERBOSE:-0}" -eq 1 ]; then
|
||||
extract_include_dirs
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo -e "\n${BLUE}=== Summary ===${NC}"
|
||||
echo "Checks performed: $CHECK_COUNT"
|
||||
|
||||
if [ $ERROR_COUNT -gt 0 ]; then
|
||||
echo -e "${RED}Errors: $ERROR_COUNT${NC}"
|
||||
fi
|
||||
|
||||
if [ $WARNING_COUNT -gt 0 ]; then
|
||||
echo -e "${YELLOW}Warnings: $WARNING_COUNT${NC}"
|
||||
fi
|
||||
|
||||
if [ $ERROR_COUNT -eq 0 ] && [ $WARNING_COUNT -eq 0 ]; then
|
||||
echo -e "${GREEN}✓ All include path checks passed!${NC}"
|
||||
exit 0
|
||||
elif [ $ERROR_COUNT -eq 0 ]; then
|
||||
echo -e "${YELLOW}⚠ Include paths have warnings but should work${NC}"
|
||||
exit 0
|
||||
else
|
||||
echo -e "${RED}✗ Include path validation failed - fix errors before building${NC}"
|
||||
echo ""
|
||||
echo "Common fixes:"
|
||||
echo " 1. Reconfigure: cmake --preset <preset> --fresh"
|
||||
echo " 2. Check dependencies.cmake for missing includes"
|
||||
echo " 3. On Windows with gRPC: verify Abseil include propagation"
|
||||
exit 1
|
||||
fi
|
||||
322
scripts/dev-setup.sh
Executable file
322
scripts/dev-setup.sh
Executable file
@@ -0,0 +1,322 @@
|
||||
#!/bin/bash
|
||||
# YAZE Developer Setup Script
|
||||
# One-command setup for new developers
|
||||
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Print functions
|
||||
print_header() {
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
echo -e "${BLUE}$1${NC}"
|
||||
echo -e "${BLUE}================================${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗ $1${NC}"
|
||||
}
|
||||
|
||||
# Detect OS
|
||||
detect_os() {
|
||||
if [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
OS="linux"
|
||||
elif [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
OS="macos"
|
||||
elif [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "cygwin" ]]; then
|
||||
OS="windows"
|
||||
else
|
||||
OS="unknown"
|
||||
fi
|
||||
echo "Detected OS: $OS"
|
||||
}
|
||||
|
||||
# Check prerequisites
|
||||
check_prerequisites() {
|
||||
print_header "Checking Prerequisites"
|
||||
|
||||
# Check Git
|
||||
if command -v git &> /dev/null; then
|
||||
print_success "Git found: $(git --version)"
|
||||
else
|
||||
print_error "Git not found. Please install Git first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check CMake
|
||||
if command -v cmake &> /dev/null; then
|
||||
CMAKE_VERSION=$(cmake --version | head -n1 | cut -d' ' -f3)
|
||||
print_success "CMake found: $CMAKE_VERSION"
|
||||
|
||||
# Check version
|
||||
CMAKE_MAJOR=$(echo $CMAKE_VERSION | cut -d'.' -f1)
|
||||
CMAKE_MINOR=$(echo $CMAKE_VERSION | cut -d'.' -f2)
|
||||
if [ "$CMAKE_MAJOR" -lt 3 ] || ([ "$CMAKE_MAJOR" -eq 3 ] && [ "$CMAKE_MINOR" -lt 16 ]); then
|
||||
print_error "CMake 3.16+ required. Found: $CMAKE_VERSION"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "CMake not found. Please install CMake 3.16+ first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check compiler
|
||||
if command -v gcc &> /dev/null; then
|
||||
GCC_VERSION=$(gcc --version | head -n1 | cut -d' ' -f4)
|
||||
print_success "GCC found: $GCC_VERSION"
|
||||
elif command -v clang &> /dev/null; then
|
||||
CLANG_VERSION=$(clang --version | head -n1 | cut -d' ' -f4)
|
||||
print_success "Clang found: $CLANG_VERSION"
|
||||
else
|
||||
print_error "No C++ compiler found. Please install GCC 12+ or Clang 14+."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check Ninja
|
||||
if command -v ninja &> /dev/null; then
|
||||
print_success "Ninja found: $(ninja --version)"
|
||||
else
|
||||
print_warning "Ninja not found. Will use Make instead."
|
||||
fi
|
||||
}
|
||||
|
||||
# Install dependencies
|
||||
install_dependencies() {
|
||||
print_header "Installing Dependencies"
|
||||
|
||||
case $OS in
|
||||
"linux")
|
||||
if command -v apt-get &> /dev/null; then
|
||||
print_success "Installing dependencies via apt..."
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential ninja-build pkg-config ccache \
|
||||
libsdl2-dev libyaml-cpp-dev libgtk-3-dev libglew-dev
|
||||
elif command -v dnf &> /dev/null; then
|
||||
print_success "Installing dependencies via dnf..."
|
||||
sudo dnf install -y gcc-c++ ninja-build pkgconfig SDL2-devel yaml-cpp-devel
|
||||
elif command -v pacman &> /dev/null; then
|
||||
print_success "Installing dependencies via pacman..."
|
||||
sudo pacman -S --needed base-devel ninja pkgconfig sdl2 yaml-cpp
|
||||
else
|
||||
print_warning "Unknown Linux distribution. Please install dependencies manually."
|
||||
fi
|
||||
;;
|
||||
"macos")
|
||||
if command -v brew &> /dev/null; then
|
||||
print_success "Installing dependencies via Homebrew..."
|
||||
brew install cmake ninja pkg-config ccache sdl2 yaml-cpp
|
||||
else
|
||||
print_warning "Homebrew not found. Please install dependencies manually."
|
||||
fi
|
||||
;;
|
||||
"windows")
|
||||
print_warning "Windows detected. Please install dependencies manually:"
|
||||
echo "1. Install Visual Studio Build Tools"
|
||||
echo "2. Install vcpkg and packages: sdl2, yaml-cpp"
|
||||
echo "3. Install Ninja from https://ninja-build.org/"
|
||||
;;
|
||||
*)
|
||||
print_warning "Unknown OS. Please install dependencies manually."
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Setup repository
|
||||
setup_repository() {
|
||||
print_header "Setting up Repository"
|
||||
|
||||
# Check if we're in a git repository
|
||||
if [ ! -d ".git" ]; then
|
||||
print_error "Not in a git repository. Please run this script from the YAZE root directory."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Update submodules
|
||||
print_success "Updating submodules..."
|
||||
git submodule update --init --recursive
|
||||
|
||||
# Check for uncommitted changes
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
print_warning "You have uncommitted changes. Consider committing or stashing them."
|
||||
fi
|
||||
}
|
||||
|
||||
# Configure IDE
|
||||
configure_ide() {
|
||||
print_header "Configuring IDE"
|
||||
|
||||
# Generate compile_commands.json
|
||||
print_success "Generating compile_commands.json..."
|
||||
cmake -B build -G Ninja -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||
|
||||
# Create VS Code settings
|
||||
if [ ! -d ".vscode" ]; then
|
||||
mkdir -p .vscode
|
||||
print_success "Creating VS Code configuration..."
|
||||
|
||||
cat > .vscode/settings.json << EOF
|
||||
{
|
||||
"C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
|
||||
"C_Cpp.default.compileCommands": "\${workspaceFolder}/build/compile_commands.json",
|
||||
"C_Cpp.default.intelliSenseMode": "macos-clang-x64",
|
||||
"files.associations": {
|
||||
"*.h": "c",
|
||||
"*.cc": "cpp"
|
||||
},
|
||||
"cmake.buildDirectory": "\${workspaceFolder}/build",
|
||||
"cmake.configureOnOpen": true,
|
||||
"cmake.useCMakePresets": "always",
|
||||
"cmake.generator": "Ninja Multi-Config",
|
||||
"cmake.preferredGenerators": [
|
||||
"Ninja Multi-Config",
|
||||
"Ninja",
|
||||
"Unix Makefiles"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > .vscode/tasks.json << EOF
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "CMake: Configure (dev)",
|
||||
"type": "shell",
|
||||
"command": "cmake",
|
||||
"args": ["--preset", "dev"],
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "CMake: Build (dev)",
|
||||
"type": "shell",
|
||||
"command": "cmake",
|
||||
"args": ["--build", "--preset", "dev"],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": ["\$gcc"],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "CMake: Clean Build",
|
||||
"type": "shell",
|
||||
"command": "cmake",
|
||||
"args": ["--build", "--preset", "dev", "--target", "clean"],
|
||||
"group": "build",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "CMake: Run Tests",
|
||||
"type": "shell",
|
||||
"command": "ctest",
|
||||
"args": ["--preset", "stable", "--output-on-failure"],
|
||||
"group": "test",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
# Run first build
|
||||
run_first_build() {
|
||||
print_header "Running First Build"
|
||||
|
||||
# Configure project
|
||||
print_success "Configuring project..."
|
||||
cmake --preset dev
|
||||
|
||||
# Build project
|
||||
print_success "Building project..."
|
||||
cmake --build build
|
||||
|
||||
# Check if build succeeded
|
||||
if [ -f "build/bin/yaze" ] || [ -f "build/bin/yaze.exe" ]; then
|
||||
print_success "Build successful! YAZE executable created."
|
||||
else
|
||||
print_error "Build failed. Check the output above for errors."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run tests
|
||||
run_tests() {
|
||||
print_header "Running Tests"
|
||||
|
||||
print_success "Running test suite..."
|
||||
cd build
|
||||
ctest --output-on-failure -L stable || print_warning "Some tests failed (this is normal for first setup)"
|
||||
cd ..
|
||||
}
|
||||
|
||||
# Print next steps
|
||||
print_next_steps() {
|
||||
print_header "Setup Complete!"
|
||||
|
||||
echo -e "${GREEN}✓ YAZE development environment is ready!${NC}"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo "1. Run the application:"
|
||||
echo " ./build/bin/yaze"
|
||||
echo ""
|
||||
echo "2. Run tests:"
|
||||
echo " cd build && ctest"
|
||||
echo ""
|
||||
echo "3. Format code:"
|
||||
echo " cmake --build build --target yaze-format"
|
||||
echo ""
|
||||
echo "4. Check formatting:"
|
||||
echo " cmake --build build --target yaze-format-check"
|
||||
echo ""
|
||||
echo "5. Read the documentation:"
|
||||
echo " docs/BUILD.md"
|
||||
echo ""
|
||||
echo "Happy coding! 🎮"
|
||||
}
|
||||
|
||||
# Main function
|
||||
main() {
|
||||
print_header "YAZE Developer Setup"
|
||||
echo "This script will set up your YAZE development environment."
|
||||
echo ""
|
||||
|
||||
detect_os
|
||||
check_prerequisites
|
||||
install_dependencies
|
||||
setup_repository
|
||||
configure_ide
|
||||
run_first_build
|
||||
run_tests
|
||||
print_next_steps
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
|
||||
268
scripts/extract-symbols.sh
Executable file
268
scripts/extract-symbols.sh
Executable file
@@ -0,0 +1,268 @@
|
||||
#!/bin/bash
|
||||
# Symbol Extraction Tool - Extract symbols from compiled object files
|
||||
# Creates a JSON database of all symbols and their defining object files
|
||||
#
|
||||
# Usage: ./scripts/extract-symbols.sh [BUILD_DIR] [OUTPUT_FILE]
|
||||
# BUILD_DIR: Path to CMake build directory (default: build)
|
||||
# OUTPUT_FILE: Path to output JSON file (default: build/symbol_database.json)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
BUILD_DIR="${1:-.}"
|
||||
OUTPUT_FILE="${2:-${BUILD_DIR}/symbol_database.json}"
|
||||
TEMP_SYMBOLS="${BUILD_DIR}/.temp_symbols.txt"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
|
||||
|
||||
# Platform detection
|
||||
UNAME_S=$(uname -s)
|
||||
IS_MACOS=false
|
||||
IS_LINUX=false
|
||||
IS_WINDOWS=false
|
||||
|
||||
case "${UNAME_S}" in
|
||||
Darwin*) IS_MACOS=true ;;
|
||||
Linux*) IS_LINUX=true ;;
|
||||
MINGW*|MSYS*|CYGWIN*) IS_WINDOWS=true ;;
|
||||
esac
|
||||
|
||||
# Validation
|
||||
if [[ ! -d "${BUILD_DIR}" ]]; then
|
||||
echo -e "${RED}Error: Build directory not found: ${BUILD_DIR}${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}=== Symbol Extraction Tool ===${NC}"
|
||||
echo -e "Build directory: ${BUILD_DIR}"
|
||||
echo -e "Output file: ${OUTPUT_FILE}"
|
||||
echo ""
|
||||
|
||||
# Function to extract symbols using nm (Unix/macOS)
|
||||
extract_symbols_unix() {
|
||||
local obj_file="$1"
|
||||
local obj_name="${obj_file##*/}"
|
||||
|
||||
if ! nm -P "${obj_file}" 2>/dev/null | while read -r sym rest; do
|
||||
# Filter out special symbols and undefined references
|
||||
if [[ -n "${sym}" ]] && [[ "${rest}" != *"U"* ]]; then
|
||||
# Get symbol type (T=text, D=data, R=read-only, etc.)
|
||||
local sym_type=$(echo "${rest}" | awk '{print $1}')
|
||||
if [[ "${sym_type}" != "U" ]]; then
|
||||
echo "${sym}|${obj_name}|${sym_type}"
|
||||
fi
|
||||
fi
|
||||
done; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to extract symbols using dumpbin (Windows)
|
||||
extract_symbols_windows() {
|
||||
local obj_file="$1"
|
||||
local obj_name="${obj_file##*/}"
|
||||
|
||||
# Use dumpbin to extract symbols
|
||||
if dumpbin /symbols "${obj_file}" 2>/dev/null | grep -E "^\s+[0-9A-F]+" | while read -r line; do
|
||||
# Parse dumpbin output
|
||||
local sym=$(echo "${line}" | awk '{print $NF}')
|
||||
if [[ -n "${sym}" ]]; then
|
||||
local sym_type="?" # Windows dumpbin doesn't clearly show type
|
||||
echo "${sym}|${obj_name}|${sym_type}"
|
||||
fi
|
||||
done; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to collect all object files
|
||||
collect_object_files() {
|
||||
local obj_list="${BUILD_DIR}/.object_files.tmp"
|
||||
> "${obj_list}"
|
||||
|
||||
# Find all .o files (Unix/macOS) and .obj files (Windows)
|
||||
if ${IS_WINDOWS}; then
|
||||
find "${BUILD_DIR}" -type f \( -name "*.obj" -o -name "*.o" \) 2>/dev/null >> "${obj_list}" || true
|
||||
else
|
||||
find "${BUILD_DIR}" -type f -name "*.o" 2>/dev/null >> "${obj_list}" || true
|
||||
fi
|
||||
|
||||
echo "${obj_list}"
|
||||
}
|
||||
|
||||
# Extract symbols from all object files
|
||||
echo -e "${BLUE}Scanning for object files...${NC}"
|
||||
OBJ_LIST=$(collect_object_files)
|
||||
OBJ_COUNT=$(wc -l < "${OBJ_LIST}")
|
||||
|
||||
if [[ ${OBJ_COUNT} -eq 0 ]]; then
|
||||
echo -e "${YELLOW}Warning: No object files found in ${BUILD_DIR}${NC}"
|
||||
echo "Make sure to build the project first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "Found ${GREEN}${OBJ_COUNT}${NC} object files"
|
||||
echo ""
|
||||
echo -e "${BLUE}Extracting symbols (this may take a moment)...${NC}"
|
||||
|
||||
# Process object files and extract symbols
|
||||
: > "${TEMP_SYMBOLS}"
|
||||
PROCESSED=0
|
||||
FAILED=0
|
||||
|
||||
while IFS= read -r obj_file; do
|
||||
[[ -z "${obj_file}" ]] && continue
|
||||
|
||||
if [[ ! -f "${obj_file}" ]]; then
|
||||
echo -e "${YELLOW}Skipping (not found): ${obj_file}${NC}"
|
||||
((FAILED++))
|
||||
continue
|
||||
fi
|
||||
|
||||
# Extract symbols based on platform
|
||||
if ${IS_WINDOWS}; then
|
||||
if extract_symbols_windows "${obj_file}" >> "${TEMP_SYMBOLS}" 2>/dev/null; then
|
||||
((PROCESSED++))
|
||||
else
|
||||
((FAILED++))
|
||||
fi
|
||||
else
|
||||
if extract_symbols_unix "${obj_file}" >> "${TEMP_SYMBOLS}" 2>/dev/null; then
|
||||
((PROCESSED++))
|
||||
else
|
||||
((FAILED++))
|
||||
fi
|
||||
fi
|
||||
|
||||
# Progress indicator
|
||||
if (( PROCESSED % 50 == 0 )); then
|
||||
echo -ne "\r Processed: ${PROCESSED}/${OBJ_COUNT} objects"
|
||||
fi
|
||||
done < "${OBJ_LIST}"
|
||||
|
||||
echo -ne "\r Processed: ${GREEN}${PROCESSED}${NC}/${OBJ_COUNT} objects (${FAILED} failed) \n"
|
||||
echo ""
|
||||
|
||||
# Generate JSON output
|
||||
echo -e "${BLUE}Generating symbol database...${NC}"
|
||||
|
||||
# Start JSON
|
||||
cat > "${OUTPUT_FILE}" << 'EOF'
|
||||
{
|
||||
"metadata": {
|
||||
"platform": "",
|
||||
"build_dir": "",
|
||||
"timestamp": "",
|
||||
"object_files_scanned": 0,
|
||||
"total_symbols": 0
|
||||
},
|
||||
"conflicts": [],
|
||||
"symbols": {}
|
||||
}
|
||||
EOF
|
||||
|
||||
# Use Python to generate proper JSON (more portable than jq)
|
||||
python3 << PYTHON_EOF
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
|
||||
# Read extracted symbols
|
||||
symbol_dict = defaultdict(list)
|
||||
total_symbols = 0
|
||||
|
||||
try:
|
||||
with open("${TEMP_SYMBOLS}", "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = line.split("|")
|
||||
if len(parts) >= 2:
|
||||
symbol = parts[0]
|
||||
obj_file = parts[1]
|
||||
sym_type = parts[2] if len(parts) > 2 else "?"
|
||||
|
||||
symbol_dict[symbol].append({
|
||||
"object_file": obj_file,
|
||||
"type": sym_type
|
||||
})
|
||||
total_symbols += 1
|
||||
except Exception as e:
|
||||
print(f"Error reading symbols: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Identify conflicts (symbols defined in multiple object files)
|
||||
conflicts = []
|
||||
for symbol, definitions in symbol_dict.items():
|
||||
if len(definitions) > 1:
|
||||
conflicts.append({
|
||||
"symbol": symbol,
|
||||
"count": len(definitions),
|
||||
"definitions": definitions
|
||||
})
|
||||
|
||||
# Sort conflicts by count (most duplicated first)
|
||||
conflicts.sort(key=lambda x: x["count"], reverse=True)
|
||||
|
||||
# Build output JSON
|
||||
output = {
|
||||
"metadata": {
|
||||
"platform": "${UNAME_S}",
|
||||
"build_dir": "${BUILD_DIR}",
|
||||
"timestamp": datetime.utcnow().isoformat() + "Z",
|
||||
"object_files_scanned": ${PROCESSED},
|
||||
"total_symbols": total_symbols,
|
||||
"total_conflicts": len(conflicts)
|
||||
},
|
||||
"conflicts": conflicts,
|
||||
"symbols": {}
|
||||
}
|
||||
|
||||
# Add symbols to output (optional - only include conflicted symbols for smaller file)
|
||||
for symbol, definitions in symbol_dict.items():
|
||||
if len(definitions) > 1:
|
||||
output["symbols"][symbol] = definitions
|
||||
|
||||
# Write JSON
|
||||
try:
|
||||
with open("${OUTPUT_FILE}", "w") as f:
|
||||
json.dump(output, f, indent=2)
|
||||
except Exception as e:
|
||||
print(f"Error writing JSON: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Symbol database written to: ${OUTPUT_FILE}")
|
||||
print(f"Total symbols: {total_symbols}")
|
||||
print(f"Conflicts found: {len(conflicts)}")
|
||||
PYTHON_EOF
|
||||
|
||||
# Cleanup
|
||||
rm -f "${TEMP_SYMBOLS}" "${OBJ_LIST}"
|
||||
|
||||
# Report results
|
||||
if [[ -f "${OUTPUT_FILE}" ]]; then
|
||||
echo -e "${GREEN}Success!${NC}"
|
||||
CONFLICT_COUNT=$(python3 -c "import json; f = json.load(open('${OUTPUT_FILE}')); print(f['metadata'].get('total_conflicts', 0))" 2>/dev/null || echo "?")
|
||||
|
||||
if [[ "${CONFLICT_COUNT}" -gt 0 ]]; then
|
||||
echo -e "${YELLOW}Found ${RED}${CONFLICT_COUNT}${YELLOW} symbol conflicts${NC}"
|
||||
exit 1 # Exit with error if conflicts found
|
||||
else
|
||||
echo -e "${GREEN}No symbol conflicts detected!${NC}"
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Failed to generate symbol database${NC}"
|
||||
exit 1
|
||||
fi
|
||||
216
scripts/install-git-hooks.sh
Executable file
216
scripts/install-git-hooks.sh
Executable file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env bash
|
||||
# Git hooks installer for yaze
|
||||
# Installs pre-push hook to run validation before pushing
|
||||
#
|
||||
# Usage:
|
||||
# scripts/install-git-hooks.sh [install|uninstall|status]
|
||||
#
|
||||
# Commands:
|
||||
# install - Install pre-push hook (default)
|
||||
# uninstall - Remove pre-push hook
|
||||
# status - Show hook installation status
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
|
||||
HOOK_DIR="$REPO_ROOT/.git/hooks"
|
||||
HOOK_FILE="$HOOK_DIR/pre-push"
|
||||
HOOK_SCRIPT="$REPO_ROOT/scripts/pre-push.sh"
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
# Check if we're in a git repository
|
||||
check_git_repo() {
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_error "Not in a git repository"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if hook directory exists
|
||||
check_hook_dir() {
|
||||
if [ ! -d "$HOOK_DIR" ]; then
|
||||
print_error "Git hooks directory not found: $HOOK_DIR"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if pre-push script exists
|
||||
check_prepush_script() {
|
||||
if [ ! -f "$HOOK_SCRIPT" ]; then
|
||||
print_error "Pre-push script not found: $HOOK_SCRIPT"
|
||||
print_info "Make sure you're running this from the repository root"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Show hook status
|
||||
show_status() {
|
||||
echo -e "${BLUE}=== Git Hook Status ===${NC}\n"
|
||||
|
||||
if [ -f "$HOOK_FILE" ]; then
|
||||
print_success "Pre-push hook is installed"
|
||||
echo ""
|
||||
echo "Hook location: $HOOK_FILE"
|
||||
echo ""
|
||||
|
||||
# Check if it's our hook
|
||||
if grep -q "scripts/pre-push.sh" "$HOOK_FILE" 2>/dev/null; then
|
||||
print_info "Hook type: yaze validation hook"
|
||||
else
|
||||
print_warning "Hook type: Custom/unknown (not yaze default)"
|
||||
print_info "To reinstall yaze hook, run: scripts/install-git-hooks.sh install"
|
||||
fi
|
||||
else
|
||||
print_warning "Pre-push hook is NOT installed"
|
||||
echo ""
|
||||
print_info "To install, run: scripts/install-git-hooks.sh install"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Pre-push script: $HOOK_SCRIPT"
|
||||
if [ -x "$HOOK_SCRIPT" ]; then
|
||||
print_success "Script is executable"
|
||||
else
|
||||
print_warning "Script is not executable"
|
||||
print_info "Run: chmod +x $HOOK_SCRIPT"
|
||||
fi
|
||||
}
|
||||
|
||||
# Install hook
|
||||
install_hook() {
|
||||
echo -e "${BLUE}=== Installing Pre-Push Hook ===${NC}\n"
|
||||
|
||||
# Backup existing hook if present
|
||||
if [ -f "$HOOK_FILE" ]; then
|
||||
print_warning "Existing hook found"
|
||||
|
||||
# Check if it's our hook
|
||||
if grep -q "scripts/pre-push.sh" "$HOOK_FILE" 2>/dev/null; then
|
||||
print_info "Existing hook is already yaze validation hook"
|
||||
print_info "Updating hook..."
|
||||
else
|
||||
local backup="$HOOK_FILE.backup.$(date +%Y%m%d_%H%M%S)"
|
||||
print_info "Backing up to: $backup"
|
||||
cp "$HOOK_FILE" "$backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create hook
|
||||
cat > "$HOOK_FILE" << 'EOF'
|
||||
#!/usr/bin/env bash
|
||||
# Pre-push hook for yaze
|
||||
# Automatically installed by scripts/install-git-hooks.sh
|
||||
#
|
||||
# To bypass this hook, use: git push --no-verify
|
||||
|
||||
# Get repository root
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${BLUE}Running pre-push validation...${NC}\n"
|
||||
|
||||
# Run validation script
|
||||
if ! "$REPO_ROOT/scripts/pre-push.sh"; then
|
||||
echo ""
|
||||
echo -e "${RED}Pre-push validation failed!${NC}"
|
||||
echo ""
|
||||
echo "Fix the issues above and try again."
|
||||
echo "To bypass this check (not recommended), use: git push --no-verify"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}Pre-push validation passed!${NC}"
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
# Make hook executable
|
||||
chmod +x "$HOOK_FILE"
|
||||
|
||||
print_success "Pre-push hook installed successfully"
|
||||
echo ""
|
||||
print_info "Hook location: $HOOK_FILE"
|
||||
print_info "The hook will run automatically before each push"
|
||||
print_info "To bypass: git push --no-verify (use sparingly)"
|
||||
echo ""
|
||||
print_info "Test the hook with: scripts/pre-push.sh"
|
||||
}
|
||||
|
||||
# Uninstall hook
|
||||
uninstall_hook() {
|
||||
echo -e "${BLUE}=== Uninstalling Pre-Push Hook ===${NC}\n"
|
||||
|
||||
if [ ! -f "$HOOK_FILE" ]; then
|
||||
print_warning "No hook to uninstall"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if it's our hook before removing
|
||||
if grep -q "scripts/pre-push.sh" "$HOOK_FILE" 2>/dev/null; then
|
||||
rm "$HOOK_FILE"
|
||||
print_success "Pre-push hook uninstalled"
|
||||
else
|
||||
print_warning "Hook exists but doesn't appear to be yaze validation hook"
|
||||
print_info "Manual removal required: rm $HOOK_FILE"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
main() {
|
||||
local command="${1:-install}"
|
||||
|
||||
check_git_repo
|
||||
check_hook_dir
|
||||
|
||||
case "$command" in
|
||||
install)
|
||||
check_prepush_script
|
||||
install_hook
|
||||
;;
|
||||
uninstall)
|
||||
uninstall_hook
|
||||
;;
|
||||
status)
|
||||
show_status
|
||||
;;
|
||||
--help|-h|help)
|
||||
grep '^#' "$0" | sed 's/^# \?//'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown command: $command"
|
||||
echo "Use: install, uninstall, status, or --help"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
main "$@"
|
||||
138
scripts/merge_feature.sh
Executable file
138
scripts/merge_feature.sh
Executable file
@@ -0,0 +1,138 @@
|
||||
#!/bin/bash
|
||||
# Quick feature branch merge script for yaze
|
||||
# Merges feature → develop → master, pushes, and cleans up
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Usage
|
||||
if [ $# -lt 1 ]; then
|
||||
echo -e "${RED}Usage: $0 <feature-branch-name>${NC}"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 feature/new-audio-system"
|
||||
echo " $0 fix/memory-leak"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FEATURE_BRANCH="$1"
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}═══════════════════════════════════════${NC}"
|
||||
echo -e "${CYAN} Quick Feature Merge: ${YELLOW}${FEATURE_BRANCH}${NC}"
|
||||
echo -e "${CYAN}═══════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if we're in the right directory
|
||||
if [ ! -f "CMakeLists.txt" ] || [ ! -d ".git" ]; then
|
||||
echo -e "${RED}❌ Error: Not in yaze project root!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Save current branch
|
||||
ORIGINAL_BRANCH=$(git branch --show-current)
|
||||
echo -e "${BLUE}📍 Current branch: ${CYAN}${ORIGINAL_BRANCH}${NC}"
|
||||
|
||||
# Check for uncommitted changes
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
echo -e "${RED}❌ You have uncommitted changes. Please commit or stash first.${NC}"
|
||||
git status --short
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fetch latest
|
||||
echo -e "${BLUE}🔄 Fetching latest from origin...${NC}"
|
||||
git fetch origin
|
||||
|
||||
# Check if feature branch exists
|
||||
if ! git show-ref --verify --quiet "refs/heads/${FEATURE_BRANCH}"; then
|
||||
echo -e "${RED}❌ Branch '${FEATURE_BRANCH}' not found locally${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📊 Commits in ${YELLOW}${FEATURE_BRANCH}${BLUE} not in develop:${NC}"
|
||||
git log develop..${FEATURE_BRANCH} --oneline --decorate | head -10
|
||||
echo ""
|
||||
|
||||
# Step 1: Merge into develop
|
||||
echo -e "${BLUE}[1/5] Merging ${YELLOW}${FEATURE_BRANCH}${BLUE} → ${CYAN}develop${NC}"
|
||||
git checkout develop
|
||||
git pull origin develop --ff-only
|
||||
git merge ${FEATURE_BRANCH} --no-edit
|
||||
|
||||
echo -e "${GREEN}✅ Merged into develop${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 2: Merge develop into master
|
||||
echo -e "${BLUE}[2/5] Merging ${CYAN}develop${BLUE} → ${CYAN}master${NC}"
|
||||
git checkout master
|
||||
git pull origin master --ff-only
|
||||
git merge develop --no-edit
|
||||
|
||||
echo -e "${GREEN}✅ Merged into master${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 3: Push master
|
||||
echo -e "${BLUE}[3/5] Pushing ${CYAN}master${BLUE} to origin...${NC}"
|
||||
git push origin master
|
||||
|
||||
echo -e "${GREEN}✅ Pushed master${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 4: Update and push develop
|
||||
echo -e "${BLUE}[4/5] Syncing ${CYAN}develop${BLUE} with master...${NC}"
|
||||
git checkout develop
|
||||
git merge master --ff-only
|
||||
git push origin develop
|
||||
|
||||
echo -e "${GREEN}✅ Pushed develop${NC}"
|
||||
echo ""
|
||||
|
||||
# Step 5: Delete feature branch
|
||||
echo -e "${BLUE}[5/5] Cleaning up ${YELLOW}${FEATURE_BRANCH}${NC}"
|
||||
git branch -d ${FEATURE_BRANCH}
|
||||
|
||||
# Delete remote branch if it exists
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/${FEATURE_BRANCH}"; then
|
||||
git push origin --delete ${FEATURE_BRANCH}
|
||||
echo -e "${GREEN}✅ Deleted remote branch${NC}"
|
||||
fi
|
||||
|
||||
echo -e "${GREEN}✅ Deleted local branch${NC}"
|
||||
echo ""
|
||||
|
||||
# Return to original branch if it still exists
|
||||
if [ "$ORIGINAL_BRANCH" != "$FEATURE_BRANCH" ]; then
|
||||
if git show-ref --verify --quiet "refs/heads/${ORIGINAL_BRANCH}"; then
|
||||
git checkout ${ORIGINAL_BRANCH}
|
||||
echo -e "${BLUE}📍 Returned to ${CYAN}${ORIGINAL_BRANCH}${NC}"
|
||||
else
|
||||
echo -e "${BLUE}📍 Staying on ${CYAN}develop${NC}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}╔═══════════════════════════════════════╗${NC}"
|
||||
echo -e "${GREEN}║ 🚀 SUCCESS! 🚀 ║${NC}"
|
||||
echo -e "${GREEN}║ Feature merged and deployed ║${NC}"
|
||||
echo -e "${GREEN}╚═══════════════════════════════════════╝${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}What happened:${NC}"
|
||||
echo -e " ✅ ${YELLOW}${FEATURE_BRANCH}${NC} → ${CYAN}develop${NC}"
|
||||
echo -e " ✅ ${CYAN}develop${NC} → ${CYAN}master${NC}"
|
||||
echo -e " ✅ Pushed both branches"
|
||||
echo -e " ✅ Deleted ${YELLOW}${FEATURE_BRANCH}${NC}"
|
||||
echo ""
|
||||
echo -e "${CYAN}Current state:${NC}"
|
||||
git log --oneline --graph --decorate -5
|
||||
echo ""
|
||||
|
||||
89
scripts/package/release.sh
Normal file
89
scripts/package/release.sh
Normal file
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_NAME=${1:-}
|
||||
BUILD_DIR=${2:-}
|
||||
ARTIFACT_NAME=${3:-}
|
||||
|
||||
if [[ -z "$TARGET_NAME" || -z "$BUILD_DIR" || -z "$ARTIFACT_NAME" ]]; then
|
||||
echo "Usage: release.sh <target-name> <build-dir> <artifact-name>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
ROOT_DIR=${GITHUB_WORKSPACE:-$(cd "$SCRIPT_DIR/../.." && pwd)}
|
||||
ARTIFACTS_DIR="$ROOT_DIR/dist"
|
||||
STAGING_DIR=$(mktemp -d)
|
||||
|
||||
mkdir -p "$ARTIFACTS_DIR"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$STAGING_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "Packaging $TARGET_NAME using build output at $BUILD_DIR"
|
||||
|
||||
case "${RUNNER_OS:-$(uname)}" in
|
||||
Windows*)
|
||||
BIN_DIR="$BUILD_DIR/bin/Release"
|
||||
if [[ ! -d "$BIN_DIR" ]]; then
|
||||
echo "::error::Expected Windows binaries under $BIN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp -R "$BIN_DIR" "$STAGING_DIR/bin"
|
||||
cp -R "$ROOT_DIR/assets" "$STAGING_DIR/assets"
|
||||
cp "$ROOT_DIR"/LICENSE "$ROOT_DIR"/README.md "$STAGING_DIR"/
|
||||
ARTIFACT_PATH="$ARTIFACTS_DIR/$ARTIFACT_NAME"
|
||||
pwsh -NoLogo -NoProfile -Command "Compress-Archive -Path '${STAGING_DIR}\*' -DestinationPath '$ARTIFACT_PATH' -Force"
|
||||
;;
|
||||
Darwin)
|
||||
APP_PATH="$BUILD_DIR/bin/yaze.app"
|
||||
if [[ ! -d "$APP_PATH" ]]; then
|
||||
echo "::error::Expected macOS app bundle at $APP_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp -R "$APP_PATH" "$STAGING_DIR/yaze.app"
|
||||
cp "$ROOT_DIR"/LICENSE "$ROOT_DIR"/README.md "$STAGING_DIR"/
|
||||
ARTIFACT_PATH="$ARTIFACTS_DIR/$ARTIFACT_NAME"
|
||||
hdiutil create -fs HFS+ -srcfolder "$STAGING_DIR/yaze.app" -volname "yaze" "$ARTIFACT_PATH"
|
||||
;;
|
||||
Linux*)
|
||||
BIN_DIR="$BUILD_DIR/bin"
|
||||
if [[ ! -d "$BIN_DIR" ]]; then
|
||||
echo "::error::Expected Linux binaries under $BIN_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "$ROOT_DIR"/LICENSE "$ROOT_DIR"/README.md "$STAGING_DIR"/
|
||||
cp -R "$BIN_DIR" "$STAGING_DIR/bin"
|
||||
cp -R "$ROOT_DIR/assets" "$STAGING_DIR/assets"
|
||||
ARTIFACT_PATH="$ARTIFACTS_DIR/$ARTIFACT_NAME"
|
||||
tar -czf "$ARTIFACT_PATH" -C "$STAGING_DIR" .
|
||||
;;
|
||||
*)
|
||||
echo "::error::Unsupported host: ${RUNNER_OS:-$(uname)}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -f "$ARTIFACT_PATH" ]]; then
|
||||
echo "::error::Packaging did not produce $ARTIFACT_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
SHA_CMD="sha256sum"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
SHA_CMD="shasum -a 256"
|
||||
else
|
||||
echo "::warning::No SHA256 utility found; skipping checksum generation" >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CHECKSUM=$($SHA_CMD "$ARTIFACT_PATH" | awk '{print $1}')
|
||||
echo "$CHECKSUM $(basename "$ARTIFACT_PATH")" >> "$ARTIFACTS_DIR/SHA256SUMS.txt"
|
||||
echo "$CHECKSUM" > "$ARTIFACT_PATH.sha256"
|
||||
|
||||
echo "Created artifact $ARTIFACT_PATH"
|
||||
|
||||
354
scripts/pre-push-test.ps1
Normal file
354
scripts/pre-push-test.ps1
Normal file
@@ -0,0 +1,354 @@
|
||||
# Pre-Push Test Script for YAZE (Windows)
|
||||
# Runs fast validation checks before pushing to remote
|
||||
# Catches 90% of CI failures in < 2 minutes
|
||||
|
||||
param(
|
||||
[string]$Preset = "",
|
||||
[string]$BuildDir = "",
|
||||
[switch]$ConfigOnly = $false,
|
||||
[switch]$SmokeOnly = $false,
|
||||
[switch]$SkipSymbols = $false,
|
||||
[switch]$SkipTests = $false,
|
||||
[switch]$Verbose = $false,
|
||||
[switch]$Help = $false
|
||||
)
|
||||
|
||||
# Script configuration
|
||||
$ErrorActionPreference = "Stop"
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ProjectRoot = (Get-Item $ScriptDir).Parent.FullName
|
||||
|
||||
# Default build directory
|
||||
if ($BuildDir -eq "") {
|
||||
$BuildDir = Join-Path $ProjectRoot "build"
|
||||
}
|
||||
|
||||
# Statistics
|
||||
$TotalChecks = 0
|
||||
$PassedChecks = 0
|
||||
$FailedChecks = 0
|
||||
$StartTime = Get-Date
|
||||
|
||||
# Helper functions
|
||||
function Print-Header {
|
||||
param([string]$Message)
|
||||
Write-Host "`n=== $Message ===" -ForegroundColor Blue
|
||||
}
|
||||
|
||||
function Print-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "→ $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Print-Success {
|
||||
param([string]$Message)
|
||||
Write-Host "✓ $Message" -ForegroundColor Green
|
||||
$script:PassedChecks++
|
||||
}
|
||||
|
||||
function Print-Error {
|
||||
param([string]$Message)
|
||||
Write-Host "✗ $Message" -ForegroundColor Red
|
||||
$script:FailedChecks++
|
||||
}
|
||||
|
||||
function Print-Info {
|
||||
param([string]$Message)
|
||||
Write-Host "ℹ $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Get-ElapsedTime {
|
||||
$elapsed = (Get-Date) - $script:StartTime
|
||||
return "{0:N0}s" -f $elapsed.TotalSeconds
|
||||
}
|
||||
|
||||
function Show-Usage {
|
||||
Write-Host @"
|
||||
Usage: .\pre-push-test.ps1 [OPTIONS]
|
||||
|
||||
Pre-push validation script that runs fast checks to catch CI failures early.
|
||||
|
||||
OPTIONS:
|
||||
-Preset NAME Use specific CMake preset (auto-detect if not specified)
|
||||
-BuildDir PATH Build directory (default: build)
|
||||
-ConfigOnly Only validate CMake configuration
|
||||
-SmokeOnly Only run smoke compilation test
|
||||
-SkipSymbols Skip symbol conflict checking
|
||||
-SkipTests Skip running unit tests
|
||||
-Verbose Show detailed output
|
||||
-Help Show this help message
|
||||
|
||||
EXAMPLES:
|
||||
.\pre-push-test.ps1 # Run all checks with auto-detected preset
|
||||
.\pre-push-test.ps1 -Preset win-dbg # Run all checks with specific preset
|
||||
.\pre-push-test.ps1 -ConfigOnly # Only validate CMake configuration
|
||||
.\pre-push-test.ps1 -SmokeOnly # Only compile representative files
|
||||
.\pre-push-test.ps1 -SkipTests # Skip unit tests (faster)
|
||||
|
||||
TIME BUDGET:
|
||||
Config validation: ~10 seconds
|
||||
Smoke compilation: ~90 seconds
|
||||
Symbol checking: ~30 seconds
|
||||
Unit tests: ~30 seconds
|
||||
─────────────────────────────
|
||||
Total (all checks): ~2 minutes
|
||||
|
||||
WHAT THIS CATCHES:
|
||||
✓ CMake configuration errors
|
||||
✓ Missing include paths
|
||||
✓ Header-only compilation issues
|
||||
✓ Symbol conflicts (ODR violations)
|
||||
✓ Unit test failures
|
||||
✓ Platform-specific issues
|
||||
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
# Auto-detect preset if not specified
|
||||
if ($Preset -eq "") {
|
||||
$Preset = "win-dbg"
|
||||
Print-Info "Auto-detected preset: $Preset"
|
||||
}
|
||||
|
||||
Set-Location $ProjectRoot
|
||||
|
||||
Print-Header "YAZE Pre-Push Validation"
|
||||
Print-Info "Preset: $Preset"
|
||||
Print-Info "Build directory: $BuildDir"
|
||||
Print-Info "Time budget: ~2 minutes"
|
||||
Write-Host ""
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 0: Static Analysis
|
||||
# ============================================================================
|
||||
|
||||
Print-Header "Level 0: Static Analysis"
|
||||
$TotalChecks++
|
||||
|
||||
Print-Step "Checking code formatting..."
|
||||
try {
|
||||
if ($Verbose) {
|
||||
cmake --build $BuildDir --target yaze-format-check 2>&1 | Out-Host
|
||||
Print-Success "Code formatting is correct"
|
||||
} else {
|
||||
cmake --build $BuildDir --target yaze-format-check 2>&1 | Out-Null
|
||||
Print-Success "Code formatting is correct"
|
||||
}
|
||||
} catch {
|
||||
Print-Error "Code formatting check failed"
|
||||
Print-Info "Run: cmake --build $BuildDir --target yaze-format"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Skip remaining checks if config-only
|
||||
if ($ConfigOnly) {
|
||||
Print-Header "Summary (Config Only)"
|
||||
Print-Info "Time elapsed: $(Get-ElapsedTime)"
|
||||
Print-Info "Total checks: $TotalChecks"
|
||||
Print-Info "Passed: $PassedChecks"
|
||||
Print-Info "Failed: $FailedChecks"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 1: Configuration Validation
|
||||
# ============================================================================
|
||||
|
||||
Print-Header "Level 1: Configuration Validation"
|
||||
$TotalChecks++
|
||||
|
||||
Print-Step "Validating CMake preset: $Preset"
|
||||
try {
|
||||
cmake --preset $Preset 2>&1 | Out-Null
|
||||
Print-Success "CMake configuration successful"
|
||||
} catch {
|
||||
Print-Error "CMake configuration failed"
|
||||
Print-Info "Run: cmake --preset $Preset (with verbose output)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check for include path issues
|
||||
$TotalChecks++
|
||||
Print-Step "Checking include path propagation..."
|
||||
$cacheFile = Join-Path $BuildDir "CMakeCache.txt"
|
||||
if (Test-Path $cacheFile) {
|
||||
$content = Get-Content $cacheFile -Raw
|
||||
if ($content -match "INCLUDE_DIRECTORIES") {
|
||||
Print-Success "Include paths configured"
|
||||
} else {
|
||||
Print-Error "Include paths not properly configured"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Print-Error "CMakeCache.txt not found"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 2: Smoke Compilation
|
||||
# ============================================================================
|
||||
|
||||
if (-not $SmokeOnly) {
|
||||
Print-Header "Level 2: Smoke Compilation"
|
||||
$TotalChecks++
|
||||
|
||||
Print-Step "Compiling representative files..."
|
||||
Print-Info "This validates headers, includes, and preprocessor directives"
|
||||
|
||||
# List of representative files (one per major library)
|
||||
$SmokeFiles = @(
|
||||
"src/app/rom.cc",
|
||||
"src/app/gfx/bitmap.cc",
|
||||
"src/zelda3/overworld/overworld.cc",
|
||||
"src/cli/service/resources/resource_catalog.cc"
|
||||
)
|
||||
|
||||
$SmokeFailed = $false
|
||||
foreach ($file in $SmokeFiles) {
|
||||
$fullPath = Join-Path $ProjectRoot $file
|
||||
if (-not (Test-Path $fullPath)) {
|
||||
Print-Info "Skipping $file (not found)"
|
||||
continue
|
||||
}
|
||||
|
||||
# Get object file name
|
||||
$objFile = [System.IO.Path]::GetFileNameWithoutExtension($file) + ".obj"
|
||||
|
||||
try {
|
||||
if ($Verbose) {
|
||||
Print-Step " Compiling $file"
|
||||
cmake --build $BuildDir --target $objFile --config Debug 2>&1 | Out-Host
|
||||
Print-Success " ✓ $file"
|
||||
} else {
|
||||
cmake --build $BuildDir --target $objFile --config Debug 2>&1 | Out-Null
|
||||
Print-Success " ✓ $file"
|
||||
}
|
||||
} catch {
|
||||
Print-Error " ✗ $file"
|
||||
if (-not $Verbose) {
|
||||
Print-Info "Run with -Verbose for details"
|
||||
}
|
||||
$SmokeFailed = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SmokeFailed) {
|
||||
Print-Success "Smoke compilation successful"
|
||||
} else {
|
||||
Print-Error "Smoke compilation failed"
|
||||
Print-Info "Run: cmake --build $BuildDir -v (for verbose output)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 3: Symbol Validation
|
||||
# ============================================================================
|
||||
|
||||
if (-not $SkipSymbols) {
|
||||
Print-Header "Level 3: Symbol Validation"
|
||||
$TotalChecks++
|
||||
|
||||
Print-Step "Checking for symbol conflicts..."
|
||||
Print-Info "This detects ODR violations and duplicate symbols"
|
||||
|
||||
$symbolScript = Join-Path $ScriptDir "verify-symbols.ps1"
|
||||
if (Test-Path $symbolScript) {
|
||||
try {
|
||||
if ($Verbose) {
|
||||
& $symbolScript -BuildDir $BuildDir 2>&1 | Out-Host
|
||||
Print-Success "No symbol conflicts detected"
|
||||
} else {
|
||||
& $symbolScript -BuildDir $BuildDir 2>&1 | Out-Null
|
||||
Print-Success "No symbol conflicts detected"
|
||||
}
|
||||
} catch {
|
||||
Print-Error "Symbol conflicts detected"
|
||||
Print-Info "Run: .\scripts\verify-symbols.ps1 -BuildDir $BuildDir"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Print-Info "Symbol checker not found (skipping)"
|
||||
Print-Info "Create: scripts\verify-symbols.ps1"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 4: Unit Tests
|
||||
# ============================================================================
|
||||
|
||||
if (-not $SkipTests) {
|
||||
Print-Header "Level 4: Unit Tests"
|
||||
$TotalChecks++
|
||||
|
||||
Print-Step "Running unit tests..."
|
||||
Print-Info "This validates component logic"
|
||||
|
||||
# Find test binary
|
||||
$TestBinary = Join-Path $BuildDir "bin\Debug\yaze_test.exe"
|
||||
if (-not (Test-Path $TestBinary)) {
|
||||
$TestBinary = Join-Path $BuildDir "bin\yaze_test.exe"
|
||||
}
|
||||
if (-not (Test-Path $TestBinary)) {
|
||||
$TestBinary = Join-Path $BuildDir "bin\RelWithDebInfo\yaze_test.exe"
|
||||
}
|
||||
|
||||
if (-not (Test-Path $TestBinary)) {
|
||||
Print-Info "Test binary not found, building..."
|
||||
try {
|
||||
cmake --build $BuildDir --target yaze_test --config Debug 2>&1 | Out-Null
|
||||
Print-Success "Test binary built"
|
||||
} catch {
|
||||
Print-Error "Failed to build test binary"
|
||||
exit 1
|
||||
}
|
||||
# Try finding it again
|
||||
$TestBinary = Join-Path $BuildDir "bin\Debug\yaze_test.exe"
|
||||
}
|
||||
|
||||
if (Test-Path $TestBinary) {
|
||||
try {
|
||||
if ($Verbose) {
|
||||
& $TestBinary --unit 2>&1 | Out-Host
|
||||
Print-Success "All unit tests passed"
|
||||
} else {
|
||||
& $TestBinary --unit 2>&1 | Out-Null
|
||||
Print-Success "All unit tests passed"
|
||||
}
|
||||
} catch {
|
||||
Print-Error "Unit tests failed"
|
||||
Print-Info "Run: $TestBinary --unit (for detailed output)"
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Print-Error "Test binary not found at: $TestBinary"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Summary
|
||||
# ============================================================================
|
||||
|
||||
Print-Header "Summary"
|
||||
$ElapsedSeconds = (Get-Date) - $StartTime | Select-Object -ExpandProperty TotalSeconds
|
||||
|
||||
Print-Info "Time elapsed: $([math]::Round($ElapsedSeconds, 0))s"
|
||||
Print-Info "Total checks: $TotalChecks"
|
||||
Print-Info "Passed: $PassedChecks"
|
||||
Print-Info "Failed: $FailedChecks"
|
||||
|
||||
if ($FailedChecks -eq 0) {
|
||||
Write-Host ""
|
||||
Print-Success "All checks passed! Safe to push."
|
||||
exit 0
|
||||
} else {
|
||||
Write-Host ""
|
||||
Print-Error "Some checks failed. Please fix before pushing."
|
||||
exit 1
|
||||
}
|
||||
399
scripts/pre-push-test.sh
Executable file
399
scripts/pre-push-test.sh
Executable file
@@ -0,0 +1,399 @@
|
||||
#!/bin/bash
|
||||
# Pre-Push Test Script for YAZE
|
||||
# Runs fast validation checks before pushing to remote
|
||||
# Catches 90% of CI failures in < 2 minutes
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Configuration
|
||||
BUILD_DIR="${BUILD_DIR:-$PROJECT_ROOT/build}"
|
||||
PRESET="${PRESET:-}"
|
||||
CONFIG_ONLY="${CONFIG_ONLY:-0}"
|
||||
SMOKE_ONLY="${SMOKE_ONLY:-0}"
|
||||
SKIP_SYMBOLS="${SKIP_SYMBOLS:-0}"
|
||||
SKIP_TESTS="${SKIP_TESTS:-0}"
|
||||
VERBOSE="${VERBOSE:-0}"
|
||||
|
||||
# Statistics
|
||||
TOTAL_CHECKS=0
|
||||
PASSED_CHECKS=0
|
||||
FAILED_CHECKS=0
|
||||
START_TIME=$(date +%s)
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}===${NC} $1 ${BLUE}===${NC}"
|
||||
}
|
||||
|
||||
print_step() {
|
||||
echo -e "${YELLOW}→${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
((PASSED_CHECKS++))
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
((FAILED_CHECKS++))
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
check_command() {
|
||||
if ! command -v "$1" &> /dev/null; then
|
||||
print_error "$1 not found. Please install it."
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
elapsed_time() {
|
||||
local END_TIME=$(date +%s)
|
||||
local ELAPSED=$((END_TIME - START_TIME))
|
||||
echo "${ELAPSED}s"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Pre-push validation script that runs fast checks to catch CI failures early.
|
||||
|
||||
OPTIONS:
|
||||
--preset NAME Use specific CMake preset (auto-detect if not specified)
|
||||
--build-dir PATH Build directory (default: build)
|
||||
--config-only Only validate CMake configuration
|
||||
--smoke-only Only run smoke compilation test
|
||||
--skip-symbols Skip symbol conflict checking
|
||||
--skip-tests Skip running unit tests
|
||||
--verbose Show detailed output
|
||||
-h, --help Show this help message
|
||||
|
||||
EXAMPLES:
|
||||
$0 # Run all checks with auto-detected preset
|
||||
$0 --preset mac-dbg # Run all checks with specific preset
|
||||
$0 --config-only # Only validate CMake configuration
|
||||
$0 --smoke-only # Only compile representative files
|
||||
$0 --skip-tests # Skip unit tests (faster)
|
||||
|
||||
TIME BUDGET:
|
||||
Config validation: ~10 seconds
|
||||
Smoke compilation: ~90 seconds
|
||||
Symbol checking: ~30 seconds
|
||||
Unit tests: ~30 seconds
|
||||
─────────────────────────────
|
||||
Total (all checks): ~2 minutes
|
||||
|
||||
WHAT THIS CATCHES:
|
||||
✓ CMake configuration errors
|
||||
✓ Missing include paths
|
||||
✓ Header-only compilation issues
|
||||
✓ Symbol conflicts (ODR violations)
|
||||
✓ Unit test failures
|
||||
✓ Platform-specific issues
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--preset)
|
||||
PRESET="$2"
|
||||
shift 2
|
||||
;;
|
||||
--build-dir)
|
||||
BUILD_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--config-only)
|
||||
CONFIG_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--smoke-only)
|
||||
SMOKE_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--skip-symbols)
|
||||
SKIP_SYMBOLS=1
|
||||
shift
|
||||
;;
|
||||
--skip-tests)
|
||||
SKIP_TESTS=1
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Auto-detect preset if not specified
|
||||
if [[ -z "$PRESET" ]]; then
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
PRESET="mac-dbg"
|
||||
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
|
||||
PRESET="lin-dbg"
|
||||
else
|
||||
print_error "Unsupported platform: $OSTYPE"
|
||||
print_info "Please specify --preset manually"
|
||||
exit 1
|
||||
fi
|
||||
print_info "Auto-detected preset: $PRESET"
|
||||
fi
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
print_header "YAZE Pre-Push Validation"
|
||||
print_info "Preset: $PRESET"
|
||||
print_info "Build directory: $BUILD_DIR"
|
||||
print_info "Time budget: ~2 minutes"
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 0: Static Analysis
|
||||
# ============================================================================
|
||||
|
||||
print_header "Level 0: Static Analysis"
|
||||
((TOTAL_CHECKS++))
|
||||
|
||||
print_step "Checking code formatting..."
|
||||
if [[ $VERBOSE -eq 1 ]]; then
|
||||
if cmake --build "$BUILD_DIR" --target yaze-format-check 2>&1; then
|
||||
print_success "Code formatting is correct"
|
||||
else
|
||||
print_error "Code formatting check failed"
|
||||
print_info "Run: cmake --build $BUILD_DIR --target yaze-format"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if cmake --build "$BUILD_DIR" --target yaze-format-check > /dev/null 2>&1; then
|
||||
print_success "Code formatting is correct"
|
||||
else
|
||||
print_error "Code formatting check failed"
|
||||
print_info "Run: cmake --build $BUILD_DIR --target yaze-format"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Skip remaining checks if config-only
|
||||
if [[ $CONFIG_ONLY -eq 1 ]]; then
|
||||
print_header "Summary (Config Only)"
|
||||
print_info "Time elapsed: $(elapsed_time)"
|
||||
print_info "Total checks: $TOTAL_CHECKS"
|
||||
print_info "Passed: ${GREEN}$PASSED_CHECKS${NC}"
|
||||
print_info "Failed: ${RED}$FAILED_CHECKS${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 1: Configuration Validation
|
||||
# ============================================================================
|
||||
|
||||
print_header "Level 1: Configuration Validation"
|
||||
((TOTAL_CHECKS++))
|
||||
|
||||
print_step "Validating CMake preset: $PRESET"
|
||||
if cmake --preset "$PRESET" -DCMAKE_VERBOSE_MAKEFILE=OFF > /dev/null 2>&1; then
|
||||
print_success "CMake configuration successful"
|
||||
else
|
||||
print_error "CMake configuration failed"
|
||||
print_info "Run: cmake --preset $PRESET (with verbose output)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for include path issues
|
||||
((TOTAL_CHECKS++))
|
||||
print_step "Checking include path propagation..."
|
||||
if [[ $VERBOSE -eq 1 ]]; then
|
||||
if grep -q "INCLUDE_DIRECTORIES" "$BUILD_DIR/CMakeCache.txt"; then
|
||||
print_success "Include paths configured"
|
||||
else
|
||||
print_error "Include paths not properly configured"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if grep -q "INCLUDE_DIRECTORIES" "$BUILD_DIR/CMakeCache.txt" 2>/dev/null; then
|
||||
print_success "Include paths configured"
|
||||
else
|
||||
print_error "Include paths not properly configured"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 2: Smoke Compilation
|
||||
# ============================================================================
|
||||
|
||||
if [[ $SMOKE_ONLY -eq 0 ]]; then
|
||||
print_header "Level 2: Smoke Compilation"
|
||||
((TOTAL_CHECKS++))
|
||||
|
||||
print_step "Compiling representative files..."
|
||||
print_info "This validates headers, includes, and preprocessor directives"
|
||||
|
||||
# List of representative files (one per major library)
|
||||
SMOKE_FILES=(
|
||||
"src/app/rom.cc"
|
||||
"src/app/gfx/bitmap.cc"
|
||||
"src/zelda3/overworld/overworld.cc"
|
||||
"src/cli/service/resources/resource_catalog.cc"
|
||||
)
|
||||
|
||||
SMOKE_FAILED=0
|
||||
for file in "${SMOKE_FILES[@]}"; do
|
||||
if [[ ! -f "$PROJECT_ROOT/$file" ]]; then
|
||||
print_info "Skipping $file (not found)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Get object file path
|
||||
OBJ_FILE="$BUILD_DIR/$(echo "$file" | sed 's/src\///' | sed 's/\.cc$/.cc.o/')"
|
||||
|
||||
if [[ $VERBOSE -eq 1 ]]; then
|
||||
print_step " Compiling $file"
|
||||
if cmake --build "$BUILD_DIR" --target "$(basename "$OBJ_FILE")" 2>&1; then
|
||||
print_success " ✓ $file"
|
||||
else
|
||||
print_error " ✗ $file"
|
||||
SMOKE_FAILED=1
|
||||
fi
|
||||
else
|
||||
if cmake --build "$BUILD_DIR" --target "$(basename "$OBJ_FILE")" > /dev/null 2>&1; then
|
||||
print_success " ✓ $file"
|
||||
else
|
||||
print_error " ✗ $file (run with --verbose for details)"
|
||||
SMOKE_FAILED=1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $SMOKE_FAILED -eq 0 ]]; then
|
||||
print_success "Smoke compilation successful"
|
||||
else
|
||||
print_error "Smoke compilation failed"
|
||||
print_info "Run: cmake --build $BUILD_DIR -v (for verbose output)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 3: Symbol Validation
|
||||
# ============================================================================
|
||||
|
||||
if [[ $SKIP_SYMBOLS -eq 0 ]]; then
|
||||
print_header "Level 3: Symbol Validation"
|
||||
((TOTAL_CHECKS++))
|
||||
|
||||
print_step "Checking for symbol conflicts..."
|
||||
print_info "This detects ODR violations and duplicate symbols"
|
||||
|
||||
if [[ -x "$SCRIPT_DIR/verify-symbols.sh" ]]; then
|
||||
if [[ $VERBOSE -eq 1 ]]; then
|
||||
if "$SCRIPT_DIR/verify-symbols.sh" --build-dir "$BUILD_DIR"; then
|
||||
print_success "No symbol conflicts detected"
|
||||
else
|
||||
print_error "Symbol conflicts detected"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if "$SCRIPT_DIR/verify-symbols.sh" --build-dir "$BUILD_DIR" > /dev/null 2>&1; then
|
||||
print_success "No symbol conflicts detected"
|
||||
else
|
||||
print_error "Symbol conflicts detected"
|
||||
print_info "Run: ./scripts/verify-symbols.sh --build-dir $BUILD_DIR"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
print_info "Symbol checker not found (skipping)"
|
||||
print_info "Create: scripts/verify-symbols.sh"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# LEVEL 4: Unit Tests
|
||||
# ============================================================================
|
||||
|
||||
if [[ $SKIP_TESTS -eq 0 ]]; then
|
||||
print_header "Level 4: Unit Tests"
|
||||
((TOTAL_CHECKS++))
|
||||
|
||||
print_step "Running unit tests..."
|
||||
print_info "This validates component logic"
|
||||
|
||||
TEST_BINARY="$BUILD_DIR/bin/yaze_test"
|
||||
if [[ ! -x "$TEST_BINARY" ]]; then
|
||||
print_info "Test binary not found, building..."
|
||||
if cmake --build "$BUILD_DIR" --target yaze_test > /dev/null 2>&1; then
|
||||
print_success "Test binary built"
|
||||
else
|
||||
print_error "Failed to build test binary"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $VERBOSE -eq 1 ]]; then
|
||||
if "$TEST_BINARY" --unit 2>&1; then
|
||||
print_success "All unit tests passed"
|
||||
else
|
||||
print_error "Unit tests failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if "$TEST_BINARY" --unit > /dev/null 2>&1; then
|
||||
print_success "All unit tests passed"
|
||||
else
|
||||
print_error "Unit tests failed"
|
||||
print_info "Run: $TEST_BINARY --unit (for detailed output)"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Summary
|
||||
# ============================================================================
|
||||
|
||||
print_header "Summary"
|
||||
END_TIME=$(date +%s)
|
||||
ELAPSED=$((END_TIME - START_TIME))
|
||||
|
||||
print_info "Time elapsed: ${ELAPSED}s"
|
||||
print_info "Total checks: $TOTAL_CHECKS"
|
||||
print_info "Passed: ${GREEN}$PASSED_CHECKS${NC}"
|
||||
print_info "Failed: ${RED}$FAILED_CHECKS${NC}"
|
||||
|
||||
if [[ $FAILED_CHECKS -eq 0 ]]; then
|
||||
echo ""
|
||||
print_success "All checks passed! Safe to push."
|
||||
exit 0
|
||||
else
|
||||
echo ""
|
||||
print_error "Some checks failed. Please fix before pushing."
|
||||
exit 1
|
||||
fi
|
||||
210
scripts/pre-push.sh
Executable file
210
scripts/pre-push.sh
Executable file
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pre-push validation script for yaze
|
||||
# Runs fast checks before pushing to catch common issues early
|
||||
#
|
||||
# Usage:
|
||||
# scripts/pre-push.sh [--skip-tests] [--skip-format]
|
||||
#
|
||||
# Options:
|
||||
# --skip-tests Skip running unit tests
|
||||
# --skip-format Skip code formatting check
|
||||
# --skip-build Skip build verification
|
||||
# --help Show this help message
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 - All checks passed
|
||||
# 1 - Build failed
|
||||
# 2 - Tests failed
|
||||
# 3 - Format check failed
|
||||
# 4 - Configuration error
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
SKIP_TESTS=false
|
||||
SKIP_FORMAT=false
|
||||
SKIP_BUILD=false
|
||||
BUILD_DIR="build"
|
||||
TEST_TIMEOUT=120 # 2 minutes max for tests
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--skip-tests)
|
||||
SKIP_TESTS=true
|
||||
shift
|
||||
;;
|
||||
--skip-format)
|
||||
SKIP_FORMAT=true
|
||||
shift
|
||||
;;
|
||||
--skip-build)
|
||||
SKIP_BUILD=true
|
||||
shift
|
||||
;;
|
||||
--help)
|
||||
grep '^#' "$0" | sed 's/^# \?//'
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo -e "${RED}❌ Unknown option: $1${NC}"
|
||||
echo "Use --help for usage information"
|
||||
exit 4
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}===${NC} $1 ${BLUE}===${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✅ $1${NC}"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}❌ $1${NC}"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠️ $1${NC}"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ️ $1${NC}"
|
||||
}
|
||||
|
||||
# Detect platform
|
||||
detect_platform() {
|
||||
case "$(uname -s)" in
|
||||
Darwin*) echo "mac" ;;
|
||||
Linux*) echo "lin" ;;
|
||||
MINGW*|MSYS*|CYGWIN*) echo "win" ;;
|
||||
*) echo "unknown" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Get appropriate preset for platform
|
||||
get_preset() {
|
||||
local platform=$1
|
||||
if [ -f "$BUILD_DIR/CMakeCache.txt" ]; then
|
||||
# Extract preset from existing build
|
||||
grep "CMAKE_PROJECT_NAME" "$BUILD_DIR/CMakeCache.txt" >/dev/null 2>&1 && echo "existing" && return
|
||||
fi
|
||||
|
||||
# Use platform default debug preset
|
||||
echo "${platform}-dbg"
|
||||
}
|
||||
|
||||
# Check if CMake is configured
|
||||
check_cmake_configured() {
|
||||
if [ ! -f "$BUILD_DIR/CMakeCache.txt" ]; then
|
||||
print_warning "Build directory not configured"
|
||||
print_info "Run: cmake --preset <preset> to configure"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Main script
|
||||
main() {
|
||||
print_header "Pre-Push Validation"
|
||||
|
||||
local platform
|
||||
platform=$(detect_platform)
|
||||
print_info "Detected platform: $platform"
|
||||
|
||||
# Check for git repository
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_error "Not in a git repository"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
local start_time
|
||||
start_time=$(date +%s)
|
||||
|
||||
# 1. Build Verification
|
||||
if [ "$SKIP_BUILD" = false ]; then
|
||||
print_header "Step 1/3: Build Verification"
|
||||
|
||||
if ! check_cmake_configured; then
|
||||
print_error "Build not configured. Skipping build check."
|
||||
print_info "Configure with: cmake --preset ${platform}-dbg"
|
||||
exit 4
|
||||
fi
|
||||
|
||||
print_info "Building yaze_test target..."
|
||||
if ! cmake --build "$BUILD_DIR" --target yaze_test 2>&1 | tail -20; then
|
||||
print_error "Build failed!"
|
||||
print_info "Fix build errors and try again"
|
||||
exit 1
|
||||
fi
|
||||
print_success "Build passed"
|
||||
else
|
||||
print_warning "Skipping build verification (--skip-build)"
|
||||
fi
|
||||
|
||||
# 2. Unit Tests
|
||||
if [ "$SKIP_TESTS" = false ]; then
|
||||
print_header "Step 2/3: Unit Tests"
|
||||
|
||||
local test_binary="$BUILD_DIR/bin/yaze_test"
|
||||
if [ ! -f "$test_binary" ]; then
|
||||
print_error "Test binary not found: $test_binary"
|
||||
print_info "Build tests first: cmake --build $BUILD_DIR --target yaze_test"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
print_info "Running unit tests (timeout: ${TEST_TIMEOUT}s)..."
|
||||
if ! timeout "$TEST_TIMEOUT" "$test_binary" --unit --gtest_brief=1 2>&1; then
|
||||
print_error "Unit tests failed!"
|
||||
print_info "Run tests manually to see details: $test_binary --unit"
|
||||
exit 2
|
||||
fi
|
||||
print_success "Unit tests passed"
|
||||
else
|
||||
print_warning "Skipping unit tests (--skip-tests)"
|
||||
fi
|
||||
|
||||
# 3. Code Formatting
|
||||
if [ "$SKIP_FORMAT" = false ]; then
|
||||
print_header "Step 3/3: Code Formatting"
|
||||
|
||||
# Check if format-check target exists
|
||||
if cmake --build "$BUILD_DIR" --target help 2>/dev/null | grep -q "format-check"; then
|
||||
print_info "Checking code formatting..."
|
||||
if ! cmake --build "$BUILD_DIR" --target format-check 2>&1 | tail -10; then
|
||||
print_error "Code formatting check failed!"
|
||||
print_info "Fix with: cmake --build $BUILD_DIR --target format"
|
||||
exit 3
|
||||
fi
|
||||
print_success "Code formatting passed"
|
||||
else
|
||||
print_warning "format-check target not available, skipping"
|
||||
fi
|
||||
else
|
||||
print_warning "Skipping format check (--skip-format)"
|
||||
fi
|
||||
|
||||
# Summary
|
||||
local end_time
|
||||
end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
|
||||
print_header "Pre-Push Validation Complete"
|
||||
print_success "All checks passed in ${duration}s"
|
||||
print_info "Safe to push!"
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
@@ -65,6 +65,22 @@ if (-not (Test-Command "git")) {
|
||||
|
||||
Write-Status "✓ Git found" "Success"
|
||||
|
||||
# Check for clang-cl
|
||||
if (Test-Command "clang-cl") {
|
||||
$clangVersion = & clang-cl --version 2>&1 | Select-Object -First 1
|
||||
Write-Status "✓ clang-cl detected: $clangVersion" "Success"
|
||||
} else {
|
||||
Write-Status "⚠ clang-cl not found. Install the \"LLVM tools for Visual Studio\" component for faster builds." "Warning"
|
||||
}
|
||||
|
||||
# Check for Ninja
|
||||
if (Test-Command "ninja") {
|
||||
$ninjaVersion = & ninja --version 2>&1
|
||||
Write-Status "✓ Ninja detected: version $ninjaVersion" "Success"
|
||||
} else {
|
||||
Write-Status "⚠ Ninja not found. Install via: choco install ninja (required for win-dbg/win-ai presets)" "Warning"
|
||||
}
|
||||
|
||||
# Clone vcpkg if needed
|
||||
if (-not (Test-Path "vcpkg")) {
|
||||
Write-Status "Cloning vcpkg..." "Warning"
|
||||
@@ -102,6 +118,12 @@ Write-Status "Installing dependencies for triplet: $Triplet" "Warning"
|
||||
& $vcpkgExe install --triplet $Triplet
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Status "✓ Dependencies installed successfully" "Success"
|
||||
$installedPath = "vcpkg\installed\$Triplet"
|
||||
if (Test-Path $installedPath) {
|
||||
Write-Status "✓ Cached packages under $installedPath" "Success"
|
||||
} else {
|
||||
Write-Status "⚠ vcpkg install folder missing (expected $installedPath). Builds may rebuild dependencies on first run." "Warning"
|
||||
}
|
||||
} else {
|
||||
Write-Status "⚠ Some dependencies may not have installed correctly" "Warning"
|
||||
}
|
||||
@@ -112,4 +134,6 @@ Write-Status "========================================" "Info"
|
||||
Write-Status ""
|
||||
Write-Status "You can now build YAZE using:" "Warning"
|
||||
Write-Status " .\scripts\build-windows.ps1" "White"
|
||||
Write-Status ""
|
||||
Write-Status "For ongoing diagnostics run: .\scripts\verify-build-environment.ps1 -FixIssues" "Info"
|
||||
Write-Status ""
|
||||
285
scripts/test-cmake-presets.sh
Executable file
285
scripts/test-cmake-presets.sh
Executable file
@@ -0,0 +1,285 @@
|
||||
#!/bin/bash
|
||||
# CMake Preset Configuration Tester
|
||||
# Tests that all CMake presets can configure successfully
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-cmake-presets.sh [OPTIONS]
|
||||
#
|
||||
# Options:
|
||||
# --parallel N Test N presets in parallel (default: 4)
|
||||
# --preset PRESET Test only specific preset
|
||||
# --platform PLATFORM Test only presets for platform (mac, win, lin)
|
||||
# --quick Skip cleaning between tests
|
||||
# --verbose Show full CMake output
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 - All presets configured successfully
|
||||
# 1 - One or more presets failed
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Default options
|
||||
PARALLEL_JOBS=4
|
||||
SPECIFIC_PRESET=""
|
||||
PLATFORM_FILTER=""
|
||||
QUICK_MODE=false
|
||||
VERBOSE=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--parallel)
|
||||
PARALLEL_JOBS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--preset)
|
||||
SPECIFIC_PRESET="$2"
|
||||
shift 2
|
||||
;;
|
||||
--platform)
|
||||
PLATFORM_FILTER="$2"
|
||||
shift 2
|
||||
;;
|
||||
--quick)
|
||||
QUICK_MODE=true
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Detect current platform
|
||||
detect_platform() {
|
||||
case "$(uname -s)" in
|
||||
Darwin*)
|
||||
echo "mac"
|
||||
;;
|
||||
Linux*)
|
||||
echo "lin"
|
||||
;;
|
||||
MINGW*|MSYS*|CYGWIN*)
|
||||
echo "win"
|
||||
;;
|
||||
*)
|
||||
echo "unknown"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
CURRENT_PLATFORM=$(detect_platform)
|
||||
|
||||
# Get list of presets from CMakePresets.json
|
||||
get_presets() {
|
||||
if [ ! -f "CMakePresets.json" ]; then
|
||||
echo -e "${RED}✗ CMakePresets.json not found${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Use jq if available, otherwise use grep
|
||||
if command -v jq &> /dev/null; then
|
||||
jq -r '.configurePresets[] | select(.hidden != true) | .name' CMakePresets.json
|
||||
else
|
||||
# Fallback to grep-based extraction
|
||||
grep -A 1 '"name":' CMakePresets.json | grep -v '"hidden": true' | grep '"name"' | cut -d'"' -f4
|
||||
fi
|
||||
}
|
||||
|
||||
# Filter presets based on criteria
|
||||
filter_presets() {
|
||||
local presets="$1"
|
||||
local filtered=""
|
||||
|
||||
if [ -n "$SPECIFIC_PRESET" ]; then
|
||||
echo "$SPECIFIC_PRESET"
|
||||
return
|
||||
fi
|
||||
|
||||
for preset in $presets; do
|
||||
# Skip hidden/base presets
|
||||
if [[ "$preset" == *"base"* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Filter by platform if specified
|
||||
if [ -n "$PLATFORM_FILTER" ]; then
|
||||
if [[ ! "$preset" == *"$PLATFORM_FILTER"* ]]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Skip platform-specific presets if not on that platform
|
||||
if [ "$CURRENT_PLATFORM" != "unknown" ]; then
|
||||
if [[ "$preset" == mac-* ]] && [ "$CURRENT_PLATFORM" != "mac" ]; then
|
||||
continue
|
||||
fi
|
||||
if [[ "$preset" == win-* ]] && [ "$CURRENT_PLATFORM" != "win" ]; then
|
||||
continue
|
||||
fi
|
||||
if [[ "$preset" == lin-* ]] && [ "$CURRENT_PLATFORM" != "lin" ]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
filtered="$filtered $preset"
|
||||
done
|
||||
|
||||
echo "$filtered"
|
||||
}
|
||||
|
||||
# Test a single preset
|
||||
test_preset() {
|
||||
local preset="$1"
|
||||
local build_dir="build_preset_test_${preset}"
|
||||
local log_file="preset_test_${preset}.log"
|
||||
|
||||
echo -e "${CYAN}Testing preset: $preset${NC}"
|
||||
|
||||
# Clean build directory unless in quick mode
|
||||
if [ "$QUICK_MODE" = false ] && [ -d "$build_dir" ]; then
|
||||
rm -rf "$build_dir"
|
||||
fi
|
||||
|
||||
# Configure preset
|
||||
local start_time=$(date +%s)
|
||||
|
||||
if [ "$VERBOSE" = true ]; then
|
||||
if cmake --preset "$preset" -B "$build_dir" 2>&1 | tee "$log_file"; then
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
echo -e "${GREEN}✓${NC} $preset configured successfully (${duration}s)"
|
||||
rm -f "$log_file"
|
||||
return 0
|
||||
else
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
echo -e "${RED}✗${NC} $preset failed (${duration}s)"
|
||||
echo " Log saved to: $log_file"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
if cmake --preset "$preset" -B "$build_dir" > "$log_file" 2>&1; then
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
echo -e "${GREEN}✓${NC} $preset configured successfully (${duration}s)"
|
||||
rm -f "$log_file"
|
||||
return 0
|
||||
else
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
echo -e "${RED}✗${NC} $preset failed (${duration}s)"
|
||||
echo " Log saved to: $log_file"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
echo -e "${BLUE}=== CMake Preset Configuration Tester ===${NC}"
|
||||
echo "Platform: $CURRENT_PLATFORM"
|
||||
echo "Parallel jobs: $PARALLEL_JOBS"
|
||||
echo ""
|
||||
|
||||
# Get and filter presets
|
||||
all_presets=$(get_presets)
|
||||
test_presets=$(filter_presets "$all_presets")
|
||||
|
||||
if [ -z "$test_presets" ]; then
|
||||
echo -e "${YELLOW}⚠ No presets to test${NC}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}Presets to test:${NC}"
|
||||
for preset in $test_presets; do
|
||||
echo " - $preset"
|
||||
done
|
||||
echo ""
|
||||
|
||||
# Test presets
|
||||
local total=0
|
||||
local passed=0
|
||||
local failed=0
|
||||
local failed_presets=""
|
||||
|
||||
# Export function for parallel execution
|
||||
export -f test_preset
|
||||
export VERBOSE
|
||||
export QUICK_MODE
|
||||
export RED GREEN YELLOW BLUE CYAN NC
|
||||
|
||||
if [ "$PARALLEL_JOBS" -gt 1 ]; then
|
||||
echo -e "${BLUE}Running tests in parallel (jobs: $PARALLEL_JOBS)...${NC}\n"
|
||||
|
||||
# Use GNU parallel if available, otherwise use xargs
|
||||
if command -v parallel &> /dev/null; then
|
||||
echo "$test_presets" | tr ' ' '\n' | parallel -j "$PARALLEL_JOBS" test_preset
|
||||
else
|
||||
echo "$test_presets" | tr ' ' '\n' | xargs -P "$PARALLEL_JOBS" -I {} bash -c "$(declare -f test_preset); test_preset {}"
|
||||
fi
|
||||
|
||||
# Collect results
|
||||
for preset in $test_presets; do
|
||||
total=$((total + 1))
|
||||
if [ -f "preset_test_${preset}.log" ]; then
|
||||
failed=$((failed + 1))
|
||||
failed_presets="$failed_presets\n - $preset"
|
||||
else
|
||||
passed=$((passed + 1))
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo -e "${BLUE}Running tests sequentially...${NC}\n"
|
||||
|
||||
for preset in $test_presets; do
|
||||
total=$((total + 1))
|
||||
if test_preset "$preset"; then
|
||||
passed=$((passed + 1))
|
||||
else
|
||||
failed=$((failed + 1))
|
||||
failed_presets="$failed_presets\n - $preset"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
echo -e "${BLUE}=== Test Summary ===${NC}"
|
||||
echo "Total presets tested: $total"
|
||||
echo -e "${GREEN}Passed: $passed${NC}"
|
||||
|
||||
if [ $failed -gt 0 ]; then
|
||||
echo -e "${RED}Failed: $failed${NC}"
|
||||
echo -e "${RED}Failed presets:${NC}$failed_presets"
|
||||
echo ""
|
||||
echo "Check log files for details: preset_test_*.log"
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN}✓ All presets configured successfully!${NC}"
|
||||
|
||||
# Cleanup test build directories
|
||||
if [ "$QUICK_MODE" = false ]; then
|
||||
echo "Cleaning up test build directories..."
|
||||
rm -rf build_preset_test_*
|
||||
fi
|
||||
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main
|
||||
main
|
||||
358
scripts/test-config-matrix.sh
Executable file
358
scripts/test-config-matrix.sh
Executable file
@@ -0,0 +1,358 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Configuration Matrix Tester
|
||||
#
|
||||
# Quick local testing of important CMake flag combinations
|
||||
# to catch cross-configuration issues before pushing.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/test-config-matrix.sh # Test all configs
|
||||
# ./scripts/test-config-matrix.sh --config minimal # Test specific config
|
||||
# ./scripts/test-config-matrix.sh --smoke # Fast smoke test only
|
||||
# ./scripts/test-config-matrix.sh --verbose # Verbose output
|
||||
# ./scripts/test-config-matrix.sh --platform linux # Platform-specific
|
||||
#
|
||||
# Environment:
|
||||
# MATRIX_BUILD_DIR: Base directory for builds (default: ./build_matrix)
|
||||
# MATRIX_JOBS: Parallel jobs (default: 4)
|
||||
# MATRIX_CONFIG: Specific configuration to test
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||||
MATRIX_BUILD_DIR="${MATRIX_BUILD_DIR:=${PROJECT_DIR}/build_matrix}"
|
||||
MATRIX_JOBS="${MATRIX_JOBS:=4}"
|
||||
PLATFORM="${PLATFORM:=$(uname -s | tr '[:upper:]' '[:lower:]')}"
|
||||
VERBOSE="${VERBOSE:=0}"
|
||||
SMOKE_ONLY="${SMOKE_ONLY:=0}"
|
||||
SPECIFIC_CONFIG="${SPECIFIC_CONFIG:=}"
|
||||
|
||||
# Test result tracking
|
||||
declare -A RESULTS
|
||||
TOTAL=0
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
|
||||
# ============================================================================
|
||||
# Configuration Definitions
|
||||
# ============================================================================
|
||||
|
||||
# Define each test configuration
|
||||
# Format: config_name|preset_name|cmake_flags|description
|
||||
|
||||
declare -a CONFIGS=(
|
||||
# Tier 1: Core builds (fast, must pass)
|
||||
"minimal|minimal|-DYAZE_ENABLE_GRPC=OFF -DYAZE_ENABLE_AI=OFF -DYAZE_ENABLE_JSON=ON|Minimal build: no AI, no gRPC"
|
||||
"grpc-only|ci-linux|-DYAZE_ENABLE_GRPC=ON -DYAZE_ENABLE_REMOTE_AUTOMATION=OFF -DYAZE_ENABLE_AI_RUNTIME=OFF|gRPC only: automation disabled"
|
||||
"full-ai|ci-linux|-DYAZE_ENABLE_GRPC=ON -DYAZE_ENABLE_REMOTE_AUTOMATION=ON -DYAZE_ENABLE_AI_RUNTIME=ON -DYAZE_ENABLE_JSON=ON|Full AI stack: all features on"
|
||||
|
||||
# Tier 2: Feature combinations
|
||||
"cli-no-grpc|minimal|-DYAZE_ENABLE_GRPC=OFF -DYAZE_BUILD_GUI=OFF -DYAZE_BUILD_EMU=OFF|CLI only: no GUI, no gRPC"
|
||||
"http-api|ci-linux|-DYAZE_ENABLE_GRPC=ON -DYAZE_ENABLE_HTTP_API=ON -DYAZE_ENABLE_JSON=ON|HTTP API: REST endpoints"
|
||||
"no-json|ci-linux|-DYAZE_ENABLE_JSON=OFF -DYAZE_ENABLE_GRPC=ON|No JSON: Ollama only"
|
||||
|
||||
# Tier 3: Edge cases
|
||||
"all-off|minimal|-DYAZE_BUILD_GUI=OFF -DYAZE_BUILD_CLI=OFF -DYAZE_BUILD_TESTS=OFF|Minimal library only"
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $*"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}[✓]${NC} $*"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[✗]${NC} $*"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}[!]${NC} $*"
|
||||
}
|
||||
|
||||
print_header() {
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE}$*${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
}
|
||||
|
||||
print_usage() {
|
||||
cat <<EOF
|
||||
Configuration Matrix Tester
|
||||
|
||||
Usage:
|
||||
$0 [OPTIONS]
|
||||
|
||||
Options:
|
||||
--config NAME Test specific configuration (e.g., minimal, full-ai)
|
||||
--smoke Fast smoke test only (configure, compile 5 files)
|
||||
--verbose Verbose output
|
||||
--platform PLAT Force platform (linux, macos, windows)
|
||||
--jobs N Parallel jobs (default: 4)
|
||||
--help Show this help message
|
||||
|
||||
Examples:
|
||||
$0 # Test all configurations
|
||||
$0 --config minimal # Test specific config
|
||||
$0 --smoke --config full-ai # Smoke test full-ai
|
||||
$0 --verbose --platform linux # Verbose, Linux only
|
||||
|
||||
Configurations Available:
|
||||
minimal - No AI, no gRPC
|
||||
grpc-only - gRPC without automation
|
||||
full-ai - All features enabled
|
||||
cli-no-grpc - CLI only, no networking
|
||||
http-api - REST API endpoints
|
||||
no-json - Ollama mode (no JSON)
|
||||
all-off - Library only
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# CMake Configuration Testing
|
||||
# ============================================================================
|
||||
|
||||
test_config() {
|
||||
local config_name="$1"
|
||||
local preset="$2"
|
||||
local cmake_flags="$3"
|
||||
local description="$4"
|
||||
|
||||
TOTAL=$((TOTAL + 1))
|
||||
|
||||
echo ""
|
||||
print_header "Testing: $config_name"
|
||||
log_info "Description: $description"
|
||||
log_info "Preset: $preset"
|
||||
log_info "CMake flags: $cmake_flags"
|
||||
|
||||
local build_dir="${MATRIX_BUILD_DIR}/${config_name}"
|
||||
|
||||
# Create build directory
|
||||
mkdir -p "$build_dir"
|
||||
|
||||
# Configure
|
||||
log_info "Configuring CMake..."
|
||||
if ! cmake --preset "$preset" \
|
||||
-B "$build_dir" \
|
||||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
$cmake_flags \
|
||||
>"${build_dir}/config.log" 2>&1; then
|
||||
log_error "Configuration failed for $config_name"
|
||||
RESULTS["$config_name"]="FAILED (configure)"
|
||||
FAILED=$((FAILED + 1))
|
||||
|
||||
if [ "$VERBOSE" == "1" ]; then
|
||||
tail -20 "${build_dir}/config.log"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
|
||||
log_success "Configuration successful"
|
||||
|
||||
# Print resolved configuration (for debugging)
|
||||
if [ "$VERBOSE" == "1" ]; then
|
||||
log_info "Resolved configuration:"
|
||||
grep "YAZE_BUILD\|YAZE_ENABLE" "${build_dir}/CMakeCache.txt" | sort | sed 's/^/ /' || true
|
||||
fi
|
||||
|
||||
# Build
|
||||
log_info "Building..."
|
||||
if [ "$SMOKE_ONLY" == "1" ]; then
|
||||
# Smoke test: just build a few key files
|
||||
log_info "Running smoke test (configure only)"
|
||||
else
|
||||
if ! cmake --build "$build_dir" \
|
||||
--config RelWithDebInfo \
|
||||
--parallel "$MATRIX_JOBS" \
|
||||
>"${build_dir}/build.log" 2>&1; then
|
||||
log_error "Build failed for $config_name"
|
||||
RESULTS["$config_name"]="FAILED (build)"
|
||||
FAILED=$((FAILED + 1))
|
||||
|
||||
if [ "$VERBOSE" == "1" ]; then
|
||||
tail -30 "${build_dir}/build.log"
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "Build successful"
|
||||
|
||||
# Run basic tests if available
|
||||
if [ -f "${build_dir}/bin/yaze_test" ] && [ "$SMOKE_ONLY" != "1" ]; then
|
||||
log_info "Running unit tests..."
|
||||
if timeout 30 "${build_dir}/bin/yaze_test" --unit >"${build_dir}/test.log" 2>&1; then
|
||||
log_success "Unit tests passed"
|
||||
else
|
||||
log_warning "Unit tests failed or timed out"
|
||||
RESULTS["$config_name"]="PASSED (build, tests failed)"
|
||||
fi
|
||||
fi
|
||||
|
||||
RESULTS["$config_name"]="PASSED"
|
||||
PASSED=$((PASSED + 1))
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Matrix Execution
|
||||
# ============================================================================
|
||||
|
||||
print_matrix_info() {
|
||||
echo ""
|
||||
echo -e "${BLUE}Configuration Matrix Test${NC}"
|
||||
echo "Project: $PROJECT_DIR"
|
||||
echo "Build directory: $MATRIX_BUILD_DIR"
|
||||
echo "Platform: $PLATFORM"
|
||||
echo "Parallel jobs: $MATRIX_JOBS"
|
||||
echo "Smoke test only: $SMOKE_ONLY"
|
||||
echo ""
|
||||
}
|
||||
|
||||
run_all_configs() {
|
||||
print_matrix_info
|
||||
|
||||
for config_line in "${CONFIGS[@]}"; do
|
||||
IFS='|' read -r config_name preset flags description <<<"$config_line"
|
||||
|
||||
# Filter by specific config if provided
|
||||
if [ -n "$SPECIFIC_CONFIG" ] && [ "$config_name" != "$SPECIFIC_CONFIG" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Filter by platform if needed
|
||||
case "$preset" in
|
||||
ci-linux | lin-* | lin-dev)
|
||||
if [[ "$PLATFORM" == "darwin" || "$PLATFORM" == "windows" ]]; then
|
||||
log_warning "Skipping $config_name (Linux-only preset on $PLATFORM)"
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
ci-macos | mac-* | win-uni)
|
||||
if [[ "$PLATFORM" != "darwin" ]]; then
|
||||
log_warning "Skipping $config_name (macOS-only preset on $PLATFORM)"
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
ci-windows | win-* | win-ai)
|
||||
if [[ "$PLATFORM" != "windows" ]]; then
|
||||
log_warning "Skipping $config_name (Windows-only preset on $PLATFORM)"
|
||||
continue
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
test_config "$config_name" "$preset" "$flags" "$description" || true
|
||||
done
|
||||
|
||||
print_summary
|
||||
}
|
||||
|
||||
print_summary() {
|
||||
print_header "Test Summary"
|
||||
|
||||
for config_name in "${!RESULTS[@]}"; do
|
||||
result="${RESULTS[$config_name]}"
|
||||
if [[ "$result" == PASSED* ]]; then
|
||||
echo -e "${GREEN}✓${NC} $config_name: $result"
|
||||
else
|
||||
echo -e "${RED}✗${NC} $config_name: $result"
|
||||
fi
|
||||
done | sort
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASSED/$TOTAL passed, $FAILED/$TOTAL failed"
|
||||
echo ""
|
||||
|
||||
if [ "$FAILED" -gt 0 ]; then
|
||||
log_error "Some configurations failed!"
|
||||
echo ""
|
||||
echo "Debug tips:"
|
||||
echo " - Check build logs in: $MATRIX_BUILD_DIR/<config>/build.log"
|
||||
echo " - Re-run with --verbose for full output"
|
||||
echo " - Check cmake errors: $MATRIX_BUILD_DIR/<config>/config.log"
|
||||
return 1
|
||||
else
|
||||
log_success "All configurations passed!"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Entry Point
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--config)
|
||||
SPECIFIC_CONFIG="$2"
|
||||
shift 2
|
||||
;;
|
||||
--smoke)
|
||||
SMOKE_ONLY=1
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=1
|
||||
shift
|
||||
;;
|
||||
--platform)
|
||||
PLATFORM="$2"
|
||||
shift 2
|
||||
;;
|
||||
--jobs)
|
||||
MATRIX_JOBS="$2"
|
||||
shift 2
|
||||
;;
|
||||
--help)
|
||||
print_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
log_error "Unknown option: $1"
|
||||
print_usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Validate environment
|
||||
if ! command -v cmake &>/dev/null; then
|
||||
log_error "cmake not found in PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean up old matrix builds on request
|
||||
if [ -d "$MATRIX_BUILD_DIR" ] && [ "$VERBOSE" == "1" ]; then
|
||||
log_info "Using existing matrix build directory"
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
if ! run_all_configs; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Run main function
|
||||
main "$@"
|
||||
40
scripts/test-linux-build.sh
Executable file
40
scripts/test-linux-build.sh
Executable file
@@ -0,0 +1,40 @@
|
||||
#!/bin/bash
|
||||
# Test Linux build in Docker container (simulates CI environment)
|
||||
set -e
|
||||
|
||||
echo "🐧 Testing Linux build in Docker container..."
|
||||
|
||||
# Use same Ubuntu version as CI
|
||||
docker run --rm -v "$PWD:/workspace" -w /workspace \
|
||||
ubuntu:22.04 bash -c '
|
||||
set -e
|
||||
echo "📦 Installing dependencies..."
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update
|
||||
apt-get install -y \
|
||||
build-essential cmake ninja-build pkg-config gcc-12 g++-12 \
|
||||
libglew-dev libxext-dev libwavpack-dev libboost-all-dev \
|
||||
libpng-dev python3-dev libpython3-dev \
|
||||
libasound2-dev libpulse-dev libaudio-dev \
|
||||
libx11-dev libxrandr-dev libxcursor-dev libxinerama-dev libxi-dev \
|
||||
libxss-dev libxxf86vm-dev libxkbcommon-dev libwayland-dev libdecor-0-dev \
|
||||
libgtk-3-dev libdbus-1-dev git
|
||||
|
||||
echo "⚙️ Configuring build..."
|
||||
cmake -B build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER=gcc-12 \
|
||||
-DCMAKE_CXX_COMPILER=g++-12 \
|
||||
-DYAZE_BUILD_TESTS=OFF \
|
||||
-DYAZE_BUILD_EMU=ON \
|
||||
-DYAZE_BUILD_Z3ED=ON \
|
||||
-DYAZE_BUILD_TOOLS=ON \
|
||||
-DNFD_PORTAL=ON
|
||||
|
||||
echo "🔨 Building..."
|
||||
cmake --build build --parallel $(nproc)
|
||||
|
||||
echo "✅ Linux build succeeded!"
|
||||
ls -lh build/bin/
|
||||
'
|
||||
|
||||
196
scripts/test-symbol-detection.sh
Executable file
196
scripts/test-symbol-detection.sh
Executable file
@@ -0,0 +1,196 @@
|
||||
#!/bin/bash
|
||||
# Integration Test - Symbol Conflict Detection System
|
||||
#
|
||||
# Verifies that extract-symbols and check-duplicate-symbols work correctly
|
||||
# This script is NOT for CI, but for manual testing and validation
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(dirname "${SCRIPT_DIR}")"
|
||||
BUILD_DIR="${PROJECT_ROOT}/build"
|
||||
|
||||
echo -e "${BLUE}=== Symbol Detection System Test ===${NC}"
|
||||
echo ""
|
||||
|
||||
# Test 1: Verify scripts are executable
|
||||
echo -e "${CYAN}[Test 1]${NC} Verifying scripts are executable..."
|
||||
if [[ -x "${SCRIPT_DIR}/extract-symbols.sh" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} extract-symbols.sh is executable"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} extract-symbols.sh is NOT executable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -x "${SCRIPT_DIR}/check-duplicate-symbols.sh" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} check-duplicate-symbols.sh is executable"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} check-duplicate-symbols.sh is NOT executable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -x "${PROJECT_ROOT}/.githooks/pre-commit" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} .githooks/pre-commit is executable"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} .githooks/pre-commit is NOT executable"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 2: Verify build directory exists
|
||||
echo -e "${CYAN}[Test 2]${NC} Checking build directory..."
|
||||
if [[ -d "${BUILD_DIR}" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} Build directory exists: ${BUILD_DIR}"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Build directory not found: ${BUILD_DIR}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for object files
|
||||
OBJ_COUNT=$(find "${BUILD_DIR}" -name "*.o" -o -name "*.obj" 2>/dev/null | wc -l)
|
||||
if [[ ${OBJ_COUNT} -gt 0 ]]; then
|
||||
echo -e " ${GREEN}✓${NC} Found ${OBJ_COUNT} object files"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} No object files found (run cmake --build build first)"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 3: Run extract-symbols
|
||||
echo -e "${CYAN}[Test 3]${NC} Running symbol extraction..."
|
||||
if cd "${PROJECT_ROOT}" && ./scripts/extract-symbols.sh "${BUILD_DIR}" 2>&1 | tail -5; then
|
||||
echo -e " ${GREEN}✓${NC} Symbol extraction completed"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Symbol extraction failed"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 4: Verify symbol database was created
|
||||
echo -e "${CYAN}[Test 4]${NC} Verifying symbol database..."
|
||||
DB_FILE="${BUILD_DIR}/symbol_database.json"
|
||||
if [[ -f "${DB_FILE}" ]]; then
|
||||
SIZE=$(du -h "${DB_FILE}" | cut -f1)
|
||||
echo -e " ${GREEN}✓${NC} Symbol database created: ${DB_FILE} (${SIZE})"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Symbol database not created"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 5: Verify JSON structure
|
||||
echo -e "${CYAN}[Test 5]${NC} Validating JSON structure..."
|
||||
if python3 -m json.tool "${DB_FILE}" > /dev/null 2>&1; then
|
||||
echo -e " ${GREEN}✓${NC} Valid JSON format"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Invalid JSON format"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for expected fields
|
||||
if python3 << PYTHON_EOF
|
||||
import json
|
||||
with open("${DB_FILE}") as f:
|
||||
data = json.load(f)
|
||||
|
||||
required = ["metadata", "conflicts", "symbols"]
|
||||
for field in required:
|
||||
if field not in data:
|
||||
exit(1)
|
||||
|
||||
metadata = data.get("metadata", {})
|
||||
required_meta = ["platform", "build_dir", "timestamp", "object_files_scanned", "total_symbols"]
|
||||
for field in required_meta:
|
||||
if field not in metadata:
|
||||
exit(1)
|
||||
|
||||
print(" ${GREEN}✓${NC} JSON structure is correct")
|
||||
exit(0)
|
||||
PYTHON_EOF
|
||||
; then
|
||||
: # Already printed
|
||||
else
|
||||
echo -e " ${RED}✗${NC} JSON structure validation failed"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 6: Run duplicate checker
|
||||
echo -e "${CYAN}[Test 6]${NC} Running duplicate symbol checker..."
|
||||
if cd "${PROJECT_ROOT}" && ./scripts/check-duplicate-symbols.sh "${DB_FILE}" 2>&1 | tail -10; then
|
||||
echo -e " ${GREEN}✓${NC} Duplicate checker completed (no conflicts)"
|
||||
else
|
||||
# Duplicate checker returns 1 if conflicts found, which is expected behavior
|
||||
exit_code=$?
|
||||
if [[ ${exit_code} -eq 1 ]]; then
|
||||
echo -e " ${YELLOW}✓${NC} Duplicate checker found conflicts (expected in some cases)"
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Duplicate checker failed with error"
|
||||
exit ${exit_code}
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 7: Verify pre-commit hook
|
||||
echo -e "${CYAN}[Test 7]${NC} Checking pre-commit hook setup..."
|
||||
HOOK_PATH="${PROJECT_ROOT}/.githooks/pre-commit"
|
||||
if [[ -f "${HOOK_PATH}" ]]; then
|
||||
echo -e " ${GREEN}✓${NC} Pre-commit hook exists"
|
||||
|
||||
# Check if git hooks are configured
|
||||
cd "${PROJECT_ROOT}"
|
||||
if git config core.hooksPath 2>/dev/null | grep -q "\.githooks"; then
|
||||
echo -e " ${GREEN}✓${NC} Git hooks path configured: $(git config core.hooksPath)"
|
||||
else
|
||||
echo -e " ${YELLOW}!${NC} Git hooks path not configured"
|
||||
echo -e " Run: ${CYAN}git config core.hooksPath .githooks${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " ${RED}✗${NC} Pre-commit hook not found: ${HOOK_PATH}"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Test 8: Display sample output
|
||||
echo -e "${CYAN}[Test 8]${NC} Sample symbol database contents..."
|
||||
python3 << PYTHON_EOF
|
||||
import json
|
||||
|
||||
with open("${DB_FILE}") as f:
|
||||
data = json.load(f)
|
||||
|
||||
meta = data.get("metadata", {})
|
||||
print(f" Platform: {meta.get('platform', '?')}")
|
||||
print(f" Object files: {meta.get('object_files_scanned', '?')}")
|
||||
print(f" Total symbols: {meta.get('total_symbols', '?')}")
|
||||
print(f" Conflicts: {meta.get('total_conflicts', 0)}")
|
||||
|
||||
conflicts = data.get("conflicts", [])
|
||||
if conflicts:
|
||||
print(f"\n Sample conflicts:")
|
||||
for conflict in conflicts[:3]:
|
||||
symbol = conflict.get("symbol", "?")
|
||||
count = conflict.get("count", 0)
|
||||
print(f" - {symbol} (x{count})")
|
||||
if len(conflicts) > 3:
|
||||
print(f" ... and {len(conflicts) - 3} more")
|
||||
PYTHON_EOF
|
||||
echo ""
|
||||
|
||||
# Final Summary
|
||||
echo -e "${GREEN}=== All Tests Passed! ===${NC}"
|
||||
echo ""
|
||||
echo -e "Next steps:"
|
||||
echo -e " 1. Configure git hooks: ${CYAN}git config core.hooksPath .githooks${NC}"
|
||||
echo -e " 2. Review symbol database: ${CYAN}cat ${DB_FILE} | head -50${NC}"
|
||||
echo -e " 3. Check for conflicts: ${CYAN}./scripts/check-duplicate-symbols.sh${NC}"
|
||||
echo -e " 4. Integrate into CI: See docs/internal/testing/symbol-conflict-detection.md"
|
||||
echo ""
|
||||
360
scripts/validate-cmake-config.cmake
Normal file
360
scripts/validate-cmake-config.cmake
Normal file
@@ -0,0 +1,360 @@
|
||||
# CMake Configuration Validator
|
||||
# Validates CMake configuration and catches dependency issues early
|
||||
#
|
||||
# Usage:
|
||||
# cmake -P scripts/validate-cmake-config.cmake [build_directory]
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 - All checks passed
|
||||
# 1 - Validation failed (errors detected)
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
# Parse command-line arguments
|
||||
if(CMAKE_ARGC GREATER 3)
|
||||
set(BUILD_DIR "${CMAKE_ARGV3}")
|
||||
else()
|
||||
set(BUILD_DIR "${CMAKE_CURRENT_SOURCE_DIR}/build")
|
||||
endif()
|
||||
|
||||
# Color output helpers
|
||||
if(WIN32)
|
||||
set(COLOR_RESET "")
|
||||
set(COLOR_RED "")
|
||||
set(COLOR_GREEN "")
|
||||
set(COLOR_YELLOW "")
|
||||
set(COLOR_BLUE "")
|
||||
else()
|
||||
string(ASCII 27 ESC)
|
||||
set(COLOR_RESET "${ESC}[0m")
|
||||
set(COLOR_RED "${ESC}[31m")
|
||||
set(COLOR_GREEN "${ESC}[32m")
|
||||
set(COLOR_YELLOW "${ESC}[33m")
|
||||
set(COLOR_BLUE "${ESC}[34m")
|
||||
endif()
|
||||
|
||||
set(VALIDATION_ERRORS 0)
|
||||
set(VALIDATION_WARNINGS 0)
|
||||
|
||||
macro(log_header msg)
|
||||
message(STATUS "${COLOR_BLUE}=== ${msg} ===${COLOR_RESET}")
|
||||
endmacro()
|
||||
|
||||
macro(log_success msg)
|
||||
message(STATUS "${COLOR_GREEN}✓${COLOR_RESET} ${msg}")
|
||||
endmacro()
|
||||
|
||||
macro(log_warning msg)
|
||||
message(STATUS "${COLOR_YELLOW}⚠${COLOR_RESET} ${msg}")
|
||||
math(EXPR VALIDATION_WARNINGS "${VALIDATION_WARNINGS} + 1")
|
||||
endmacro()
|
||||
|
||||
macro(log_error msg)
|
||||
message(STATUS "${COLOR_RED}✗${COLOR_RESET} ${msg}")
|
||||
math(EXPR VALIDATION_ERRORS "${VALIDATION_ERRORS} + 1")
|
||||
endmacro()
|
||||
|
||||
# ============================================================================
|
||||
# Check build directory exists
|
||||
# ============================================================================
|
||||
log_header("Checking build directory")
|
||||
|
||||
if(NOT EXISTS "${BUILD_DIR}")
|
||||
log_error("Build directory not found: ${BUILD_DIR}")
|
||||
log_error("Run cmake configure first: cmake --preset <preset-name>")
|
||||
message(FATAL_ERROR "Build directory does not exist")
|
||||
endif()
|
||||
|
||||
if(NOT EXISTS "${BUILD_DIR}/CMakeCache.txt")
|
||||
log_error("CMakeCache.txt not found in ${BUILD_DIR}")
|
||||
log_error("Configuration incomplete - run cmake configure first")
|
||||
message(FATAL_ERROR "CMake configuration not found")
|
||||
endif()
|
||||
|
||||
log_success("Build directory: ${BUILD_DIR}")
|
||||
|
||||
# ============================================================================
|
||||
# Load CMake cache variables
|
||||
# ============================================================================
|
||||
log_header("Loading CMake configuration")
|
||||
|
||||
file(STRINGS "${BUILD_DIR}/CMakeCache.txt" CACHE_LINES)
|
||||
set(CACHE_VARS "")
|
||||
|
||||
foreach(line ${CACHE_LINES})
|
||||
if(line MATCHES "^([^:]+):([^=]+)=(.*)$")
|
||||
set(var_name "${CMAKE_MATCH_1}")
|
||||
set(var_type "${CMAKE_MATCH_2}")
|
||||
set(var_value "${CMAKE_MATCH_3}")
|
||||
set(CACHE_${var_name} "${var_value}")
|
||||
list(APPEND CACHE_VARS "${var_name}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
log_success("Loaded ${CMAKE_ARGC} cache variables")
|
||||
|
||||
# ============================================================================
|
||||
# Validate required targets exist
|
||||
# ============================================================================
|
||||
log_header("Validating required targets")
|
||||
|
||||
set(REQUIRED_TARGETS
|
||||
"yaze_common"
|
||||
)
|
||||
|
||||
set(OPTIONAL_TARGETS_GRPC
|
||||
"grpc::grpc++"
|
||||
"grpc::grpc++_reflection"
|
||||
"protobuf::libprotobuf"
|
||||
"protoc"
|
||||
"grpc_cpp_plugin"
|
||||
)
|
||||
|
||||
set(OPTIONAL_TARGETS_ABSL
|
||||
"absl::base"
|
||||
"absl::status"
|
||||
"absl::statusor"
|
||||
"absl::strings"
|
||||
"absl::flags"
|
||||
"absl::flags_parse"
|
||||
)
|
||||
|
||||
# Check for targets by looking for their CMake files
|
||||
foreach(target ${REQUIRED_TARGETS})
|
||||
string(REPLACE "::" "_" target_safe "${target}")
|
||||
if(EXISTS "${BUILD_DIR}/CMakeFiles/${target_safe}.dir")
|
||||
log_success("Required target exists: ${target}")
|
||||
else()
|
||||
log_error("Required target missing: ${target}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# ============================================================================
|
||||
# Validate feature flags and dependencies
|
||||
# ============================================================================
|
||||
log_header("Validating feature flags")
|
||||
|
||||
if(DEFINED CACHE_YAZE_ENABLE_GRPC)
|
||||
if(CACHE_YAZE_ENABLE_GRPC)
|
||||
log_success("gRPC enabled: ${CACHE_YAZE_ENABLE_GRPC}")
|
||||
|
||||
# Validate gRPC-related cache variables
|
||||
if(NOT DEFINED CACHE_GRPC_VERSION)
|
||||
log_warning("GRPC_VERSION not set in cache")
|
||||
else()
|
||||
log_success("gRPC version: ${CACHE_GRPC_VERSION}")
|
||||
endif()
|
||||
else()
|
||||
log_success("gRPC disabled")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(DEFINED CACHE_YAZE_BUILD_TESTS)
|
||||
if(CACHE_YAZE_BUILD_TESTS)
|
||||
log_success("Tests enabled")
|
||||
else()
|
||||
log_success("Tests disabled")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(DEFINED CACHE_YAZE_ENABLE_AI)
|
||||
if(CACHE_YAZE_ENABLE_AI)
|
||||
log_success("AI features enabled")
|
||||
|
||||
# When AI is enabled, gRPC should also be enabled
|
||||
if(NOT CACHE_YAZE_ENABLE_GRPC)
|
||||
log_error("AI enabled but gRPC disabled - AI requires gRPC")
|
||||
endif()
|
||||
else()
|
||||
log_success("AI features disabled")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Validate compiler flags
|
||||
# ============================================================================
|
||||
log_header("Validating compiler flags")
|
||||
|
||||
if(DEFINED CACHE_CMAKE_CXX_STANDARD)
|
||||
if(CACHE_CMAKE_CXX_STANDARD EQUAL 23)
|
||||
log_success("C++ standard: ${CACHE_CMAKE_CXX_STANDARD}")
|
||||
else()
|
||||
log_warning("C++ standard is ${CACHE_CMAKE_CXX_STANDARD}, expected 23")
|
||||
endif()
|
||||
else()
|
||||
log_error("CMAKE_CXX_STANDARD not set")
|
||||
endif()
|
||||
|
||||
if(DEFINED CACHE_CMAKE_CXX_FLAGS)
|
||||
log_success("CXX flags set: ${CACHE_CMAKE_CXX_FLAGS}")
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Validate Abseil configuration (Windows-specific)
|
||||
# ============================================================================
|
||||
if(WIN32 OR CACHE_CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
log_header("Validating Windows/Abseil configuration")
|
||||
|
||||
# Check for MSVC runtime library setting
|
||||
if(DEFINED CACHE_CMAKE_MSVC_RUNTIME_LIBRARY)
|
||||
if(CACHE_CMAKE_MSVC_RUNTIME_LIBRARY MATCHES "MultiThreaded")
|
||||
log_success("MSVC runtime: ${CACHE_CMAKE_MSVC_RUNTIME_LIBRARY}")
|
||||
else()
|
||||
log_warning("MSVC runtime: ${CACHE_CMAKE_MSVC_RUNTIME_LIBRARY} (expected MultiThreaded)")
|
||||
endif()
|
||||
else()
|
||||
log_warning("CMAKE_MSVC_RUNTIME_LIBRARY not set")
|
||||
endif()
|
||||
|
||||
# Check for Abseil propagation flag
|
||||
if(DEFINED CACHE_ABSL_PROPAGATE_CXX_STD)
|
||||
if(CACHE_ABSL_PROPAGATE_CXX_STD)
|
||||
log_success("Abseil CXX standard propagation enabled")
|
||||
else()
|
||||
log_warning("ABSL_PROPAGATE_CXX_STD is OFF - may cause issues")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Check for circular dependencies
|
||||
# ============================================================================
|
||||
log_header("Checking for circular dependencies")
|
||||
|
||||
if(EXISTS "${BUILD_DIR}/CMakeFiles/TargetDirectories.txt")
|
||||
file(READ "${BUILD_DIR}/CMakeFiles/TargetDirectories.txt" TARGET_DIRS)
|
||||
string(REGEX MATCHALL "[^\n]+" TARGET_LIST "${TARGET_DIRS}")
|
||||
list(LENGTH TARGET_LIST NUM_TARGETS)
|
||||
log_success("Found ${NUM_TARGETS} build targets")
|
||||
else()
|
||||
log_warning("TargetDirectories.txt not found - cannot validate targets")
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Validate compile_commands.json
|
||||
# ============================================================================
|
||||
log_header("Validating compile commands")
|
||||
|
||||
if(NOT DEFINED CACHE_CMAKE_EXPORT_COMPILE_COMMANDS)
|
||||
log_warning("CMAKE_EXPORT_COMPILE_COMMANDS not set")
|
||||
elseif(NOT CACHE_CMAKE_EXPORT_COMPILE_COMMANDS)
|
||||
log_warning("Compile commands export disabled")
|
||||
else()
|
||||
if(EXISTS "${BUILD_DIR}/compile_commands.json")
|
||||
file(READ "${BUILD_DIR}/compile_commands.json" COMPILE_COMMANDS)
|
||||
string(LENGTH "${COMPILE_COMMANDS}" COMPILE_COMMANDS_SIZE)
|
||||
|
||||
if(COMPILE_COMMANDS_SIZE GREATER 100)
|
||||
log_success("compile_commands.json generated (${COMPILE_COMMANDS_SIZE} bytes)")
|
||||
else()
|
||||
log_warning("compile_commands.json is very small or empty")
|
||||
endif()
|
||||
else()
|
||||
log_warning("compile_commands.json not found")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Platform-specific validation
|
||||
# ============================================================================
|
||||
log_header("Platform-specific checks")
|
||||
|
||||
if(DEFINED CACHE_CMAKE_SYSTEM_NAME)
|
||||
log_success("Target system: ${CACHE_CMAKE_SYSTEM_NAME}")
|
||||
|
||||
if(CACHE_CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
# macOS-specific checks
|
||||
if(DEFINED CACHE_CMAKE_OSX_ARCHITECTURES)
|
||||
log_success("macOS architectures: ${CACHE_CMAKE_OSX_ARCHITECTURES}")
|
||||
endif()
|
||||
elseif(CACHE_CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||
# Windows-specific checks
|
||||
if(DEFINED CACHE_CMAKE_GENERATOR)
|
||||
log_success("Generator: ${CACHE_CMAKE_GENERATOR}")
|
||||
|
||||
if(CACHE_CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
log_success("Using Visual Studio generator")
|
||||
elseif(CACHE_CMAKE_GENERATOR MATCHES "Ninja")
|
||||
log_success("Using Ninja generator")
|
||||
endif()
|
||||
endif()
|
||||
elseif(CACHE_CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
# Linux-specific checks
|
||||
if(DEFINED CACHE_CMAKE_CXX_COMPILER_ID)
|
||||
log_success("Compiler: ${CACHE_CMAKE_CXX_COMPILER_ID}")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Validate output directories
|
||||
# ============================================================================
|
||||
log_header("Validating output directories")
|
||||
|
||||
set(OUTPUT_DIRS
|
||||
"${BUILD_DIR}/bin"
|
||||
"${BUILD_DIR}/lib"
|
||||
)
|
||||
|
||||
foreach(dir ${OUTPUT_DIRS})
|
||||
if(EXISTS "${dir}")
|
||||
log_success("Output directory exists: ${dir}")
|
||||
else()
|
||||
log_warning("Output directory missing: ${dir}")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
# ============================================================================
|
||||
# Check for common configuration issues
|
||||
# ============================================================================
|
||||
log_header("Checking for common issues")
|
||||
|
||||
# Issue 1: Missing Abseil include paths on Windows
|
||||
if(CACHE_YAZE_ENABLE_GRPC AND (WIN32 OR CACHE_CMAKE_SYSTEM_NAME STREQUAL "Windows"))
|
||||
if(EXISTS "${BUILD_DIR}/_deps/grpc-build/third_party/abseil-cpp")
|
||||
log_success("Abseil include directory exists in gRPC build")
|
||||
else()
|
||||
log_warning("Abseil directory not found in expected location")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Issue 2: Flag propagation
|
||||
if(DEFINED CACHE_YAZE_SUPPRESS_WARNINGS)
|
||||
if(CACHE_YAZE_SUPPRESS_WARNINGS)
|
||||
log_success("Warnings suppressed (use -v preset for verbose)")
|
||||
else()
|
||||
log_success("Verbose warnings enabled")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Issue 3: LTO enabled in debug builds
|
||||
if(DEFINED CACHE_CMAKE_BUILD_TYPE)
|
||||
if(CACHE_CMAKE_BUILD_TYPE STREQUAL "Debug" AND DEFINED CACHE_YAZE_ENABLE_LTO)
|
||||
if(CACHE_YAZE_ENABLE_LTO)
|
||||
log_warning("LTO enabled in Debug build - may slow compilation")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Summary
|
||||
# ============================================================================
|
||||
log_header("Validation Summary")
|
||||
|
||||
if(VALIDATION_ERRORS GREATER 0)
|
||||
message(STATUS "${COLOR_RED}${VALIDATION_ERRORS} error(s) found${COLOR_RESET}")
|
||||
endif()
|
||||
|
||||
if(VALIDATION_WARNINGS GREATER 0)
|
||||
message(STATUS "${COLOR_YELLOW}${VALIDATION_WARNINGS} warning(s) found${COLOR_RESET}")
|
||||
endif()
|
||||
|
||||
if(VALIDATION_ERRORS EQUAL 0 AND VALIDATION_WARNINGS EQUAL 0)
|
||||
message(STATUS "${COLOR_GREEN}✓ All validation checks passed!${COLOR_RESET}")
|
||||
message(STATUS "Configuration is ready for build")
|
||||
elseif(VALIDATION_ERRORS EQUAL 0)
|
||||
message(STATUS "${COLOR_YELLOW}Configuration has warnings but is buildable${COLOR_RESET}")
|
||||
else()
|
||||
message(STATUS "${COLOR_RED}Configuration has errors - fix before building${COLOR_RESET}")
|
||||
message(FATAL_ERROR "CMake configuration validation failed")
|
||||
endif()
|
||||
326
scripts/validate-cmake-config.sh
Executable file
326
scripts/validate-cmake-config.sh
Executable file
@@ -0,0 +1,326 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# CMake Configuration Validator
|
||||
#
|
||||
# Validates that a set of CMake flags would result in a valid configuration.
|
||||
# Catches common mistakes before running a full build.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/validate-cmake-config.sh [FLAGS]
|
||||
#
|
||||
# Examples:
|
||||
# ./scripts/validate-cmake-config.sh -DYAZE_ENABLE_GRPC=ON -DYAZE_ENABLE_REMOTE_AUTOMATION=OFF
|
||||
# ./scripts/validate-cmake-config.sh -DYAZE_ENABLE_HTTP_API=ON -DYAZE_ENABLE_AGENT_CLI=OFF
|
||||
# ./scripts/validate-cmake-config.sh -DYAZE_BUILD_AGENT_UI=ON -DYAZE_BUILD_GUI=OFF
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# Validation rules
|
||||
# Format: "Flag1=Value1" "Flag2=Value2" "ERROR_MESSAGE"
|
||||
declare -a INCOMPATIBLE_PAIRS=(
|
||||
"YAZE_ENABLE_GRPC=OFF" "YAZE_ENABLE_REMOTE_AUTOMATION=ON" "REMOTE_AUTOMATION requires GRPC"
|
||||
"YAZE_ENABLE_HTTP_API=ON" "YAZE_ENABLE_AGENT_CLI=OFF" "HTTP_API requires AGENT_CLI"
|
||||
"YAZE_BUILD_AGENT_UI=ON" "YAZE_BUILD_GUI=OFF" "AGENT_UI requires BUILD_GUI"
|
||||
"YAZE_ENABLE_AI_RUNTIME=ON" "YAZE_ENABLE_AI=OFF" "AI_RUNTIME requires ENABLE_AI"
|
||||
)
|
||||
|
||||
# Single-flag rules (flag must have specific value)
|
||||
declare -A SINGLE_FLAG_RULES=(
|
||||
# If left side is ON, right side must also be ON
|
||||
# Format handled specially below
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Helper Functions
|
||||
# ============================================================================
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}✗ ERROR${NC}: $*"
|
||||
}
|
||||
|
||||
log_warning() {
|
||||
echo -e "${YELLOW}! WARNING${NC}: $*"
|
||||
}
|
||||
|
||||
log_success() {
|
||||
echo -e "${GREEN}✓${NC} $*"
|
||||
}
|
||||
|
||||
log_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $*"
|
||||
}
|
||||
|
||||
print_usage() {
|
||||
cat <<'EOF'
|
||||
CMake Configuration Validator
|
||||
|
||||
Validates CMake flag combinations before building.
|
||||
|
||||
Usage:
|
||||
./scripts/validate-cmake-config.sh [CMake flags]
|
||||
|
||||
Examples:
|
||||
# Check if this combination is valid
|
||||
./scripts/validate-cmake-config.sh \
|
||||
-DYAZE_ENABLE_GRPC=ON \
|
||||
-DYAZE_ENABLE_REMOTE_AUTOMATION=OFF
|
||||
|
||||
# Batch check multiple combinations
|
||||
./scripts/validate-cmake-config.sh \
|
||||
-DYAZE_ENABLE_HTTP_API=ON \
|
||||
-DYAZE_ENABLE_AGENT_CLI=OFF
|
||||
|
||||
Rules Checked:
|
||||
1. Dependency constraints
|
||||
2. Mutual exclusivity
|
||||
3. Feature prerequisites
|
||||
4. Platform-specific requirements
|
||||
|
||||
Output:
|
||||
✓ All checks passed
|
||||
✗ Error: Specific problem found
|
||||
! Warning: Potential issue
|
||||
|
||||
EOF
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Configuration Parsing
|
||||
# ============================================================================
|
||||
|
||||
parse_flags() {
|
||||
declare -gA FLAGS_PROVIDED
|
||||
|
||||
for flag in "$@"; do
|
||||
if [[ "$flag" =~ ^-D([A-Z_]+)=(.*)$ ]]; then
|
||||
local key="${BASH_REMATCH[1]}"
|
||||
local value="${BASH_REMATCH[2]}"
|
||||
FLAGS_PROVIDED["$key"]="$value"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
get_flag_value() {
|
||||
local flag="$1"
|
||||
local default="${2:-}"
|
||||
|
||||
# Return user-provided value if available
|
||||
if [[ -n "${FLAGS_PROVIDED[$flag]:-}" ]]; then
|
||||
echo "${FLAGS_PROVIDED[$flag]}"
|
||||
else
|
||||
echo "$default"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Validation Rules
|
||||
# ============================================================================
|
||||
|
||||
check_dependency_constraint() {
|
||||
local dependent="$1"
|
||||
local dependency="$2"
|
||||
local error_msg="$3"
|
||||
|
||||
local dependent_value=$(get_flag_value "$dependent" "OFF")
|
||||
local dependency_value=$(get_flag_value "$dependency" "OFF")
|
||||
|
||||
# If dependent is ON, dependency must be ON
|
||||
if [[ "$dependent_value" == "ON" ]] && [[ "$dependency_value" != "ON" ]]; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_remote_automation() {
|
||||
# REMOTE_AUTOMATION requires GRPC
|
||||
if ! check_dependency_constraint "YAZE_ENABLE_REMOTE_AUTOMATION" "YAZE_ENABLE_GRPC" \
|
||||
"YAZE_ENABLE_REMOTE_AUTOMATION=ON requires YAZE_ENABLE_GRPC=ON"; then
|
||||
log_error "YAZE_ENABLE_REMOTE_AUTOMATION=ON requires YAZE_ENABLE_GRPC=ON"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_http_api() {
|
||||
# HTTP_API requires AGENT_CLI
|
||||
if ! check_dependency_constraint "YAZE_ENABLE_HTTP_API" "YAZE_ENABLE_AGENT_CLI" \
|
||||
"YAZE_ENABLE_HTTP_API=ON requires YAZE_ENABLE_AGENT_CLI=ON"; then
|
||||
log_error "YAZE_ENABLE_HTTP_API=ON requires YAZE_ENABLE_AGENT_CLI=ON"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_agent_ui() {
|
||||
# AGENT_UI requires BUILD_GUI
|
||||
if ! check_dependency_constraint "YAZE_BUILD_AGENT_UI" "YAZE_BUILD_GUI" \
|
||||
"YAZE_BUILD_AGENT_UI=ON requires YAZE_BUILD_GUI=ON"; then
|
||||
log_error "YAZE_BUILD_AGENT_UI=ON requires YAZE_BUILD_GUI=ON"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_ai_runtime() {
|
||||
# AI_RUNTIME requires AI
|
||||
if ! check_dependency_constraint "YAZE_ENABLE_AI_RUNTIME" "YAZE_ENABLE_AI" \
|
||||
"YAZE_ENABLE_AI_RUNTIME=ON requires YAZE_ENABLE_AI=ON"; then
|
||||
log_error "YAZE_ENABLE_AI_RUNTIME=ON requires YAZE_ENABLE_AI=ON"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_gemini_support() {
|
||||
# Gemini support needs both AI_RUNTIME and JSON
|
||||
local ai_runtime=$(get_flag_value "YAZE_ENABLE_AI_RUNTIME" "OFF")
|
||||
local json=$(get_flag_value "YAZE_ENABLE_JSON" "ON")
|
||||
|
||||
if [[ "$ai_runtime" == "ON" ]] && [[ "$json" != "ON" ]]; then
|
||||
log_warning "YAZE_ENABLE_AI_RUNTIME=ON without YAZE_ENABLE_JSON=ON"
|
||||
log_info " → Gemini service requires JSON parsing"
|
||||
log_info " → Ollama will still work without JSON"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_agent_cli_auto_enable() {
|
||||
# AGENT_CLI is auto-enabled if BUILD_CLI or BUILD_Z3ED enabled
|
||||
local build_cli=$(get_flag_value "YAZE_BUILD_CLI" "ON")
|
||||
local build_z3ed=$(get_flag_value "YAZE_BUILD_Z3ED" "ON")
|
||||
local agent_cli=$(get_flag_value "YAZE_ENABLE_AGENT_CLI" "")
|
||||
|
||||
if [[ "$agent_cli" == "OFF" ]] && ([[ "$build_cli" == "ON" ]] || [[ "$build_z3ed" == "ON" ]]); then
|
||||
log_warning "YAZE_ENABLE_AGENT_CLI=OFF but YAZE_BUILD_CLI or BUILD_Z3ED is ON"
|
||||
log_info " → AGENT_CLI will be auto-enabled by CMake"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Recommendations
|
||||
# ============================================================================
|
||||
|
||||
suggest_configuration() {
|
||||
local remote_auto=$(get_flag_value "YAZE_ENABLE_REMOTE_AUTOMATION" "OFF")
|
||||
local http_api=$(get_flag_value "YAZE_ENABLE_HTTP_API" "OFF")
|
||||
local ai_runtime=$(get_flag_value "YAZE_ENABLE_AI_RUNTIME" "OFF")
|
||||
local build_gui=$(get_flag_value "YAZE_BUILD_GUI" "ON")
|
||||
local build_cli=$(get_flag_value "YAZE_BUILD_CLI" "ON")
|
||||
|
||||
echo ""
|
||||
echo "Suggested preset configurations:"
|
||||
echo ""
|
||||
|
||||
if [[ "$build_gui" == "ON" ]] && [[ "$ai_runtime" != "ON" ]]; then
|
||||
log_info "GUI-only user? Use preset: mac-dbg, lin-dbg, or win-dbg"
|
||||
fi
|
||||
|
||||
if [[ "$remote_auto" == "ON" ]] && [[ "$ai_runtime" == "ON" ]]; then
|
||||
log_info "Full-featured dev? Use preset: mac-ai, lin-ai, or win-ai"
|
||||
fi
|
||||
|
||||
if [[ "$build_cli" == "ON" ]] && [[ "$build_gui" != "ON" ]]; then
|
||||
log_info "CLI-only user? Use preset: win-z3ed or custom CLI config"
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Validation
|
||||
# ============================================================================
|
||||
|
||||
validate_configuration() {
|
||||
local errors=0
|
||||
local warnings=0
|
||||
|
||||
log_info "Validating CMake configuration..."
|
||||
echo ""
|
||||
|
||||
# Run all validation checks
|
||||
if ! validate_remote_automation; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
if ! validate_http_api; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
if ! validate_agent_ui; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
if ! validate_ai_runtime; then
|
||||
errors=$((errors + 1))
|
||||
fi
|
||||
|
||||
if ! validate_gemini_support; then
|
||||
warnings=$((warnings + 1))
|
||||
fi
|
||||
|
||||
if ! validate_agent_cli_auto_enable; then
|
||||
warnings=$((warnings + 1))
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
if [[ $errors -gt 0 ]]; then
|
||||
log_error "$errors critical issue(s) found"
|
||||
echo ""
|
||||
echo "Fix these before building:"
|
||||
echo " - Check documentation: docs/internal/configuration-matrix.md"
|
||||
echo " - Run: ./scripts/test-config-matrix.sh --verbose"
|
||||
suggest_configuration
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $warnings -gt 0 ]]; then
|
||||
log_warning "$warnings warning(s) found"
|
||||
echo ""
|
||||
log_info "These may work, but might not have all features"
|
||||
fi
|
||||
|
||||
if [[ $errors -eq 0 ]]; then
|
||||
log_success "Configuration is valid!"
|
||||
suggest_configuration
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Entry Point
|
||||
# ============================================================================
|
||||
|
||||
main() {
|
||||
if [[ $# -eq 0 ]]; then
|
||||
print_usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
|
||||
print_usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse provided flags
|
||||
parse_flags "$@"
|
||||
|
||||
# Run validation
|
||||
if validate_configuration; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -56,12 +56,14 @@ function Get-CMakeVersion {
|
||||
|
||||
function Test-GitSubmodules {
|
||||
$submodules = @(
|
||||
"src/lib/SDL",
|
||||
"ext/SDL",
|
||||
"src/lib/abseil-cpp",
|
||||
"src/lib/asar",
|
||||
"src/lib/imgui",
|
||||
"third_party/json",
|
||||
"third_party/httplib"
|
||||
"ext/asar",
|
||||
"ext/imgui",
|
||||
"ext/json",
|
||||
"ext/httplib",
|
||||
"ext/imgui_test_engine",
|
||||
"ext/nativefiledialog-extended"
|
||||
)
|
||||
|
||||
$allPresent = $true
|
||||
@@ -165,6 +167,36 @@ function Test-Vcpkg {
|
||||
}
|
||||
}
|
||||
|
||||
function Test-VcpkgCache {
|
||||
$vcpkgPath = Join-Path $PSScriptRoot ".." "vcpkg"
|
||||
$installedDir = Join-Path $vcpkgPath "installed"
|
||||
|
||||
if (-not (Test-Path $installedDir)) {
|
||||
return
|
||||
}
|
||||
|
||||
$triplets = @("x64-windows", "arm64-windows")
|
||||
$hasPackages = $false
|
||||
|
||||
foreach ($triplet in $triplets) {
|
||||
$tripletDir = Join-Path $installedDir $triplet
|
||||
if (Test-Path $tripletDir) {
|
||||
$count = (Get-ChildItem $tripletDir -Force | Measure-Object).Count
|
||||
if ($count -gt 0) {
|
||||
$hasPackages = $true
|
||||
Write-Status "vcpkg cache populated for $triplet" "Success"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $hasPackages) {
|
||||
Write-Status "vcpkg/installed is empty. Run scripts\\setup-vcpkg-windows.ps1 to prefetch dependencies." "Warning"
|
||||
$script:warnings += "vcpkg cache empty - builds may spend extra time compiling dependencies."
|
||||
} else {
|
||||
$script:success += "vcpkg cache ready for Windows presets"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-CMakeCache {
|
||||
$buildDirs = @("build", "build-windows", "build-test", "build-ai", "out/build")
|
||||
$cacheIssues = $false
|
||||
@@ -202,7 +234,7 @@ function Clean-CMakeCache {
|
||||
$cleaned = $true
|
||||
Write-Status " ✓ Removed '$dir'" "Success"
|
||||
} catch {
|
||||
Write-Status " ✗ Failed to remove '$dir`: $_" "Error"
|
||||
Write-Status " ✗ Failed to remove '$dir': $_" "Error"
|
||||
$script:warnings += "Could not fully clean '$dir' (some files may be locked)"
|
||||
}
|
||||
}
|
||||
@@ -260,6 +292,222 @@ function Sync-GitSubmodules {
|
||||
}
|
||||
}
|
||||
|
||||
function Test-Ninja {
|
||||
Write-Status "Checking Ninja build system..." "Step"
|
||||
if (Test-Command "ninja") {
|
||||
try {
|
||||
$ninjaVersion = & ninja --version 2>&1
|
||||
Write-Status "Ninja found: version $ninjaVersion" "Success"
|
||||
$script:success += "Ninja build system available (required for win-dbg presets)"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Status "Ninja command exists but version check failed" "Warning"
|
||||
return $true
|
||||
}
|
||||
} else {
|
||||
Write-Status "Ninja not found in PATH" "Warning"
|
||||
$script:warnings += "Ninja not installed. Required for win-dbg, win-rel, win-ai presets. Use win-vs-* presets instead or install Ninja."
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-ClangCL {
|
||||
Write-Status "Checking clang-cl compiler..." "Step"
|
||||
if (Test-Command "clang-cl") {
|
||||
try {
|
||||
$clangVersion = & clang-cl --version 2>&1 | Select-Object -First 1
|
||||
Write-Status "clang-cl found: $clangVersion" "Success"
|
||||
$script:success += "clang-cl available (recommended for win-* presets)"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Status "clang-cl command exists but version check failed" "Warning"
|
||||
return $true
|
||||
}
|
||||
} else {
|
||||
Write-Status "clang-cl not found (LLVM toolset for MSVC missing?)" "Warning"
|
||||
$script:warnings += "Install the \"LLVM tools for Visual Studio\" component or enable clang-cl via Visual Studio Installer."
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-NASM {
|
||||
Write-Status "Checking NASM assembler..." "Step"
|
||||
if (Test-Command "nasm") {
|
||||
try {
|
||||
$nasmVersion = & nasm -version 2>&1 | Select-Object -First 1
|
||||
Write-Status "NASM found: $nasmVersion" "Success"
|
||||
$script:success += "NASM assembler available (needed for BoringSSL in gRPC)"
|
||||
return $true
|
||||
} catch {
|
||||
Write-Status "NASM command exists but version check failed" "Warning"
|
||||
return $true
|
||||
}
|
||||
} else {
|
||||
Write-Status "NASM not found in PATH (optional)" "Info"
|
||||
Write-Status "NASM is required for gRPC builds with BoringSSL. Install via: choco install nasm" "Info"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-VSCode {
|
||||
Write-Status "Checking Visual Studio Code installation..." "Step"
|
||||
|
||||
# Check for VSCode in common locations
|
||||
$vscodeLocations = @(
|
||||
"$env:LOCALAPPDATA\Programs\Microsoft VS Code\Code.exe",
|
||||
"$env:ProgramFiles\Microsoft VS Code\Code.exe",
|
||||
"$env:ProgramFiles(x86)\Microsoft VS Code\Code.exe"
|
||||
)
|
||||
|
||||
$vscodeFound = $false
|
||||
$vscodePath = $null
|
||||
|
||||
foreach ($location in $vscodeLocations) {
|
||||
if (Test-Path $location) {
|
||||
$vscodeFound = $true
|
||||
$vscodePath = $location
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $vscodeFound) {
|
||||
# Try to find it via command
|
||||
if (Test-Command "code") {
|
||||
$vscodeFound = $true
|
||||
$vscodePath = (Get-Command code).Source
|
||||
}
|
||||
}
|
||||
|
||||
if ($vscodeFound) {
|
||||
Write-Status "VS Code found: $vscodePath" "Success"
|
||||
|
||||
# Check for CMake Tools extension
|
||||
$extensionsOutput = & code --list-extensions 2>&1
|
||||
if ($extensionsOutput -match "ms-vscode.cmake-tools") {
|
||||
Write-Status "VS Code CMake Tools extension installed" "Success"
|
||||
$script:success += "VS Code with CMake Tools ready for development"
|
||||
} else {
|
||||
Write-Status "VS Code found but CMake Tools extension not installed" "Warning"
|
||||
$script:warnings += "Install CMake Tools extension: code --install-extension ms-vscode.cmake-tools"
|
||||
}
|
||||
return $true
|
||||
} else {
|
||||
Write-Status "VS Code not found (optional)" "Info"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-RomAssets {
|
||||
Write-Status "Checking for local Zelda 3 ROM assets..." "Step"
|
||||
$romPaths = @(
|
||||
"zelda3.sfc",
|
||||
"assets/zelda3.sfc",
|
||||
"assets/zelda3.yaze",
|
||||
"Roms/zelda3.sfc"
|
||||
)
|
||||
|
||||
foreach ($relativePath in $romPaths) {
|
||||
$fullPath = Join-Path $PSScriptRoot ".." $relativePath
|
||||
if (Test-Path $fullPath) {
|
||||
Write-Status "Found ROM asset at '$relativePath'" "Success"
|
||||
$script:success += "ROM asset available for GUI/editor smoke tests"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Write-Status "No ROM asset detected. Place a clean 'zelda3.sfc' in the repo root or assets/ directory." "Warning"
|
||||
$script:warnings += "ROM assets missing - GUI workflows that load ROMs will fail until one is provided."
|
||||
}
|
||||
|
||||
function Test-CMakePresets {
|
||||
Write-Status "Validating CMakePresets.json..." "Step"
|
||||
|
||||
$presetsPath = Join-Path $PSScriptRoot ".." "CMakePresets.json"
|
||||
|
||||
if (-not (Test-Path $presetsPath)) {
|
||||
Write-Status "CMakePresets.json not found!" "Error"
|
||||
$script:issuesFound += "CMakePresets.json missing from repository"
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
$presets = Get-Content $presetsPath -Raw | ConvertFrom-Json
|
||||
$configurePresets = $presets.configurePresets | Where-Object { $_.name -like "win-*" }
|
||||
|
||||
if ($configurePresets.Count -eq 0) {
|
||||
Write-Status "No Windows presets found in CMakePresets.json" "Error"
|
||||
$script:issuesFound += "CMakePresets.json has no Windows presets (win-dbg, win-rel, etc.)"
|
||||
return $false
|
||||
}
|
||||
|
||||
Write-Status "CMakePresets.json valid with $($configurePresets.Count) Windows presets" "Success"
|
||||
|
||||
# List available presets if verbose
|
||||
if ($Verbose) {
|
||||
Write-Status "Available Windows presets:" "Info"
|
||||
foreach ($preset in $configurePresets) {
|
||||
Write-Host " - $($preset.name): $($preset.description)" -ForegroundColor Gray
|
||||
}
|
||||
}
|
||||
|
||||
$script:success += "CMakePresets.json contains Windows build configurations"
|
||||
return $true
|
||||
|
||||
} catch {
|
||||
Write-Status "Failed to parse CMakePresets.json: $_" "Error"
|
||||
$script:issuesFound += "CMakePresets.json is invalid or corrupted"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-VisualStudioComponents {
|
||||
Write-Status "Checking Visual Studio C++ components..." "Step"
|
||||
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (-not (Test-Path $vswhere)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
# Check for specific components needed for C++
|
||||
$requiredComponents = @(
|
||||
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
|
||||
"Microsoft.VisualStudio.Component.Windows10SDK"
|
||||
)
|
||||
|
||||
$recommendedComponents = @(
|
||||
"Microsoft.VisualStudio.Component.VC.CMake.Project",
|
||||
"Microsoft.VisualStudio.Component.VC.Llvm.Clang",
|
||||
"Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset"
|
||||
)
|
||||
|
||||
$allComponentsPresent = $true
|
||||
|
||||
foreach ($component in $requiredComponents) {
|
||||
$result = & $vswhere -latest -requires $component -format value -property instanceId 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($result)) {
|
||||
Write-Status "Missing required component: $component" "Error"
|
||||
$script:issuesFound += "Visual Studio component not installed: $component"
|
||||
$allComponentsPresent = $false
|
||||
} elseif ($Verbose) {
|
||||
Write-Status "Component installed: $component" "Success"
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($component in $recommendedComponents) {
|
||||
$result = & $vswhere -latest -requires $component -format value -property instanceId 2>&1
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($result)) {
|
||||
Write-Status "Recommended component not installed: $component" "Info"
|
||||
if ($component -match "CMake") {
|
||||
$script:warnings += "Visual Studio CMake support not installed (recommended for IDE integration)"
|
||||
}
|
||||
} elseif ($Verbose) {
|
||||
Write-Status "Recommended component installed: $component" "Success"
|
||||
}
|
||||
}
|
||||
|
||||
return $allComponentsPresent
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main Verification Process
|
||||
# ============================================================================
|
||||
@@ -317,7 +565,12 @@ if (Test-Command "git") {
|
||||
$script:issuesFound += "Git not installed or not in PATH"
|
||||
}
|
||||
|
||||
# Step 3: Check Visual Studio
|
||||
# Step 3: Check Build Tools (Ninja, clang-cl, NASM)
|
||||
Test-Ninja | Out-Null
|
||||
Test-ClangCL | Out-Null
|
||||
Test-NASM | Out-Null
|
||||
|
||||
# Step 4: Check Visual Studio
|
||||
Write-Status "Checking Visual Studio installation..." "Step"
|
||||
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
if (Test-Path $vswhere) {
|
||||
@@ -329,6 +582,9 @@ if (Test-Path $vswhere) {
|
||||
Write-Status "Visual Studio with C++ Desktop workload found: version $vsVersion" "Success"
|
||||
Write-Status " Path: $vsPath" "Info"
|
||||
$script:success += "Visual Studio C++ workload detected (version $vsVersion)"
|
||||
|
||||
# Check for detailed components
|
||||
Test-VisualStudioComponents | Out-Null
|
||||
} else {
|
||||
Write-Status "Visual Studio found, but 'Desktop development with C++' workload is missing." "Error"
|
||||
$script:issuesFound += "Visual Studio 'Desktop development with C++' workload not installed."
|
||||
@@ -338,10 +594,17 @@ if (Test-Path $vswhere) {
|
||||
$script:issuesFound += "Visual Studio installation not detected."
|
||||
}
|
||||
|
||||
# Step 4: Check vcpkg
|
||||
Test-Vcpkg | Out-Null
|
||||
# Step 5: Check VSCode (optional)
|
||||
Test-VSCode | Out-Null
|
||||
|
||||
# Step 5: Check Git Submodules
|
||||
# Step 6: Check CMakePresets.json
|
||||
Test-CMakePresets | Out-Null
|
||||
|
||||
# Step 7: Check vcpkg
|
||||
Test-Vcpkg | Out-Null
|
||||
Test-VcpkgCache | Out-Null
|
||||
|
||||
# Step 8: Check Git Submodules
|
||||
Write-Status "Checking git submodules..." "Step"
|
||||
$submodulesOk = Test-GitSubmodules
|
||||
if ($submodulesOk) {
|
||||
@@ -359,7 +622,7 @@ if ($submodulesOk) {
|
||||
}
|
||||
}
|
||||
|
||||
# Step 6: Check CMake Cache
|
||||
# Step 9: Check CMake Cache
|
||||
Write-Status "Checking CMake cache..." "Step"
|
||||
if (Test-CMakeCache) {
|
||||
Write-Status "CMake cache appears up to date." "Success"
|
||||
@@ -376,6 +639,9 @@ if (Test-CMakeCache) {
|
||||
}
|
||||
}
|
||||
|
||||
# Step 10: Check ROM assets
|
||||
Test-RomAssets | Out-Null
|
||||
|
||||
# ============================================================================
|
||||
# Summary Report
|
||||
# ============================================================================
|
||||
@@ -438,6 +704,24 @@ if ($script:issuesFound.Count -gt 0) {
|
||||
Write-Host " git config --global core.longpaths true" -ForegroundColor Gray
|
||||
Write-Host " Or, run this script again with the '-FixIssues' flag.`n"
|
||||
}
|
||||
if ($script:warnings -join ' ' -match 'Ninja') {
|
||||
Write-Host " • Ninja build system:" -ForegroundColor White
|
||||
Write-Host " Ninja is required for win-dbg, win-rel, and win-ai presets." -ForegroundColor Gray
|
||||
Write-Host " Install via Chocolatey: choco install ninja" -ForegroundColor Gray
|
||||
Write-Host " Or use win-vs-* presets which use Visual Studio generator instead.`n"
|
||||
}
|
||||
if ($script:warnings -join ' ' -match 'CMake Tools') {
|
||||
Write-Host " • VS Code CMake Tools extension:" -ForegroundColor White
|
||||
Write-Host " For VS Code integration, install the CMake Tools extension:" -ForegroundColor Gray
|
||||
Write-Host " code --install-extension ms-vscode.cmake-tools" -ForegroundColor Gray
|
||||
Write-Host " Or install manually from the Extensions panel.`n"
|
||||
}
|
||||
if ($script:issuesFound -join ' ' -match 'CMakePresets') {
|
||||
Write-Host " • CMakePresets.json missing or invalid:" -ForegroundColor White
|
||||
Write-Host " This file is required for preset-based builds." -ForegroundColor Gray
|
||||
Write-Host " Ensure you're in the yaze repository root and the file exists." -ForegroundColor Gray
|
||||
Write-Host " Pull latest changes from git to get the updated presets.`n"
|
||||
}
|
||||
|
||||
Write-Host "If problems persist, check the build instructions in 'docs/B1-build-instructions.md'`n" -ForegroundColor Cyan
|
||||
|
||||
@@ -447,16 +731,85 @@ if ($script:issuesFound.Count -gt 0) {
|
||||
Write-Host "║ ✓ Build Environment Ready for Development! ║" -ForegroundColor Green
|
||||
Write-Host "╚════════════════════════════════════════════════════════════════╝`n" -ForegroundColor Green
|
||||
|
||||
Write-Host "Next Steps:" -ForegroundColor Cyan
|
||||
Write-Host " Visual Studio (Recommended):" -ForegroundColor White
|
||||
Write-Host " 1. Open Visual Studio 2022." -ForegroundColor Gray
|
||||
Write-Host " 2. Select 'File -> Open -> Folder...' and choose the 'yaze' directory." -ForegroundColor Gray
|
||||
Write-Host " 3. Select a Windows preset (e.g., 'win-dbg') from the dropdown." -ForegroundColor Gray
|
||||
Write-Host " 4. Press F5 to build and debug.`n" -ForegroundColor Gray
|
||||
# Determine which IDE and preset to recommend
|
||||
$hasVS = Test-Path "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
|
||||
$hasVSCode = Test-Command "code"
|
||||
$hasNinja = Test-Command "ninja"
|
||||
|
||||
Write-Host " Command Line:" -ForegroundColor White
|
||||
Write-Host " cmake --preset win-dbg" -ForegroundColor Gray
|
||||
Write-Host " cmake --build --preset win-dbg`n" -ForegroundColor Gray
|
||||
Write-Host "Next Steps:" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Recommend presets based on available tools
|
||||
if ($hasVS -and -not $hasNinja) {
|
||||
Write-Host " Recommended: Visual Studio Generator Presets" -ForegroundColor Yellow
|
||||
Write-Host " (Ninja not found - use win-vs-* presets)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
} elseif ($hasNinja) {
|
||||
Write-Host " Recommended: Ninja Generator Presets (faster builds)" -ForegroundColor Yellow
|
||||
Write-Host " (Ninja detected - use win-* presets)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Visual Studio instructions
|
||||
if ($hasVS) {
|
||||
Write-Host " Option 1: Visual Studio 2022 (Full IDE)" -ForegroundColor White
|
||||
Write-Host " 1. Open Visual Studio 2022" -ForegroundColor Gray
|
||||
Write-Host " 2. Select 'File -> Open -> Folder...' and choose the 'yaze' directory" -ForegroundColor Gray
|
||||
if ($hasNinja) {
|
||||
Write-Host " 3. Select preset: 'win-dbg' (Ninja) or 'win-vs-dbg' (VS Generator)" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host " 3. Select preset: 'win-vs-dbg' (install Ninja for win-dbg option)" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host " 4. Press F5 to build and debug" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# VSCode instructions
|
||||
if ($hasVSCode) {
|
||||
Write-Host " Option 2: Visual Studio Code (Lightweight)" -ForegroundColor White
|
||||
Write-Host " 1. Open folder in VS Code: code ." -ForegroundColor Gray
|
||||
Write-Host " 2. Install CMake Tools extension (if not installed)" -ForegroundColor Gray
|
||||
if ($hasNinja) {
|
||||
Write-Host " 3. Select CMake preset: 'win-dbg' from status bar" -ForegroundColor Gray
|
||||
} else {
|
||||
Write-Host " 3. Install Ninja first: choco install ninja" -ForegroundColor Gray
|
||||
Write-Host " Then select preset: 'win-dbg'" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host " 4. Press F7 to build, F5 to debug" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Command line instructions
|
||||
Write-Host " Option 3: Command Line" -ForegroundColor White
|
||||
if ($hasNinja) {
|
||||
Write-Host " # Basic build (Ninja generator - fast)" -ForegroundColor Gray
|
||||
Write-Host " cmake --preset win-dbg" -ForegroundColor Cyan
|
||||
Write-Host " cmake --build --preset win-dbg" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host " # With AI features (gRPC + JSON)" -ForegroundColor Gray
|
||||
Write-Host " cmake --preset win-ai" -ForegroundColor Cyan
|
||||
Write-Host " cmake --build --preset win-ai" -ForegroundColor Cyan
|
||||
} else {
|
||||
Write-Host " # Visual Studio generator (install Ninja for faster builds)" -ForegroundColor Gray
|
||||
Write-Host " cmake --preset win-vs-dbg" -ForegroundColor Cyan
|
||||
Write-Host " cmake --build --preset win-vs-dbg" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host " # Install Ninja for faster builds:" -ForegroundColor Yellow
|
||||
Write-Host " choco install ninja" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Available presets summary
|
||||
Write-Host " Available Presets:" -ForegroundColor White
|
||||
if ($hasNinja) {
|
||||
Write-Host " win-dbg - Debug build (Ninja)" -ForegroundColor Gray
|
||||
Write-Host " win-rel - Release build (Ninja)" -ForegroundColor Gray
|
||||
Write-Host " win-ai - Debug with AI/gRPC features (Ninja)" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host " win-vs-dbg - Debug build (Visual Studio)" -ForegroundColor Gray
|
||||
Write-Host " win-vs-rel - Release build (Visual Studio)" -ForegroundColor Gray
|
||||
Write-Host " win-vs-ai - Debug with AI/gRPC features (Visual Studio)" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -90,12 +90,14 @@ function get_cmake_version() {
|
||||
|
||||
function test_git_submodules() {
|
||||
local submodules=(
|
||||
"src/lib/SDL"
|
||||
"src/lib/abseil-cpp"
|
||||
"src/lib/asar"
|
||||
"src/lib/imgui"
|
||||
"third_party/json"
|
||||
"third_party/httplib"
|
||||
"ext/SDL"
|
||||
"src/lib/abseil-cpp"
|
||||
"ext/asar"
|
||||
"ext/imgui"
|
||||
"ext/json"
|
||||
"ext/httplib"
|
||||
"ext/imgui_test_engine"
|
||||
"ext/nativefiledialog-extended"
|
||||
)
|
||||
|
||||
local all_present=1
|
||||
@@ -112,7 +114,7 @@ function test_git_submodules() {
|
||||
}
|
||||
|
||||
function test_cmake_cache() {
|
||||
local build_dirs=("build" "build-test" "build-grpc-test" "build-rooms" "build-windows")
|
||||
local build_dirs=("build" "build_test" "build-test" "build-grpc-test" "build-rooms" "build-windows" "build_ai" "build_ai_claude" "build_agent" "build_ci")
|
||||
local cache_issues=0
|
||||
|
||||
for dir in "${build_dirs[@]}"; do
|
||||
@@ -189,7 +191,7 @@ function test_agent_folder_structure() {
|
||||
function clean_cmake_cache() {
|
||||
write_status "Cleaning CMake cache and build directories..." "Step"
|
||||
|
||||
local build_dirs=("build" "build-test" "build-grpc-test" "build-rooms" "build-windows")
|
||||
local build_dirs=("build" "build_test" "build-test" "build-grpc-test" "build-rooms" "build-windows" "build_ai" "build_ai_claude" "build_agent" "build_ci")
|
||||
local cleaned=0
|
||||
|
||||
for dir in "${build_dirs[@]}"; do
|
||||
@@ -242,14 +244,14 @@ function test_dependency_compatibility() {
|
||||
write_status "Testing dependency configuration..." "Step"
|
||||
|
||||
# Check httplib configuration
|
||||
if [[ -f "third_party/httplib/CMakeLists.txt" ]]; then
|
||||
write_status "httplib found in third_party" "Success"
|
||||
if [[ -f "ext/httplib/CMakeLists.txt" ]]; then
|
||||
write_status "httplib found in ext/" "Success"
|
||||
SUCCESS+=("httplib header-only library available")
|
||||
fi
|
||||
|
||||
# Check json library
|
||||
if [[ -d "third_party/json/include" ]]; then
|
||||
write_status "nlohmann/json found in third_party" "Success"
|
||||
if [[ -d "ext/json/include" ]]; then
|
||||
write_status "nlohmann/json found in ext/" "Success"
|
||||
SUCCESS+=("nlohmann/json header-only library available")
|
||||
fi
|
||||
}
|
||||
@@ -455,4 +457,4 @@ else
|
||||
echo ""
|
||||
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
323
scripts/verify-symbols.sh
Executable file
323
scripts/verify-symbols.sh
Executable file
@@ -0,0 +1,323 @@
|
||||
#!/bin/bash
|
||||
# Symbol Conflict Detector for YAZE
|
||||
# Detects ODR violations and duplicate symbols across libraries
|
||||
# Prevents link-time failures that only appear in CI
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Script directory
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
# Configuration
|
||||
BUILD_DIR="${BUILD_DIR:-$PROJECT_ROOT/build}"
|
||||
VERBOSE="${VERBOSE:-0}"
|
||||
SHOW_ALL="${SHOW_ALL:-0}"
|
||||
|
||||
# Statistics
|
||||
TOTAL_LIBRARIES=0
|
||||
DUPLICATE_SYMBOLS=0
|
||||
POTENTIAL_ODR=0
|
||||
|
||||
# Helper functions
|
||||
print_header() {
|
||||
echo -e "\n${BLUE}===${NC} $1 ${BLUE}===${NC}"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}⚠${NC} $1"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}ℹ${NC} $1"
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
usage() {
|
||||
cat << EOF
|
||||
Usage: $0 [OPTIONS]
|
||||
|
||||
Symbol conflict detector that scans built libraries for duplicate symbols
|
||||
and ODR violations.
|
||||
|
||||
OPTIONS:
|
||||
--build-dir PATH Build directory (default: build)
|
||||
--verbose Show detailed symbol information
|
||||
--show-all Show all symbols (including safe duplicates)
|
||||
-h, --help Show this help message
|
||||
|
||||
EXAMPLES:
|
||||
$0 # Scan default build directory
|
||||
$0 --build-dir build_test # Scan specific build directory
|
||||
$0 --verbose # Show detailed output
|
||||
$0 --show-all # Show all symbols (verbose)
|
||||
|
||||
WHAT IT DETECTS:
|
||||
✓ Duplicate symbol definitions (ODR violations)
|
||||
✓ FLAGS_* conflicts (gflags issues)
|
||||
✓ Multiple weak symbols
|
||||
✓ Template instantiation conflicts
|
||||
|
||||
SAFE SYMBOLS (ignored):
|
||||
- vtable symbols (typeinfo)
|
||||
- guard variables (__guard)
|
||||
- std::* standard library symbols
|
||||
- Abseil inline namespaces
|
||||
|
||||
PLATFORMS:
|
||||
- macOS: Uses 'nm' and 'c++filt'
|
||||
- Linux: Uses 'nm' and 'c++filt'
|
||||
- Windows: Uses 'dumpbin' (not implemented yet)
|
||||
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--build-dir)
|
||||
BUILD_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=1
|
||||
shift
|
||||
;;
|
||||
--show-all)
|
||||
SHOW_ALL=1
|
||||
VERBOSE=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
print_error "Unknown option: $1"
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Check for required tools
|
||||
check_tools() {
|
||||
local MISSING=0
|
||||
|
||||
if ! command -v nm &> /dev/null; then
|
||||
print_error "nm not found (required for symbol inspection)"
|
||||
MISSING=1
|
||||
fi
|
||||
|
||||
if ! command -v c++filt &> /dev/null; then
|
||||
print_warning "c++filt not found (symbol demangling disabled)"
|
||||
fi
|
||||
|
||||
if [[ $MISSING -eq 1 ]]; then
|
||||
print_info "Install build tools: xcode-select --install (macOS) or build-essential (Linux)"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Filter out safe duplicate symbols
|
||||
is_safe_symbol() {
|
||||
local symbol="$1"
|
||||
|
||||
# Safe patterns (these are expected to have duplicates)
|
||||
local SAFE_PATTERNS=(
|
||||
"^typeinfo" # RTTI typeinfo
|
||||
"^vtable" # Virtual tables
|
||||
"^guard variable" # Static initialization guards
|
||||
"^std::" # Standard library
|
||||
"^__cxx" # C++ runtime
|
||||
"^__gnu" # GNU extensions
|
||||
"^absl::lts_" # Abseil LTS inline namespace (versioning)
|
||||
"^non-virtual thunk" # Virtual inheritance thunks
|
||||
"^construction vtable" # Construction virtual tables
|
||||
)
|
||||
|
||||
for pattern in "${SAFE_PATTERNS[@]}"; do
|
||||
if [[ "$symbol" =~ $pattern ]]; then
|
||||
return 0 # Safe symbol
|
||||
fi
|
||||
done
|
||||
|
||||
return 1 # Potentially problematic symbol
|
||||
}
|
||||
|
||||
# Check if symbol is a FLAGS definition
|
||||
is_flags_symbol() {
|
||||
local symbol="$1"
|
||||
if [[ "$symbol" =~ ^FLAGS_ ]] || [[ "$symbol" =~ fL[A-Z] ]]; then
|
||||
return 0 # FLAGS symbol
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
# Extract and analyze symbols
|
||||
analyze_symbols() {
|
||||
print_header "Symbol Conflict Detection"
|
||||
|
||||
# Find all static libraries
|
||||
local LIBRARIES=()
|
||||
if [[ -d "$BUILD_DIR/lib" ]]; then
|
||||
while IFS= read -r -d '' lib; do
|
||||
LIBRARIES+=("$lib")
|
||||
done < <(find "$BUILD_DIR/lib" -name "*.a" -print0 2>/dev/null)
|
||||
fi
|
||||
if [[ -d "$BUILD_DIR" ]]; then
|
||||
while IFS= read -r -d '' lib; do
|
||||
LIBRARIES+=("$lib")
|
||||
done < <(find "$BUILD_DIR" -name "libyaze*.a" -print0 2>/dev/null)
|
||||
fi
|
||||
|
||||
TOTAL_LIBRARIES=${#LIBRARIES[@]}
|
||||
|
||||
if [[ $TOTAL_LIBRARIES -eq 0 ]]; then
|
||||
print_error "No static libraries found in $BUILD_DIR"
|
||||
print_info "Build the project first: cmake --build $BUILD_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Found $TOTAL_LIBRARIES libraries to scan"
|
||||
echo ""
|
||||
|
||||
# Collect all symbols across libraries
|
||||
declare -A SYMBOL_MAP # symbol -> list of libraries
|
||||
local TEMP_FILE=$(mktemp)
|
||||
|
||||
print_info "Scanning libraries for symbols..."
|
||||
for lib in "${LIBRARIES[@]}"; do
|
||||
local lib_name=$(basename "$lib")
|
||||
if [[ $VERBOSE -eq 1 ]]; then
|
||||
print_info " Scanning: $lib_name"
|
||||
fi
|
||||
|
||||
# Extract defined symbols (T = text/code, D = data, R = read-only data, B = BSS)
|
||||
# Filter for global symbols (uppercase letters = global, lowercase = local)
|
||||
nm -g "$lib" 2>/dev/null | grep -E ' [TDRB] ' | while read -r addr type symbol; do
|
||||
# Demangle C++ symbols if possible
|
||||
if command -v c++filt &> /dev/null; then
|
||||
symbol=$(echo "$symbol" | c++filt)
|
||||
fi
|
||||
|
||||
# Record symbol and which library defines it
|
||||
echo "$symbol|$lib_name" >> "$TEMP_FILE"
|
||||
done
|
||||
done
|
||||
|
||||
print_info "Analyzing symbol duplicates..."
|
||||
echo ""
|
||||
|
||||
# Find duplicate symbols
|
||||
local DUPLICATES=$(mktemp)
|
||||
sort "$TEMP_FILE" | uniq -w 200 -D > "$DUPLICATES"
|
||||
|
||||
# Group duplicates by symbol
|
||||
local CURRENT_SYMBOL=""
|
||||
local LIBS=()
|
||||
local HAS_DUPLICATES=0
|
||||
|
||||
while IFS='|' read -r symbol lib; do
|
||||
if [[ "$symbol" != "$CURRENT_SYMBOL" ]]; then
|
||||
# Process previous symbol if it had duplicates
|
||||
if [[ ${#LIBS[@]} -gt 1 ]]; then
|
||||
if [[ $SHOW_ALL -eq 1 ]] || ! is_safe_symbol "$CURRENT_SYMBOL"; then
|
||||
HAS_DUPLICATES=1
|
||||
((DUPLICATE_SYMBOLS++))
|
||||
|
||||
# Check if it's a FLAGS symbol
|
||||
if is_flags_symbol "$CURRENT_SYMBOL"; then
|
||||
((POTENTIAL_ODR++))
|
||||
print_error "FLAGS symbol conflict: $CURRENT_SYMBOL"
|
||||
else
|
||||
print_warning "Duplicate symbol: $CURRENT_SYMBOL"
|
||||
fi
|
||||
|
||||
for lib in "${LIBS[@]}"; do
|
||||
echo " → $lib"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Start new symbol
|
||||
CURRENT_SYMBOL="$symbol"
|
||||
LIBS=("$lib")
|
||||
else
|
||||
LIBS+=("$lib")
|
||||
fi
|
||||
done < "$DUPLICATES"
|
||||
|
||||
# Process last symbol
|
||||
if [[ ${#LIBS[@]} -gt 1 ]]; then
|
||||
if [[ $SHOW_ALL -eq 1 ]] || ! is_safe_symbol "$CURRENT_SYMBOL"; then
|
||||
HAS_DUPLICATES=1
|
||||
((DUPLICATE_SYMBOLS++))
|
||||
|
||||
if is_flags_symbol "$CURRENT_SYMBOL"; then
|
||||
((POTENTIAL_ODR++))
|
||||
print_error "FLAGS symbol conflict: $CURRENT_SYMBOL"
|
||||
else
|
||||
print_warning "Duplicate symbol: $CURRENT_SYMBOL"
|
||||
fi
|
||||
|
||||
for lib in "${LIBS[@]}"; do
|
||||
echo " → $lib"
|
||||
done
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f "$TEMP_FILE" "$DUPLICATES"
|
||||
|
||||
# Report results
|
||||
print_header "Summary"
|
||||
print_info "Libraries scanned: $TOTAL_LIBRARIES"
|
||||
|
||||
if [[ $POTENTIAL_ODR -gt 0 ]]; then
|
||||
print_error "FLAGS symbol conflicts: $POTENTIAL_ODR (ODR violations)"
|
||||
print_info "These are gflags-related conflicts that will cause link errors on Linux"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ $DUPLICATE_SYMBOLS -gt 0 ]]; then
|
||||
print_warning "Duplicate symbols: $DUPLICATE_SYMBOLS"
|
||||
if [[ $SHOW_ALL -eq 0 ]]; then
|
||||
print_info "Most duplicates are safe (vtables, typeinfo, etc.)"
|
||||
print_info "Run with --show-all to see all duplicates"
|
||||
fi
|
||||
print_success "No critical ODR violations detected"
|
||||
return 0
|
||||
else
|
||||
print_success "No duplicate symbols detected"
|
||||
return 0
|
||||
fi
|
||||
}
|
||||
|
||||
# Main
|
||||
cd "$PROJECT_ROOT"
|
||||
|
||||
if [[ ! -d "$BUILD_DIR" ]]; then
|
||||
print_error "Build directory not found: $BUILD_DIR"
|
||||
print_info "Build the project first: cmake --preset <preset> && cmake --build build"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
check_tools
|
||||
analyze_symbols
|
||||
exit $?
|
||||
422
scripts/visualize-deps.py
Executable file
422
scripts/visualize-deps.py
Executable file
@@ -0,0 +1,422 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dependency Graph Visualizer
|
||||
Parses CMake targets and generates dependency graphs
|
||||
|
||||
Usage:
|
||||
python3 scripts/visualize-deps.py [build_directory] [--format FORMAT]
|
||||
|
||||
Formats:
|
||||
- graphviz: DOT format for graphviz (default)
|
||||
- mermaid: Mermaid diagram format
|
||||
- text: Simple text tree
|
||||
|
||||
Exit codes:
|
||||
0 - Success
|
||||
1 - Error (build directory not found, etc.)
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Dict, Set, List, Tuple
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
class Colors:
|
||||
"""ANSI color codes"""
|
||||
RESET = '\033[0m'
|
||||
RED = '\033[31m'
|
||||
GREEN = '\033[32m'
|
||||
YELLOW = '\033[33m'
|
||||
BLUE = '\033[34m'
|
||||
MAGENTA = '\033[35m'
|
||||
CYAN = '\033[36m'
|
||||
|
||||
|
||||
class DependencyGraph:
|
||||
"""Parse and analyze CMake dependency graph"""
|
||||
|
||||
def __init__(self, build_dir: Path):
|
||||
self.build_dir = build_dir
|
||||
self.targets: Dict[str, Set[str]] = defaultdict(set)
|
||||
self.target_types: Dict[str, str] = {}
|
||||
self.circular_deps: List[List[str]] = []
|
||||
|
||||
def parse_cmake_files(self):
|
||||
"""Parse CMakeLists.txt files to extract targets and dependencies"""
|
||||
print(f"{Colors.BLUE}Parsing CMake configuration...{Colors.RESET}")
|
||||
|
||||
# Try to parse from CMake's dependency info
|
||||
dep_info_dir = self.build_dir / "CMakeFiles" / "TargetDirectories.txt"
|
||||
|
||||
if dep_info_dir.exists():
|
||||
with open(dep_info_dir, 'r') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line:
|
||||
# Extract target name from path
|
||||
match = re.search(r'CMakeFiles/([^/]+)\.dir', line)
|
||||
if match:
|
||||
target_name = match.group(1)
|
||||
self.targets[target_name] = set()
|
||||
self.target_types[target_name] = "UNKNOWN"
|
||||
|
||||
# Try to extract more info from compile_commands.json
|
||||
compile_commands = self.build_dir / "compile_commands.json"
|
||||
if compile_commands.exists():
|
||||
try:
|
||||
with open(compile_commands, 'r') as f:
|
||||
commands = json.load(f)
|
||||
|
||||
for cmd in commands:
|
||||
file_path = cmd.get('file', '')
|
||||
# Try to infer target from file path
|
||||
if '/src/' in file_path:
|
||||
parts = file_path.split('/src/')[-1].split('/')
|
||||
if len(parts) > 1:
|
||||
target = parts[0]
|
||||
if target not in self.targets:
|
||||
self.targets[target] = set()
|
||||
self.target_types[target] = "LIBRARY"
|
||||
|
||||
except json.JSONDecodeError:
|
||||
print(f"{Colors.YELLOW}⚠ Could not parse compile_commands.json{Colors.RESET}")
|
||||
|
||||
# Parse dependency information from generated cmake files
|
||||
self._parse_cmake_depends()
|
||||
|
||||
print(f"{Colors.GREEN}✓ Found {len(self.targets)} targets{Colors.RESET}")
|
||||
|
||||
def _parse_cmake_depends(self):
|
||||
"""Parse CMake depend.make files for dependency information"""
|
||||
cmake_files_dir = self.build_dir / "CMakeFiles"
|
||||
|
||||
if not cmake_files_dir.exists():
|
||||
return
|
||||
|
||||
# Look for depend.make files
|
||||
for target_dir in cmake_files_dir.glob("*.dir"):
|
||||
target_name = target_dir.name.replace('.dir', '')
|
||||
|
||||
depend_make = target_dir / "depend.make"
|
||||
if depend_make.exists():
|
||||
try:
|
||||
with open(depend_make, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Extract dependencies from depend.make
|
||||
# Format: target_name.dir/file.cc.o: path/to/header.h
|
||||
for line in content.split('\n'):
|
||||
if ':' in line and not line.startswith('#'):
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 2:
|
||||
deps = parts[1].strip()
|
||||
# Look for other target dependencies
|
||||
for other_target in self.targets.keys():
|
||||
if other_target in deps and other_target != target_name:
|
||||
self.targets[target_name].add(other_target)
|
||||
|
||||
except Exception as e:
|
||||
print(f"{Colors.YELLOW}⚠ Error parsing {depend_make}: {e}{Colors.RESET}")
|
||||
|
||||
# Also check link.txt for library dependencies
|
||||
for target_dir in cmake_files_dir.glob("*.dir"):
|
||||
target_name = target_dir.name.replace('.dir', '')
|
||||
link_txt = target_dir / "link.txt"
|
||||
|
||||
if link_txt.exists():
|
||||
try:
|
||||
with open(link_txt, 'r') as f:
|
||||
link_cmd = f.read()
|
||||
|
||||
# Parse linked libraries
|
||||
for other_target in self.targets.keys():
|
||||
if other_target in link_cmd and other_target != target_name:
|
||||
self.targets[target_name].add(other_target)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def detect_circular_dependencies(self) -> List[List[str]]:
|
||||
"""Detect circular dependencies using DFS"""
|
||||
print(f"{Colors.BLUE}Checking for circular dependencies...{Colors.RESET}")
|
||||
|
||||
visited = set()
|
||||
rec_stack = set()
|
||||
cycles = []
|
||||
|
||||
def dfs(node: str, path: List[str]):
|
||||
visited.add(node)
|
||||
rec_stack.add(node)
|
||||
path.append(node)
|
||||
|
||||
for neighbor in self.targets.get(node, []):
|
||||
if neighbor not in visited:
|
||||
dfs(neighbor, path.copy())
|
||||
elif neighbor in rec_stack:
|
||||
# Found a cycle
|
||||
cycle_start = path.index(neighbor)
|
||||
cycle = path[cycle_start:] + [neighbor]
|
||||
cycles.append(cycle)
|
||||
|
||||
rec_stack.remove(node)
|
||||
|
||||
for target in self.targets:
|
||||
if target not in visited:
|
||||
dfs(target, [])
|
||||
|
||||
self.circular_deps = cycles
|
||||
|
||||
if cycles:
|
||||
print(f"{Colors.RED}✗ Found {len(cycles)} circular dependencies{Colors.RESET}")
|
||||
for cycle in cycles:
|
||||
print(f" {' -> '.join(cycle)}")
|
||||
else:
|
||||
print(f"{Colors.GREEN}✓ No circular dependencies detected{Colors.RESET}")
|
||||
|
||||
return cycles
|
||||
|
||||
def generate_graphviz(self, output_file: Path = None):
|
||||
"""Generate GraphViz DOT format"""
|
||||
print(f"{Colors.BLUE}Generating GraphViz diagram...{Colors.RESET}")
|
||||
|
||||
dot = ["digraph Dependencies {"]
|
||||
dot.append(" rankdir=LR;")
|
||||
dot.append(" node [shape=box, style=rounded];")
|
||||
dot.append("")
|
||||
|
||||
# Define node styles
|
||||
dot.append(" // Node definitions")
|
||||
for target, target_type in self.target_types.items():
|
||||
if target_type == "EXECUTABLE":
|
||||
color = "lightblue"
|
||||
elif target_type == "LIBRARY":
|
||||
color = "lightgreen"
|
||||
else:
|
||||
color = "lightgray"
|
||||
|
||||
safe_name = target.replace("-", "_").replace("::", "_")
|
||||
dot.append(f' {safe_name} [label="{target}", fillcolor={color}, style="rounded,filled"];')
|
||||
|
||||
dot.append("")
|
||||
dot.append(" // Dependencies")
|
||||
|
||||
# Add edges
|
||||
for target, deps in self.targets.items():
|
||||
safe_target = target.replace("-", "_").replace("::", "_")
|
||||
for dep in deps:
|
||||
safe_dep = dep.replace("-", "_").replace("::", "_")
|
||||
|
||||
# Highlight circular dependencies in red
|
||||
is_circular = any(
|
||||
target in cycle and dep in cycle
|
||||
for cycle in self.circular_deps
|
||||
)
|
||||
|
||||
if is_circular:
|
||||
dot.append(f' {safe_target} -> {safe_dep} [color=red, penwidth=2];')
|
||||
else:
|
||||
dot.append(f' {safe_target} -> {safe_dep};')
|
||||
|
||||
dot.append("}")
|
||||
|
||||
result = "\n".join(dot)
|
||||
|
||||
if output_file:
|
||||
output_file.write_text(result)
|
||||
print(f"{Colors.GREEN}✓ GraphViz diagram written to {output_file}{Colors.RESET}")
|
||||
else:
|
||||
print(result)
|
||||
|
||||
return result
|
||||
|
||||
def generate_mermaid(self, output_file: Path = None):
|
||||
"""Generate Mermaid diagram format"""
|
||||
print(f"{Colors.BLUE}Generating Mermaid diagram...{Colors.RESET}")
|
||||
|
||||
mermaid = ["graph LR"]
|
||||
|
||||
# Add nodes and edges
|
||||
for target, deps in self.targets.items():
|
||||
safe_target = target.replace("-", "_").replace("::", "_")
|
||||
|
||||
for dep in deps:
|
||||
safe_dep = dep.replace("-", "_").replace("::", "_")
|
||||
|
||||
# Highlight circular dependencies
|
||||
is_circular = any(
|
||||
target in cycle and dep in cycle
|
||||
for cycle in self.circular_deps
|
||||
)
|
||||
|
||||
if is_circular:
|
||||
mermaid.append(f' {safe_target}-->|CIRCULAR|{safe_dep}')
|
||||
mermaid.append(f' style {safe_target} fill:#ff6b6b')
|
||||
mermaid.append(f' style {safe_dep} fill:#ff6b6b')
|
||||
else:
|
||||
mermaid.append(f' {safe_target}-->{safe_dep}')
|
||||
|
||||
result = "\n".join(mermaid)
|
||||
|
||||
if output_file:
|
||||
output_file.write_text(result)
|
||||
print(f"{Colors.GREEN}✓ Mermaid diagram written to {output_file}{Colors.RESET}")
|
||||
else:
|
||||
print(result)
|
||||
|
||||
return result
|
||||
|
||||
def generate_text_tree(self, output_file: Path = None):
|
||||
"""Generate simple text tree representation"""
|
||||
print(f"{Colors.BLUE}Generating text tree...{Colors.RESET}")
|
||||
|
||||
lines = []
|
||||
visited = set()
|
||||
|
||||
def print_tree(target: str, indent: int = 0, prefix: str = ""):
|
||||
if target in visited:
|
||||
lines.append(f"{prefix}├── {target} (circular)")
|
||||
return
|
||||
|
||||
visited.add(target)
|
||||
deps = list(self.targets.get(target, []))
|
||||
|
||||
lines.append(f"{prefix}├── {target}")
|
||||
|
||||
for i, dep in enumerate(deps):
|
||||
is_last = i == len(deps) - 1
|
||||
new_prefix = prefix + (" " if is_last else "│ ")
|
||||
print_tree(dep, indent + 1, new_prefix)
|
||||
|
||||
# Find root targets (targets with no incoming dependencies)
|
||||
all_deps = set()
|
||||
for deps in self.targets.values():
|
||||
all_deps.update(deps)
|
||||
|
||||
roots = [t for t in self.targets.keys() if t not in all_deps]
|
||||
|
||||
if not roots:
|
||||
# If no clear roots, just use all targets
|
||||
roots = list(self.targets.keys())
|
||||
|
||||
lines.append("Dependency Tree:")
|
||||
for root in roots[:10]: # Limit to first 10 roots
|
||||
visited = set()
|
||||
print_tree(root)
|
||||
lines.append("")
|
||||
|
||||
result = "\n".join(lines)
|
||||
|
||||
if output_file:
|
||||
output_file.write_text(result)
|
||||
print(f"{Colors.GREEN}✓ Text tree written to {output_file}{Colors.RESET}")
|
||||
else:
|
||||
print(result)
|
||||
|
||||
return result
|
||||
|
||||
def print_statistics(self):
|
||||
"""Print graph statistics"""
|
||||
print(f"\n{Colors.BLUE}=== Dependency Statistics ==={Colors.RESET}")
|
||||
|
||||
total_targets = len(self.targets)
|
||||
total_edges = sum(len(deps) for deps in self.targets.values())
|
||||
avg_deps = total_edges / total_targets if total_targets > 0 else 0
|
||||
|
||||
print(f"Total targets: {total_targets}")
|
||||
print(f"Total dependencies: {total_edges}")
|
||||
print(f"Average dependencies per target: {avg_deps:.2f}")
|
||||
|
||||
# Find most connected targets
|
||||
dep_counts = [(t, len(deps)) for t, deps in self.targets.items()]
|
||||
dep_counts.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
print(f"\n{Colors.CYAN}Most connected targets:{Colors.RESET}")
|
||||
for target, count in dep_counts[:5]:
|
||||
print(f" {target}: {count} dependencies")
|
||||
|
||||
# Find targets with no dependencies
|
||||
isolated = [t for t, deps in self.targets.items() if len(deps) == 0]
|
||||
if isolated:
|
||||
print(f"\n{Colors.YELLOW}Isolated targets (no dependencies):{Colors.RESET}")
|
||||
for target in isolated[:10]:
|
||||
print(f" {target}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Visualize CMake dependency graph")
|
||||
parser.add_argument(
|
||||
"build_dir",
|
||||
nargs="?",
|
||||
default="build",
|
||||
help="Build directory (default: build)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--format",
|
||||
choices=["graphviz", "mermaid", "text"],
|
||||
default="graphviz",
|
||||
help="Output format (default: graphviz)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
type=Path,
|
||||
help="Output file (default: stdout)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--stats",
|
||||
action="store_true",
|
||||
help="Show statistics"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
build_dir = Path(args.build_dir)
|
||||
|
||||
if not build_dir.exists():
|
||||
print(f"{Colors.RED}✗ Build directory not found: {build_dir}{Colors.RESET}")
|
||||
print("Run cmake configure first: cmake --preset <preset-name>")
|
||||
sys.exit(1)
|
||||
|
||||
if not (build_dir / "CMakeCache.txt").exists():
|
||||
print(f"{Colors.RED}✗ CMakeCache.txt not found in {build_dir}{Colors.RESET}")
|
||||
print("Configuration incomplete - run cmake configure first")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"{Colors.BLUE}=== CMake Dependency Visualizer ==={Colors.RESET}")
|
||||
print(f"Build directory: {build_dir}\n")
|
||||
|
||||
graph = DependencyGraph(build_dir)
|
||||
graph.parse_cmake_files()
|
||||
graph.detect_circular_dependencies()
|
||||
|
||||
if args.stats:
|
||||
graph.print_statistics()
|
||||
|
||||
# Generate output
|
||||
output_file = args.output
|
||||
|
||||
if args.format == "graphviz":
|
||||
if not output_file:
|
||||
output_file = Path("dependencies.dot")
|
||||
graph.generate_graphviz(output_file)
|
||||
print(f"\nTo render: dot -Tpng {output_file} -o dependencies.png")
|
||||
|
||||
elif args.format == "mermaid":
|
||||
if not output_file:
|
||||
output_file = Path("dependencies.mmd")
|
||||
graph.generate_mermaid(output_file)
|
||||
print(f"\nView at: https://mermaid.live/edit")
|
||||
|
||||
elif args.format == "text":
|
||||
graph.generate_text_tree(output_file)
|
||||
|
||||
print(f"\n{Colors.GREEN}✓ Dependency analysis complete{Colors.RESET}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user