178 lines
5.7 KiB
Python
178 lines
5.7 KiB
Python
|
|
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"
|