backend-infra-engineer: Release v0.3.3 snapshot

This commit is contained in:
scawful
2025-11-21 21:35:50 -05:00
parent 3d71417f62
commit 476dd1cd1c
818 changed files with 65706 additions and 35514 deletions

View File

@@ -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)

View File

@@ -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
View 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.

View 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") + ")")
'

View 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
View 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"

View 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
View 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

View 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"

View 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
View 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
View 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
View 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
View 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
View 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 ""

View 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
View 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
View 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
View 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 "$@"

View File

@@ -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
View 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
View 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
View 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
View 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 ""

View 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
View 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 "$@"

View File

@@ -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
}

View File

@@ -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
View 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
View 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()