Add SSM deployment and reliable Maven setup
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user