120 lines
4.2 KiB
Python
120 lines
4.2 KiB
Python
"""环境检测与修复逻辑。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import platform
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from aide.core import output
|
||
from aide.core.config import ConfigManager
|
||
|
||
|
||
class EnvManager:
|
||
def __init__(self, root: Path):
|
||
self.root = root
|
||
|
||
def ensure(self, runtime_only: bool, cfg: ConfigManager) -> bool:
|
||
"""运行环境检测入口。"""
|
||
required_py = self._get_required_python(cfg, runtime_only)
|
||
if not self._check_python_version(required_py):
|
||
return False
|
||
uv_version = self._check_uv()
|
||
if uv_version is None:
|
||
return False
|
||
|
||
if runtime_only:
|
||
output.ok(f"运行时环境就绪 (python:{platform.python_version()}, uv:{uv_version})")
|
||
return True
|
||
|
||
config = cfg.ensure_config()
|
||
cfg.ensure_gitignore()
|
||
|
||
env_config = config.get("env", {})
|
||
venv_path = self.root / env_config.get("venv", ".venv")
|
||
req_path = self.root / env_config.get("requirements", "requirements.txt")
|
||
|
||
self._ensure_requirements_file(req_path)
|
||
if not self._ensure_venv(venv_path):
|
||
return False
|
||
if not self._install_requirements(venv_path, req_path):
|
||
return False
|
||
|
||
task_config = config.get("task", {})
|
||
output.info(f"任务原文档: {task_config.get('source', 'task-now.md')}")
|
||
output.info(f"任务细则文档: {task_config.get('spec', 'task-spec.md')}")
|
||
output.ok(f"环境就绪 (python:{platform.python_version()}, uv:{uv_version}, venv:{venv_path})")
|
||
return True
|
||
|
||
@staticmethod
|
||
def _get_required_python(cfg: ConfigManager, runtime_only: bool) -> str:
|
||
if runtime_only:
|
||
return "3.11"
|
||
data = cfg.load_config()
|
||
runtime = data.get("runtime", {})
|
||
return str(runtime.get("python_min", "3.11"))
|
||
|
||
@staticmethod
|
||
def _parse_version(version: str) -> tuple[int, ...]:
|
||
parts = []
|
||
for part in version.split("."):
|
||
try:
|
||
parts.append(int(part))
|
||
except ValueError:
|
||
break
|
||
return tuple(parts)
|
||
|
||
def _check_python_version(self, required: str) -> bool:
|
||
current = self._parse_version(platform.python_version())
|
||
target = self._parse_version(required)
|
||
if current >= target:
|
||
return True
|
||
output.err(f"Python 版本不足,要求>={required},当前 {platform.python_version()}")
|
||
return False
|
||
|
||
def _check_uv(self) -> str | None:
|
||
try:
|
||
result = subprocess.run(
|
||
["uv", "--version"],
|
||
check=True,
|
||
capture_output=True,
|
||
text=True,
|
||
)
|
||
return result.stdout.strip()
|
||
except (subprocess.CalledProcessError, FileNotFoundError) as exc:
|
||
output.err(f"未检测到 uv,请先安装({exc})")
|
||
return None
|
||
|
||
def _ensure_venv(self, venv_path: Path) -> bool:
|
||
if venv_path.exists():
|
||
return True
|
||
output.info(f"创建虚拟环境: {venv_path}")
|
||
try:
|
||
subprocess.run(["uv", "venv", str(venv_path)], check=True)
|
||
output.ok("已创建虚拟环境")
|
||
return True
|
||
except subprocess.CalledProcessError as exc:
|
||
output.err(f"创建虚拟环境失败: {exc}")
|
||
return False
|
||
|
||
@staticmethod
|
||
def _ensure_requirements_file(req_path: Path) -> None:
|
||
if req_path.exists():
|
||
return
|
||
req_path.write_text("# 在此添加依赖\n", encoding="utf-8")
|
||
output.warn(f"未找到 {req_path.name},已创建空文件")
|
||
|
||
def _install_requirements(self, venv_path: Path, req_path: Path) -> bool:
|
||
if not req_path.exists():
|
||
output.err(f"缺少 {req_path}")
|
||
return False
|
||
cmd = ["uv", "pip", "install", "-r", str(req_path), "--python", str(venv_path)]
|
||
output.info("安装依赖(uv pip install -r requirements.txt)")
|
||
try:
|
||
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.STDOUT)
|
||
return True
|
||
except subprocess.CalledProcessError as exc:
|
||
output.err(f"安装依赖失败: {exc}")
|
||
return False
|