mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
446 lines
15 KiB
Python
446 lines
15 KiB
Python
"""
|
|
Build Tool - Compile/build artifacts narrowly by default
|
|
=======================================================
|
|
|
|
Purpose: compile/build artifacts with smart scope resolution.
|
|
|
|
Same scope logic as test tool:
|
|
- file → derive owning package/project and build it
|
|
- dir → build that subtree
|
|
- pkg → use explicitly
|
|
- ws → workspace-wide
|
|
|
|
Backends:
|
|
- go: go build
|
|
- ts: tsc or workspace build (pnpm build)
|
|
- py: optional (python -m build) for packaging
|
|
- rs: cargo build
|
|
- cc: cmake --build, ninja, make
|
|
- sol: forge build / hardhat compile
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Any, Dict, Optional
|
|
|
|
from .dev_tools import DevResult, DevToolBase, create_dev_result
|
|
|
|
|
|
class BuildTool(DevToolBase):
|
|
"""Build/compilation tool"""
|
|
|
|
def __init__(self, target: str, **kwargs):
|
|
super().__init__(target, **kwargs)
|
|
self.opts = kwargs.get("opts", {})
|
|
|
|
async def execute(self) -> DevResult:
|
|
"""Execute build operation"""
|
|
try:
|
|
if self.language == "go":
|
|
return await self._build_go()
|
|
elif self.language == "ts":
|
|
return await self._build_typescript()
|
|
elif self.language == "py":
|
|
return await self._build_python()
|
|
elif self.language == "rs":
|
|
return await self._build_rust()
|
|
elif self.language == "cc":
|
|
return await self._build_cpp()
|
|
elif self.language == "sol":
|
|
return await self._build_solidity()
|
|
else:
|
|
return create_dev_result(
|
|
ok=False,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used=self.backend,
|
|
scope_resolved=self.target,
|
|
errors=[f"Build not supported for language: {self.language}"],
|
|
)
|
|
except Exception as e:
|
|
return create_dev_result(
|
|
ok=False,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used=self.backend,
|
|
scope_resolved=self.target,
|
|
errors=[str(e)],
|
|
)
|
|
|
|
async def _build_go(self) -> DevResult:
|
|
"""Build Go packages"""
|
|
cmd = ["go", "build"]
|
|
|
|
# Determine build scope
|
|
if self.resolved["type"] == "file":
|
|
pkg = self.resolved["package"]
|
|
if pkg and pkg != ".":
|
|
cmd.append(f"./{pkg}")
|
|
else:
|
|
cmd.append(".")
|
|
elif self.resolved["type"] == "directory":
|
|
dir_path = self.resolved["scope"]
|
|
cmd.append(f"./{dir_path}/...")
|
|
elif self.resolved["type"] == "package":
|
|
pkg_spec = self.resolved["scope"]
|
|
cmd.append(pkg_spec)
|
|
elif self.resolved["type"] == "workspace":
|
|
cmd.append("./...")
|
|
else:
|
|
cmd.append(".")
|
|
|
|
# Add build options
|
|
if self.opts.get("race"):
|
|
cmd.append("-race")
|
|
if self.opts.get("tags"):
|
|
cmd.extend(["-tags", self.opts["tags"]])
|
|
if self.opts.get("ldflags"):
|
|
cmd.extend(["-ldflags", self.opts["ldflags"]])
|
|
|
|
result = self._run_command(cmd)
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="go",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
async def _build_typescript(self) -> DevResult:
|
|
"""Build TypeScript/JavaScript"""
|
|
# Try different build approaches
|
|
if self.backend == "tsc" or self._has_tsconfig():
|
|
return await self._build_with_tsc()
|
|
elif self.backend in ["pnpm", "npm", "yarn"]:
|
|
return await self._build_with_package_script()
|
|
else:
|
|
return create_dev_result(
|
|
ok=False,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used=self.backend,
|
|
scope_resolved=self.target,
|
|
errors=[
|
|
"No build configuration found (tsconfig.json or package.json build script)"
|
|
],
|
|
)
|
|
|
|
async def _build_python(self) -> DevResult:
|
|
"""Build Python packages"""
|
|
# Python builds are typically for packaging
|
|
if Path(self.workspace["root"], "pyproject.toml").exists():
|
|
cmd = ["python", "-m", "build"]
|
|
elif Path(self.workspace["root"], "setup.py").exists():
|
|
cmd = ["python", "setup.py", "build"]
|
|
else:
|
|
return create_dev_result(
|
|
ok=True,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="python",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout="Python doesn't require explicit build - interpret/lint is the main verification",
|
|
)
|
|
|
|
result = self._run_command(cmd)
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="python",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
async def _build_rust(self) -> DevResult:
|
|
"""Build Rust packages"""
|
|
cmd = ["cargo", "build"]
|
|
|
|
# Add package filter if building specific package
|
|
if self.resolved["type"] == "file":
|
|
cargo_toml = self._find_rust_package(self.resolved["scope"])
|
|
if cargo_toml:
|
|
package_name = self._get_rust_package_name(cargo_toml)
|
|
if package_name:
|
|
cmd.extend(["-p", package_name])
|
|
|
|
# Add build options
|
|
if self.opts.get("release"):
|
|
cmd.append("--release")
|
|
if self.opts.get("features"):
|
|
cmd.extend(["--features", self.opts["features"]])
|
|
|
|
result = self._run_command(cmd)
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="cargo",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
async def _build_cpp(self) -> DevResult:
|
|
"""Build C/C++ projects"""
|
|
build_dir = Path(self.workspace["root"]) / "build"
|
|
|
|
if self.backend == "cmake":
|
|
return await self._build_with_cmake()
|
|
elif self.backend == "ninja":
|
|
return await self._build_with_ninja()
|
|
elif self.backend == "make":
|
|
return await self._build_with_make()
|
|
else:
|
|
# Auto-detect
|
|
if (Path(self.workspace["root"]) / "CMakeLists.txt").exists():
|
|
return await self._build_with_cmake()
|
|
elif (build_dir / "build.ninja").exists():
|
|
return await self._build_with_ninja()
|
|
elif (Path(self.workspace["root"]) / "Makefile").exists():
|
|
return await self._build_with_make()
|
|
else:
|
|
return create_dev_result(
|
|
ok=False,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used=self.backend,
|
|
scope_resolved=self.target,
|
|
errors=[
|
|
"No build system detected (CMakeLists.txt, build.ninja, or Makefile)"
|
|
],
|
|
)
|
|
|
|
async def _build_solidity(self) -> DevResult:
|
|
"""Build Solidity contracts"""
|
|
if self.backend == "forge" or self._has_forge():
|
|
cmd = ["forge", "build"]
|
|
backend = "forge"
|
|
elif self.backend == "hardhat" or self._has_hardhat():
|
|
cmd = ["npx", "hardhat", "compile"]
|
|
backend = "hardhat"
|
|
else:
|
|
return create_dev_result(
|
|
ok=False,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used=self.backend,
|
|
scope_resolved=self.target,
|
|
errors=["No Solidity build system detected (forge or hardhat)"],
|
|
)
|
|
|
|
result = self._run_command(cmd)
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used=backend,
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
async def _build_with_tsc(self) -> DevResult:
|
|
"""Build with TypeScript compiler"""
|
|
cmd = ["tsc"]
|
|
|
|
# Add project file if exists
|
|
if self.resolved["type"] != "workspace":
|
|
# Look for nearest tsconfig.json
|
|
tsconfig = self._find_tsconfig(self.resolved["scope"])
|
|
if tsconfig:
|
|
cmd.extend(["-p", str(tsconfig.parent)])
|
|
|
|
result = self._run_command(cmd)
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="tsc",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
async def _build_with_package_script(self) -> DevResult:
|
|
"""Build via package.json script"""
|
|
if self.backend == "pnpm":
|
|
cmd = ["pnpm", "build"]
|
|
elif self.backend == "yarn":
|
|
cmd = ["yarn", "build"]
|
|
else:
|
|
cmd = ["npm", "run", "build"]
|
|
|
|
result = self._run_command(cmd)
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used=self.backend,
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
async def _build_with_cmake(self) -> DevResult:
|
|
"""Build with CMake"""
|
|
build_dir = Path(self.workspace["root"]) / "build"
|
|
|
|
# Configure if needed
|
|
if not build_dir.exists():
|
|
config_result = self._run_command(["cmake", "-B", "build", "-S", "."])
|
|
if config_result.returncode != 0:
|
|
return create_dev_result(
|
|
ok=False,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="cmake",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=config_result.stdout,
|
|
stderr=config_result.stderr,
|
|
exit_code=config_result.returncode,
|
|
errors=["CMake configure failed"],
|
|
)
|
|
|
|
# Build
|
|
result = self._run_command(["cmake", "--build", "build"])
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="cmake",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
async def _build_with_ninja(self) -> DevResult:
|
|
"""Build with Ninja"""
|
|
result = self._run_command(
|
|
["ninja"], cwd=str(Path(self.workspace["root"]) / "build")
|
|
)
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="ninja",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
async def _build_with_make(self) -> DevResult:
|
|
"""Build with Make"""
|
|
result = self._run_command(["make"])
|
|
|
|
return create_dev_result(
|
|
ok=result.returncode == 0,
|
|
root=self.workspace["root"],
|
|
language_used=self.language,
|
|
backend_used="make",
|
|
scope_resolved=self.resolved["scope"],
|
|
stdout=result.stdout,
|
|
stderr=result.stderr,
|
|
exit_code=result.returncode,
|
|
)
|
|
|
|
def _has_tsconfig(self) -> bool:
|
|
"""Check if tsconfig.json exists"""
|
|
return Path(self.workspace["root"], "tsconfig.json").exists()
|
|
|
|
def _find_tsconfig(self, file_path: str) -> Optional[Path]:
|
|
"""Find nearest tsconfig.json"""
|
|
path = Path(file_path)
|
|
current = path.parent if path.is_file() else path
|
|
|
|
while current >= Path(self.workspace["root"]):
|
|
tsconfig = current / "tsconfig.json"
|
|
if tsconfig.exists():
|
|
return tsconfig
|
|
current = current.parent
|
|
|
|
return None
|
|
|
|
def _has_forge(self) -> bool:
|
|
"""Check if Forge is available"""
|
|
try:
|
|
result = self._run_command(["forge", "--version"])
|
|
return result.returncode == 0
|
|
except (OSError, FileNotFoundError):
|
|
return False
|
|
|
|
def _has_hardhat(self) -> bool:
|
|
"""Check if Hardhat is available"""
|
|
return (
|
|
Path(self.workspace["root"], "hardhat.config.js").exists()
|
|
or Path(self.workspace["root"], "hardhat.config.ts").exists()
|
|
)
|
|
|
|
def _find_rust_package(self, file_path: str) -> Optional[str]:
|
|
"""Find Cargo.toml for given file"""
|
|
path = Path(file_path)
|
|
current = path.parent if path.is_file() else path
|
|
|
|
while current >= Path(self.workspace["root"]):
|
|
cargo_toml = current / "Cargo.toml"
|
|
if cargo_toml.exists():
|
|
return str(cargo_toml)
|
|
current = current.parent
|
|
|
|
return None
|
|
|
|
def _get_rust_package_name(self, cargo_toml_path: str) -> Optional[str]:
|
|
"""Extract package name from Cargo.toml"""
|
|
try:
|
|
import toml
|
|
|
|
with open(cargo_toml_path) as f:
|
|
data = toml.load(f)
|
|
return data.get("package", {}).get("name")
|
|
except (OSError, ImportError, ValueError, KeyError):
|
|
return None
|
|
|
|
|
|
# MCP tool integration
|
|
async def build_tool_handler(
|
|
target: str,
|
|
language: str = "auto",
|
|
backend: str = "auto",
|
|
root: Optional[str] = None,
|
|
env: Optional[Dict[str, str]] = None,
|
|
dry_run: bool = False,
|
|
opts: Optional[Dict[str, Any]] = None,
|
|
) -> Dict[str, Any]:
|
|
"""MCP handler for build tool"""
|
|
|
|
tool = BuildTool(
|
|
target=target,
|
|
language=language,
|
|
backend=backend,
|
|
root=root,
|
|
env=env,
|
|
dry_run=dry_run,
|
|
opts=opts or {},
|
|
)
|
|
|
|
result = await tool.execute()
|
|
return result.dict()
|