# Docker build and run commands # Usage: just docker::build, just docker::build-all, etc. # Default Docker image name (matches Docker Hub and PyPI package name) image_name := "freecad-robust-mcp" registry := "spkane" gui_test_image := "freecad-gui-test" # Project root directory (justfile_directory() returns the main justfile's directory) project_root := justfile_directory() # Build Docker image for local architecture only build: docker build -t {{image_name}} {{project_root}} # Build Docker image with specific tag build-tag tag: docker build --load -t {{image_name}}:{{tag}} {{project_root}} # Validate multi-architecture build (amd64 and arm64) # This is a dry-run that verifies both architectures compile successfully. # The build populates the builder cache but does NOT produce a usable image. # Use cases: # - CI validation before pushing (verify PR doesn't break either arch) # - Local verification that changes build on both architectures # To actually publish a multi-arch image, use: just docker::build-push build-multi: setup-buildx docker buildx build --platform linux/amd64,linux/arm64 -t {{image_name}} {{project_root}} # Build and push multi-architecture image to registry (produces usable multi-arch image) build-push tag="latest": setup-buildx docker buildx build --platform linux/amd64,linux/arm64 \ -t {{registry}}/{{image_name}}:{{tag}} \ --push {{project_root}} # Build and load image for current architecture using buildx build-load: setup-buildx #!/usr/bin/env bash set -euo pipefail # Detect current architecture ARCH=$(uname -m) case "$ARCH" in x86_64) PLATFORM="linux/amd64" ;; aarch64|arm64) PLATFORM="linux/arm64" ;; *) echo "Unsupported architecture: $ARCH"; exit 1 ;; esac echo "Building for detected architecture: $PLATFORM" docker buildx build --platform "$PLATFORM" \ -t {{image_name}} \ --load {{project_root}} # Run the Docker container (connects to FreeCAD on host) run: docker run --rm -i \ --add-host=host.docker.internal:host-gateway \ -e FREECAD_MODE=xmlrpc \ -e FREECAD_SOCKET_HOST=host.docker.internal \ {{image_name}} # Run with custom environment variables run-env *args: docker run --rm -i \ --add-host=host.docker.internal:host-gateway \ {{args}} \ {{image_name}} # Run container interactively with shell shell: docker run --rm -it \ --add-host=host.docker.internal:host-gateway \ -e FREECAD_MODE=xmlrpc \ -e FREECAD_SOCKET_HOST=host.docker.internal \ --entrypoint /bin/sh \ {{image_name}} # Show image size and layers inspect: docker images {{image_name}} @echo "" docker history {{image_name}} # Remove local Docker image clean: docker rmi {{image_name}} 2>/dev/null || true docker rmi {{registry}}/{{image_name}} 2>/dev/null || true # Remove Docker images and build cache clean-all: docker rmi {{image_name}} 2>/dev/null || true docker rmi {{registry}}/{{image_name}} 2>/dev/null || true docker builder prune -f @echo "Docker images and build cache cleaned." # Scan Docker image for vulnerabilities (warn on all severities) scan: #!/usr/bin/env bash set -euo pipefail echo "Scanning {{image_name}} for vulnerabilities..." echo "" trivy image --severity HIGH,CRITICAL {{image_name}} echo "" echo "Scanning for MEDIUM/LOW (informational)..." trivy image --severity MEDIUM,LOW {{image_name}} || true # Scan Docker image with strict settings (fail on HIGH or CRITICAL) scan-strict: #!/usr/bin/env bash set -euo pipefail echo "Scanning {{image_name}} for HIGH/CRITICAL vulnerabilities..." echo "(Build will fail if any HIGH or CRITICAL CVEs are found)" echo "" trivy image --severity HIGH,CRITICAL --exit-code 1 {{image_name}} echo "" echo "✓ No HIGH or CRITICAL vulnerabilities found!" echo "" echo "Scanning for MEDIUM/LOW (informational only)..." trivy image --severity MEDIUM,LOW --exit-code 0 {{image_name}} || true # Scan Docker image and output SARIF report scan-sarif output="trivy-results.sarif": trivy image --format sarif --output {{project_root}}/{{output}} {{image_name}} @echo "SARIF report written to {{project_root}}/{{output}}" # Create and configure buildx builder for multi-arch builds setup-buildx: #!/usr/bin/env bash set -euo pipefail if ! docker buildx inspect freecad-builder > /dev/null 2>&1; then echo "Creating buildx builder 'freecad-builder'..." docker buildx create --name freecad-builder --use --bootstrap else echo "Builder 'freecad-builder' already exists, using it..." docker buildx use freecad-builder fi docker buildx inspect --bootstrap # Test Docker container integration with FreeCAD # This builds the image, starts FreeCAD with the bridge, runs the container, # and verifies communication is working correctly. test: #!/usr/bin/env bash set -euo pipefail # Initialize variables for cleanup STARTED_FREECAD=false FREECAD_PID="" FREECAD_LOG="" MCP_INPUT="" # Comprehensive cleanup trap for all exit paths cleanup() { [ -n "${MCP_INPUT:-}" ] && rm -f "$MCP_INPUT" 2>/dev/null || true [ -n "${FREECAD_LOG:-}" ] && rm -f "$FREECAD_LOG" 2>/dev/null || true if [ "$STARTED_FREECAD" = true ] && [ -n "${FREECAD_PID:-}" ]; then kill "$FREECAD_PID" 2>/dev/null || true fi } trap cleanup EXIT INT TERM echo "==========================================" echo "Docker Integration Test" echo "==========================================" echo "" # Function to check if XML-RPC server is ready using a simple POST request check_xmlrpc() { # Send a minimal XML-RPC system.listMethods call curl -s --max-time 2 -X POST \ -H "Content-Type: text/xml" \ -d 'system.listMethods' \ http://localhost:9875 > /dev/null 2>&1 } # Detect timeout command (not available on macOS by default) TIMEOUT_CMD="" if command -v timeout >/dev/null 2>&1; then TIMEOUT_CMD="timeout" elif command -v gtimeout >/dev/null 2>&1; then TIMEOUT_CMD="gtimeout" # macOS coreutils fi # Build the Docker image echo "Step 1: Building Docker image..." docker build -t {{image_name}}:test {{project_root}} echo "✓ Docker image built successfully" echo "" # Check if FreeCAD bridge is already running echo "Step 2: Checking for FreeCAD MCP bridge..." if check_xmlrpc; then echo "✓ FreeCAD MCP bridge is already running on port 9875" STARTED_FREECAD=false else echo "FreeCAD MCP bridge not detected. Starting FreeCAD headless..." echo " (This may take 30-60 seconds for FreeCAD to initialize...)" # Start FreeCAD headless in background, capturing output FREECAD_LOG=$(mktemp) just freecad::run-headless > "$FREECAD_LOG" 2>&1 & FREECAD_PID=$! # Wait for bridge to be ready (longer timeout for FreeCAD startup) MAX_RETRIES=60 RETRY_COUNT=0 while ! check_xmlrpc; do RETRY_COUNT=$((RETRY_COUNT + 1)) if [ $RETRY_COUNT -ge $MAX_RETRIES ]; then echo "✗ ERROR: FreeCAD MCP bridge did not start within ${MAX_RETRIES}s" echo "" echo "FreeCAD log output:" tail -30 "$FREECAD_LOG" exit 1 fi # Show progress less frequently to reduce noise if [ $((RETRY_COUNT % 5)) -eq 0 ]; then echo " Waiting for bridge... ($RETRY_COUNT/$MAX_RETRIES)" fi sleep 1 done rm -f "$FREECAD_LOG" FREECAD_LOG="" # Clear so cleanup doesn't try to delete again echo "✓ FreeCAD MCP bridge started (took ${RETRY_COUNT}s)" STARTED_FREECAD=true fi echo "" # Run the container and test communication echo "Step 3: Running container and testing MCP communication..." echo " Running Robust MCP Server in container..." # Send MCP initialize request via JSON-RPC over stdio # Using temp file for input so stdin closes after message # Note: Container may exit non-zero when stdin closes (ClosedResourceError), which is expected MCP_INPUT=$(mktemp) echo '{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' > "$MCP_INPUT" # Capture output; ignore exit code since stdin close causes expected error # Use timeout command if available for better reliability if [ -n "$TIMEOUT_CMD" ]; then CONTAINER_OUTPUT=$($TIMEOUT_CMD 30 docker run --rm -i \ --add-host=host.docker.internal:host-gateway \ -e FREECAD_MODE=xmlrpc \ -e FREECAD_SOCKET_HOST=host.docker.internal \ {{image_name}}:test 2>&1 < "$MCP_INPUT" || true) else CONTAINER_OUTPUT=$(docker run --rm -i \ --add-host=host.docker.internal:host-gateway \ -e FREECAD_MODE=xmlrpc \ -e FREECAD_SOCKET_HOST=host.docker.internal \ {{image_name}}:test 2>&1 < "$MCP_INPUT" || true) fi rm -f "$MCP_INPUT" MCP_INPUT="" # Clear so cleanup doesn't try to delete again echo "" echo "Step 4: Verifying response..." # Track test results TEST_PASSED=true # Check if we got a valid MCP initialize response if echo "$CONTAINER_OUTPUT" | grep -q '"result".*"protocolVersion"'; then echo "✓ Container responded with valid MCP initialize response" else echo "✗ Container failed to respond with valid MCP response" TEST_PASSED=false fi # Check for server info in response if echo "$CONTAINER_OUTPUT" | grep -q '"serverInfo".*"freecad-mcp"'; then echo "✓ Server identified as freecad-mcp" else echo "⚠ Warning: Could not confirm server identity" fi # Check for FreeCAD bridge connection in logs (use grep -E for clearer alternation) if echo "$CONTAINER_OUTPUT" | grep -Eq 'FreeCAD bridge connected|FreeCAD.*GUI'; then echo "✓ FreeCAD bridge connection logged" else echo "⚠ Warning: No FreeCAD bridge connection in logs" fi echo "" echo "==========================================" echo "Raw container output (last 20 lines):" echo "==========================================" echo "$CONTAINER_OUTPUT" | tail -20 echo "" # Note: FreeCAD cleanup is handled by the EXIT trap echo "" echo "==========================================" if [ "$TEST_PASSED" = true ]; then echo "✓ PASSED: Docker integration test succeeded!" echo " - Container built and ran successfully" echo " - Robust MCP Server responded to initialize request" else echo "✗ FAILED: Docker integration test had errors" echo " - Review the output above for details" fi echo "==========================================" # Exit with appropriate code if [ "$TEST_PASSED" = false ]; then exit 1 fi # ============================================================================ # GUI Test Container (for CI debugging) # ============================================================================ # Build the GUI test container (replicates GitHub Actions CI environment) # Supports both x86_64 and aarch64 architectures (downloads correct AppImage) build-gui-test: docker build -f {{project_root}}/tests/ci-test/Dockerfile.gui-test \ -t {{gui_test_image}} \ {{project_root}} # Run GUI test container interactively for debugging gui-test-shell: docker run --rm -it \ -v {{project_root}}:/workspace \ {{gui_test_image}} \ /bin/bash # Run the automated GUI tests in the container gui-test-run: docker run --rm -i \ -v {{project_root}}:/workspace \ {{gui_test_image}} \ /usr/local/bin/run-gui-test.sh # Run GUI test with custom command gui-test-cmd *args: docker run --rm -it \ -v {{project_root}}:/workspace \ {{gui_test_image}} \ {{args}} # Quick rebuild and test cycle for GUI debugging gui-test: build-gui-test gui-test-run