Add SSM deployment and reliable Maven setup

This commit is contained in:
王鹏
2026-10-08 17:58:09 +08:00
parent 2d7b6216ba
commit d1695eba74
13 changed files with 626 additions and 46 deletions

View File

@@ -1,8 +1,16 @@
from __future__ import annotations
import re
import hashlib
import os
import shutil
import subprocess
import tempfile
import urllib.request
import zipfile
from dataclasses import dataclass
from pathlib import Path
from typing import BinaryIO, Callable
from .scanner import ScanResult
@@ -15,13 +23,19 @@ JDK_PACKAGES = {
}
WINGET_PACKAGES = {
"Maven": "Apache.Maven",
"Gradle": "Gradle.Gradle",
"Node.js": "OpenJS.NodeJS.LTS",
"Git": "Git.Git",
"MySQL": "Oracle.MySQL",
}
MAVEN_VERSION = "3.10.0"
MAVEN_ARCHIVE_NAME = f"apache-maven-{MAVEN_VERSION}-bin.zip"
MAVEN_DOWNLOAD_BASE = f"https://dlcdn.apache.org/maven/maven-3/{MAVEN_VERSION}/binaries"
TOMCAT_VERSION = "9.0.122"
TOMCAT_ARCHIVE_NAME = f"apache-tomcat-{TOMCAT_VERSION}-windows-x64.zip"
TOMCAT_DOWNLOAD_BASE = f"https://dlcdn.apache.org/tomcat/tomcat-9/v{TOMCAT_VERSION}/bin"
ANSI_PATTERN = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
PERCENT_PATTERN = re.compile(r"(\d{1,3})\s*%")
PROGRESS_CHARS = "█▓▒░▏▎▍▌▋▊▉■□▪▫▬─━═|/-\\"
@@ -65,6 +79,153 @@ def build_install_command(tool_name: str, version: str | None = None) -> list[st
return ["winget", "install", "-e", "--id", package_id]
def default_maven_install_root() -> Path:
local_app_data = os.environ.get("LOCALAPPDATA", "").strip()
base = Path(local_app_data) if local_app_data else Path.home() / ".autodeploy"
return base / "AutoDeploy" / "tools"
def install_maven(
on_output: Callable[[str], None],
install_root: Path | None = None,
opener: Callable[..., BinaryIO] | None = None,
) -> Path:
root = Path(install_root) if install_root is not None else default_maven_install_root()
archive_url = f"{MAVEN_DOWNLOAD_BASE}/{MAVEN_ARCHIVE_NAME}"
return _install_verified_apache_archive(
product_name="Maven",
version=MAVEN_VERSION,
archive_name=MAVEN_ARCHIVE_NAME,
archive_url=archive_url,
extracted_dir_name=f"apache-maven-{MAVEN_VERSION}",
executable_relative=Path("bin") / "mvn.cmd",
on_output=on_output,
install_root=root,
opener=opener,
)
def install_tomcat(
on_output: Callable[[str], None],
install_root: Path | None = None,
opener: Callable[..., BinaryIO] | None = None,
) -> Path:
root = Path(install_root) if install_root is not None else default_maven_install_root()
archive_url = f"{TOMCAT_DOWNLOAD_BASE}/{TOMCAT_ARCHIVE_NAME}"
return _install_verified_apache_archive(
product_name="Tomcat",
version=TOMCAT_VERSION,
archive_name=TOMCAT_ARCHIVE_NAME,
archive_url=archive_url,
extracted_dir_name=f"apache-tomcat-{TOMCAT_VERSION}",
executable_relative=Path("bin") / "catalina.bat",
on_output=on_output,
install_root=root,
opener=opener,
)
def _install_verified_apache_archive(
*,
product_name: str,
version: str,
archive_name: str,
archive_url: str,
extracted_dir_name: str,
executable_relative: Path,
on_output: Callable[[str], None],
install_root: Path,
opener: Callable[..., BinaryIO] | None,
) -> Path:
product_home = install_root / extracted_dir_name
executable = product_home / executable_relative
if executable.exists():
on_output(f"{product_name} {version} 已安装: {executable}")
return executable
open_url = opener or urllib.request.urlopen
checksum_url = f"{archive_url}.sha512"
install_root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix=f"autodeploy-{product_name.lower()}-") as temporary:
temp_dir = Path(temporary)
archive_path = temp_dir / archive_name
on_output(f"正在从 Apache 官方地址下载 {product_name} {version}...")
_download_to_file(archive_url, archive_path, open_url, on_output)
expected_checksum = _download_checksum(checksum_url, open_url)
actual_checksum = _sha512(archive_path)
if actual_checksum.lower() != expected_checksum.lower():
raise RuntimeError(f"{product_name} 安装包 SHA-512 校验失败,已停止安装。")
on_output(f"{product_name} 安装包校验通过,正在解压...")
extract_root = temp_dir / "extracted"
_safe_extract_zip(archive_path, extract_root)
extracted_home = extract_root / extracted_dir_name
extracted_executable = extracted_home / executable_relative
if not extracted_executable.exists():
raise RuntimeError(f"{product_name} 安装包结构异常,未找到 {executable_relative}。")
shutil.copytree(extracted_home, product_home, dirs_exist_ok=True)
if not executable.exists():
raise RuntimeError(f"{product_name} 解压完成,但未找到 {executable_relative}。")
on_output(f"{product_name} {version} 安装完成: {executable}")
return executable
def _download_to_file(
url: str,
destination: Path,
opener: Callable[..., BinaryIO],
on_output: Callable[[str], None],
) -> None:
with opener(url, timeout=60) as response, destination.open("wb") as output:
headers = getattr(response, "headers", {})
total_text = headers.get("Content-Length", "") if hasattr(headers, "get") else ""
total = int(total_text) if str(total_text).isdigit() else 0
downloaded = 0
last_percent = -10
while True:
chunk = response.read(1024 * 256)
if not chunk:
break
output.write(chunk)
downloaded += len(chunk)
if total:
percent = min(downloaded * 100 // total, 100)
if percent >= last_percent + 10 or percent == 100:
on_output(f"下载进度: {percent}%")
last_percent = percent
def _download_checksum(url: str, opener: Callable[..., BinaryIO]) -> str:
with opener(url, timeout=30) as response:
text = response.read().decode("ascii", errors="strict").strip()
checksum = text.split()[0] if text else ""
if not re.fullmatch(r"[0-9a-fA-F]{128}", checksum):
raise RuntimeError("Apache Maven 校验文件格式无效。")
return checksum
def _sha512(path: Path) -> str:
digest = hashlib.sha512()
with path.open("rb") as source:
for chunk in iter(lambda: source.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def _safe_extract_zip(archive_path: Path, destination: Path) -> None:
destination.mkdir(parents=True, exist_ok=True)
destination_resolved = destination.resolve()
with zipfile.ZipFile(archive_path) as archive:
for member in archive.infolist():
target = (destination / member.filename).resolve()
if target != destination_resolved and destination_resolved not in target.parents:
raise RuntimeError("Maven 安装包包含不安全的文件路径。")
archive.extractall(destination)
def run_install_command(command: list[str], on_output) -> int:
output_lines: list[str] = []
process = subprocess.Popen(