Initial commit: add AutoDeploy project
This commit is contained in:
189
deploy_tool/environment.py
Normal file
189
deploy_tool/environment.py
Normal file
@@ -0,0 +1,189 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Callable, Iterable
|
||||
|
||||
|
||||
CommandRunner = Callable[[list[str]], subprocess.CompletedProcess[str]]
|
||||
|
||||
|
||||
DEFAULT_CHECKS: dict[str, list[str]] = {
|
||||
"JDK": ["java", "-version"],
|
||||
"Maven": ["mvn", "-version"],
|
||||
"Gradle": ["gradle", "-version"],
|
||||
"Node.js": ["node", "--version"],
|
||||
"npm": ["npm", "--version"],
|
||||
"Vue CLI": ["vue", "--version"],
|
||||
"MySQL": ["mysql", "--version"],
|
||||
"Git": ["git", "--version"],
|
||||
}
|
||||
|
||||
EXECUTABLE_NAMES: dict[str, list[str]] = {
|
||||
"JDK": ["java.exe", "java"],
|
||||
"Maven": ["mvn.cmd", "mvn.bat", "mvn"],
|
||||
"Gradle": ["gradle.bat", "gradle"],
|
||||
"Node.js": ["node.exe", "node"],
|
||||
"npm": ["npm.cmd", "npm"],
|
||||
"Vue CLI": ["vue.cmd", "vue"],
|
||||
"MySQL": ["mysql.exe", "mysql"],
|
||||
"Git": ["git.exe", "git"],
|
||||
}
|
||||
|
||||
CONFIG_DIR = Path(".deploy_tool_config")
|
||||
TOOL_PATHS_FILE = CONFIG_DIR / "tool-paths.json"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EnvironmentCheck:
|
||||
name: str
|
||||
command: list[str]
|
||||
installed: bool
|
||||
version: str
|
||||
message: str
|
||||
source: str = "PATH"
|
||||
|
||||
|
||||
def check_environment(
|
||||
runner: CommandRunner | None = None,
|
||||
checks: dict[str, list[str]] | None = None,
|
||||
manual_paths: dict[str, str | Path] | None = None,
|
||||
search_roots: Iterable[Path] | None = None,
|
||||
) -> list[EnvironmentCheck]:
|
||||
command_runner = runner or _run_command
|
||||
selected_checks = checks or DEFAULT_CHECKS
|
||||
configured_paths = {name: Path(value) for name, value in (manual_paths or {}).items() if str(value).strip()}
|
||||
roots = list(search_roots) if search_roots is not None else default_search_roots()
|
||||
results: list[EnvironmentCheck] = []
|
||||
|
||||
for name, command in selected_checks.items():
|
||||
path_result = _try_command(name, command, "PATH", command_runner)
|
||||
if path_result.installed:
|
||||
results.append(path_result)
|
||||
continue
|
||||
|
||||
manual_path = configured_paths.get(name)
|
||||
if manual_path and manual_path.exists():
|
||||
manual_command = [str(manual_path), *command[1:]]
|
||||
manual_result = _try_command(name, manual_command, "手动指定", command_runner)
|
||||
if manual_result.installed:
|
||||
results.append(manual_result)
|
||||
continue
|
||||
|
||||
discovered = discover_executable(name, roots)
|
||||
if discovered:
|
||||
discovered_command = [str(discovered), *command[1:]]
|
||||
discovered_result = _try_command(name, discovered_command, "自动发现", command_runner)
|
||||
if discovered_result.installed:
|
||||
results.append(discovered_result)
|
||||
continue
|
||||
|
||||
results.append(path_result)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def discover_executable(name: str, search_roots: Iterable[Path] | None = None) -> Path | None:
|
||||
executable_names = {item.lower() for item in EXECUTABLE_NAMES.get(name, [])}
|
||||
if not executable_names:
|
||||
return None
|
||||
|
||||
for root in search_roots or default_search_roots():
|
||||
if not root.exists() or not root.is_dir():
|
||||
continue
|
||||
for candidate in _direct_candidates(name, root):
|
||||
if candidate.exists() and candidate.name.lower() in executable_names:
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def default_search_roots() -> list[Path]:
|
||||
roots = [
|
||||
Path(os.environ.get("ProgramFiles", r"C:\Program Files")),
|
||||
Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")),
|
||||
Path(os.environ.get("LOCALAPPDATA", "")),
|
||||
Path(r"C:\ProgramData"),
|
||||
Path(r"C:\xampp"),
|
||||
Path(r"C:\phpstudy_pro"),
|
||||
Path(r"D:\Program Files"),
|
||||
Path(r"D:\phpstudy_pro"),
|
||||
]
|
||||
return [root for root in roots if str(root)]
|
||||
|
||||
|
||||
def load_tool_paths(config_file: Path = TOOL_PATHS_FILE) -> dict[str, Path]:
|
||||
if not config_file.exists():
|
||||
return {}
|
||||
try:
|
||||
data = json.loads(config_file.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
return {}
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
return {str(name): Path(value) for name, value in data.items() if isinstance(value, str) and value.strip()}
|
||||
|
||||
|
||||
def save_tool_paths(paths: dict[str, str | Path], config_file: Path = TOOL_PATHS_FILE) -> None:
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
data = {name: str(path) for name, path in paths.items() if str(path).strip()}
|
||||
config_file.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def _try_command(name: str, command: list[str], source: str, runner: CommandRunner) -> EnvironmentCheck:
|
||||
try:
|
||||
completed = runner(command)
|
||||
except FileNotFoundError:
|
||||
return EnvironmentCheck(name, command, False, "", f"{name} 未安装或不在 PATH 中", source)
|
||||
except subprocess.TimeoutExpired:
|
||||
return EnvironmentCheck(name, command, False, "", f"{name} 检测超时", source)
|
||||
except OSError as exc:
|
||||
return EnvironmentCheck(name, command, False, "", f"{name} 检测失败: {exc}", source)
|
||||
|
||||
output = _combine_output(completed)
|
||||
installed = completed.returncode == 0
|
||||
message = "已安装" if installed else f"命令返回错误码 {completed.returncode}"
|
||||
if installed and source != "PATH":
|
||||
message = f"已安装,来源: {source}"
|
||||
return EnvironmentCheck(name, command, installed, output, message, source)
|
||||
|
||||
|
||||
def _run_command(command: list[str]) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(command, capture_output=True, text=True, timeout=6, shell=False)
|
||||
|
||||
|
||||
def _combine_output(completed: subprocess.CompletedProcess[str]) -> str:
|
||||
output = "\n".join(part.strip() for part in (completed.stdout, completed.stderr) if part and part.strip())
|
||||
return output.splitlines()[0] if output else ""
|
||||
|
||||
|
||||
def _direct_candidates(name: str, root: Path) -> list[Path]:
|
||||
if name == "MySQL":
|
||||
return [
|
||||
root / "MySQL" / "MySQL Server 8.4" / "bin" / "mysql.exe",
|
||||
root / "MySQL" / "MySQL Server 8.0" / "bin" / "mysql.exe",
|
||||
root / "mysql" / "bin" / "mysql.exe",
|
||||
root / "Extensions" / "MySQL" / "bin" / "mysql.exe",
|
||||
]
|
||||
if name == "JDK":
|
||||
return _glob(root, "Java/*/bin/java.exe") + _glob(root, "Eclipse Adoptium/*/bin/java.exe")
|
||||
if name == "Node.js":
|
||||
return [root / "nodejs" / "node.exe", root / "Programs" / "nodejs" / "node.exe"]
|
||||
if name == "npm":
|
||||
return [root / "nodejs" / "npm.cmd", root / "Programs" / "nodejs" / "npm.cmd"]
|
||||
if name == "Maven":
|
||||
return _glob(root, "apache-maven*/bin/mvn.cmd")
|
||||
if name == "Gradle":
|
||||
return _glob(root, "gradle*/bin/gradle.bat")
|
||||
if name == "Git":
|
||||
return [root / "Git" / "bin" / "git.exe", root / "Git" / "cmd" / "git.exe"]
|
||||
return []
|
||||
|
||||
|
||||
def _glob(root: Path, pattern: str) -> list[Path]:
|
||||
try:
|
||||
return list(root.glob(pattern))
|
||||
except OSError:
|
||||
return []
|
||||
Reference in New Issue
Block a user