from __future__ import annotations from typing import Any from pathlib import Path from core.tools.base import BaseTool, ToolContext from core.tools.registry import registry from core.safety import safety from core.events import bus from core.subprocess import run_command class CMakeTool(BaseTool): name = "cmake" description = "CMake build system operations (configure, build, generate, clean)" # ========================================================= # EXECUTE ROUTER # ========================================================= def execute(self, payload: dict[str, Any], ctx: ToolContext): action = str(payload.get("action", "")).strip() bus.log( "CMAKE", "cmake_execute", "INFO", {"action": action} ) match action: case "configure": return self.configure(payload) case "build": return self.build(payload) case "clean": return self.clean(payload) case "generate": return self.generate(payload) case _: raise ValueError(f"Unknown cmake action: {action}") # ========================================================= # HELPERS # ========================================================= def _path(self, value: str) -> Path: return safety.validate_path(value) # ========================================================= # CONFIGURE # ========================================================= def configure(self, payload: dict[str, Any]): source_dir = payload.get("source_dir", ".") build_dir = payload.get("build_dir", "build") generator = payload.get("generator") # optional build_type = payload.get("build_type", "Release") if not isinstance(source_dir, str): raise ValueError("source_dir must be string") if not isinstance(build_dir, str): raise ValueError("build_dir must be string") src = self._path(source_dir) bld = self._path(build_dir) bld.mkdir(parents=True, exist_ok=True) cmd = [ "cmake", "-S", str(src), "-B", str(bld), f"-DCMAKE_BUILD_TYPE={build_type}" ] if generator: if not isinstance(generator, str): raise ValueError("generator must be string") cmd.extend(["-G", generator]) result = run_command(cmd=cmd) return { "action": "configure", "source_dir": str(src), "build_dir": str(bld), "status": "success" if result.get("return_code") == 0 else "error", "stdout": result.get("stdout", ""), "stderr": result.get("stderr", "") } # ========================================================= # BUILD # ========================================================= def build(self, payload: dict[str, Any]): build_dir = payload.get("build_dir", "build") target = payload.get("target") # optional jobs = payload.get("jobs", 0) if not isinstance(build_dir, str): raise ValueError("build_dir must be string") bld = self._path(build_dir) cmd = ["cmake", "--build", str(bld)] if target: if not isinstance(target, str): raise ValueError("target must be string") cmd.extend(["--target", target]) if isinstance(jobs, int) and jobs > 0: cmd.extend(["--parallel", str(jobs)]) result = run_command(cmd=cmd) return { "action": "build", "build_dir": str(bld), "target": target, "status": "success" if result.get("return_code") == 0 else "error", "stdout": result.get("stdout", ""), "stderr": result.get("stderr", "") } # ========================================================= # GENERATE (alias convenience) # ========================================================= def generate(self, payload: dict[str, Any]): # CMake modern workflow usually doesn't need this separately, # but kept for explicit "generate-only" workflows. return self.configure(payload) # ========================================================= # CLEAN # ========================================================= def clean(self, payload: dict[str, Any]): build_dir = payload.get("build_dir", "build") if not isinstance(build_dir, str): raise ValueError("build_dir must be string") bld = self._path(build_dir) if not bld.exists(): return { "action": "clean", "status": "skipped", "message": "build directory does not exist" } # safe clean: only remove cache artifacts, not full directory unless requested cache_file = bld / "CMakeCache.txt" removed = [] if cache_file.exists(): cache_file.unlink() removed.append("CMakeCache.txt") # optional: remove CMakeFiles cmake_files = bld / "CMakeFiles" if cmake_files.exists(): import shutil shutil.rmtree(cmake_files) removed.append("CMakeFiles/") return { "action": "clean", "build_dir": str(bld), "removed": removed, "status": "ok" } # ========================================================= # REGISTER # ========================================================= registry.register(CMakeTool())