Initial commit: add AutoDeploy project

This commit is contained in:
wangpeng
2026-07-14 16:30:36 +08:00
commit 2d7b6216ba
23 changed files with 3615 additions and 0 deletions

177
deploy_tool/installer.py Normal file
View File

@@ -0,0 +1,177 @@
from __future__ import annotations
import re
import subprocess
from dataclasses import dataclass
from .scanner import ScanResult
JDK_PACKAGES = {
"8": "EclipseAdoptium.Temurin.8.JDK",
"11": "EclipseAdoptium.Temurin.11.JDK",
"17": "EclipseAdoptium.Temurin.17.JDK",
"21": "EclipseAdoptium.Temurin.21.JDK",
}
WINGET_PACKAGES = {
"Maven": "Apache.Maven",
"Gradle": "Gradle.Gradle",
"Node.js": "OpenJS.NodeJS.LTS",
"Git": "Git.Git",
"MySQL": "Oracle.MySQL",
}
ANSI_PATTERN = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
PERCENT_PATTERN = re.compile(r"(\d{1,3})\s*%")
PROGRESS_CHARS = "█▓▒░▏▎▍▌▋▊▉■□▪▫▬─━═|/-\\"
@dataclass(frozen=True)
class JdkRecommendation:
version: str
reason: str
def recommend_jdk_version(scan: ScanResult | None) -> JdkRecommendation:
if not scan or not scan.backend:
return JdkRecommendation("8", "\u672a\u626b\u63cf\u5230\u540e\u7aef\u7248\u672c\u4fe1\u606f\uff0c\u9ed8\u8ba4\u63a8\u8350 JDK 8\u3002")
metadata = scan.backend.metadata
java_version = metadata.get("java_version")
if java_version:
mapped = _map_java_version(java_version)
source = metadata.get("java_version_source", "\u9879\u76ee\u914d\u7f6e")
return JdkRecommendation(mapped, f"\u6839\u636e {source}={java_version} \u63a8\u8350 JDK {mapped}\u3002")
boot_version = metadata.get("spring_boot_version", "")
if boot_version.startswith("3."):
return JdkRecommendation("17", f"Spring Boot 3.x \u9700\u8981 Java 17+\uff0c\u68c0\u6d4b\u5230 Spring Boot {boot_version}\uff0c\u63a8\u8350 JDK 17\u3002")
if boot_version.startswith("2."):
return JdkRecommendation("8", f"\u68c0\u6d4b\u5230 Spring Boot {boot_version}\uff0c\u9ed8\u8ba4\u63a8\u8350\u517c\u5bb9\u6027\u66f4\u597d\u7684 JDK 8\u3002")
return JdkRecommendation("8", "\u672a\u53d1\u73b0\u660e\u786e Java \u7248\u672c\uff0c\u9ed8\u8ba4\u63a8\u8350 JDK 8\u3002")
def build_install_command(tool_name: str, version: str | None = None) -> list[str]:
if tool_name == "JDK":
package_id = JDK_PACKAGES.get(version or "8")
if not package_id:
raise ValueError(f"\u4e0d\u652f\u6301\u7684 JDK \u7248\u672c: {version}")
else:
package_id = WINGET_PACKAGES.get(tool_name)
if not package_id:
raise ValueError(f"\u4e0d\u652f\u6301\u81ea\u52a8\u5b89\u88c5: {tool_name}")
return ["winget", "install", "-e", "--id", package_id]
def run_install_command(command: list[str], on_output) -> int:
output_lines: list[str] = []
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
shell=False,
bufsize=0,
)
assert process.stdout is not None
last_line = ""
for raw_line in process.stdout:
for line in format_install_output(raw_line):
if line and line != last_line:
on_output(line)
output_lines.append(line)
last_line = line
exit_code = process.wait()
failure_hint = explain_install_failure(_tool_name_from_command(command), exit_code, output_lines)
if failure_hint:
on_output(failure_hint)
return exit_code
def explain_install_failure(tool_name: str | None, exit_code: int, output_lines: list[str]) -> str:
if exit_code == 0:
return ""
output_text = "\n".join(output_lines)
if tool_name == "Maven" and ("找不到与输入条件匹配的程序包" in output_text or "No package found" in output_text):
return (
"winget 当前源找不到 Maven 包。请手动下载 Apache Maven解压后在工具里选择 "
"apache-maven 的 bin\\mvn.cmd或把该 bin 目录加入 PATH。"
)
return ""
def format_install_output(raw: bytes) -> list[str]:
text = _decode_process_bytes(raw)
text = ANSI_PATTERN.sub("", text).replace("\b", "")
lines: list[str] = []
for part in re.split(r"[\r\n]+", text):
cleaned = _clean_install_line(part)
if cleaned:
lines.append(cleaned)
return lines
def _decode_process_bytes(raw: bytes) -> str:
for encoding in ("utf-8", "utf-16", "gb18030", "mbcs"):
try:
return raw.decode(encoding)
except (LookupError, UnicodeDecodeError):
continue
return raw.decode("utf-8", errors="replace")
def _clean_install_line(line: str) -> str:
text = line.strip()
if not text:
return ""
percent = PERCENT_PATTERN.search(text)
progress_only = _looks_like_progress_line(text)
if percent and progress_only:
value = min(int(percent.group(1)), 100)
return f"\u5b89\u88c5\u8fdb\u5ea6: {value}%"
if progress_only:
return ""
return text
def _looks_like_progress_line(text: str) -> bool:
if not text:
return False
progress_count = sum(1 for char in text if char in PROGRESS_CHARS)
percent = bool(PERCENT_PATTERN.search(text))
if percent and progress_count:
return True
visible = [char for char in text if not char.isspace()]
if not visible:
return False
return progress_count / len(visible) > 0.45
def _tool_name_from_command(command: list[str]) -> str | None:
package_ids = {package_id: name for name, package_id in WINGET_PACKAGES.items()}
package_ids.update({package_id: "JDK" for package_id in JDK_PACKAGES.values()})
for part in command:
name = package_ids.get(part)
if name:
return name
return None
def _map_java_version(version: str) -> str:
try:
major = int(version.split(".", 1)[0])
except ValueError:
return "8"
if major <= 8:
return "8"
if major <= 11:
return "11"
if major <= 17:
return "17"
return "21"