generated from coulomb/repo-seed
268 lines
9.1 KiB
Python
268 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Install a local Java/Maven toolchain under .local without sudo."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tarfile
|
|
import urllib.request
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
DEFAULT_JDK_URL = (
|
|
"https://api.adoptium.net/v3/binary/latest/17/ga/linux/x64/"
|
|
"jdk/hotspot/normal/eclipse?project=jdk"
|
|
)
|
|
DEFAULT_MAVEN_VERSION = "3.9.11"
|
|
DEFAULT_MAVEN_URL = (
|
|
"https://archive.apache.org/dist/maven/maven-3/{version}/binaries/"
|
|
"apache-maven-{version}-bin.tar.gz"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--install-dir", type=Path, default=Path(".local") / "toolchains")
|
|
parser.add_argument("--jdk-url", default=DEFAULT_JDK_URL)
|
|
parser.add_argument("--jdk-sha256", default="")
|
|
parser.add_argument("--maven-version", default=DEFAULT_MAVEN_VERSION)
|
|
parser.add_argument("--maven-url", default="")
|
|
parser.add_argument("--skip-jdk", action="store_true")
|
|
parser.add_argument("--skip-maven", action="store_true")
|
|
parser.add_argument("--force", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
extension_root = Path(__file__).resolve().parents[1]
|
|
install_dir = (extension_root / args.install_dir).resolve()
|
|
summary = install_toolchain(
|
|
install_dir=install_dir,
|
|
jdk_url=args.jdk_url,
|
|
jdk_sha256=args.jdk_sha256 or None,
|
|
maven_version=args.maven_version,
|
|
maven_url=args.maven_url or DEFAULT_MAVEN_URL.format(version=args.maven_version),
|
|
install_jdk=not args.skip_jdk,
|
|
install_maven=not args.skip_maven,
|
|
force=args.force,
|
|
)
|
|
print(json.dumps(summary, indent=2, sort_keys=True))
|
|
return 0 if summary["status"] == "ready" else 2
|
|
|
|
|
|
def install_toolchain(
|
|
install_dir: Path,
|
|
jdk_url: str,
|
|
jdk_sha256: str | None,
|
|
maven_version: str,
|
|
maven_url: str,
|
|
install_jdk: bool = True,
|
|
install_maven: bool = True,
|
|
force: bool = False,
|
|
) -> dict[str, Any]:
|
|
downloads_dir = install_dir / "downloads"
|
|
jdks_dir = install_dir / "jdks"
|
|
mavens_dir = install_dir / "mavens"
|
|
downloads_dir.mkdir(parents=True, exist_ok=True)
|
|
jdks_dir.mkdir(parents=True, exist_ok=True)
|
|
mavens_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
jdk_home = _existing_link(install_dir / "current-jdk")
|
|
maven_home = _existing_link(install_dir / "current-maven")
|
|
downloads: list[dict[str, Any]] = []
|
|
|
|
if install_jdk and (force or jdk_home is None):
|
|
archive = downloads_dir / "temurin-jdk-17-linux-x64.tar.gz"
|
|
downloads.append(_download(jdk_url, archive, expected_sha256=jdk_sha256))
|
|
if force:
|
|
_remove_link_or_dir(install_dir / "current-jdk")
|
|
jdk_home = _extract_tool(archive, jdks_dir, "bin/java")
|
|
_replace_symlink_or_copy(jdk_home, install_dir / "current-jdk")
|
|
|
|
if install_maven and (force or maven_home is None):
|
|
archive = downloads_dir / f"apache-maven-{maven_version}-bin.tar.gz"
|
|
downloads.append(_download(maven_url, archive))
|
|
_verify_maven_sha512(maven_url, archive)
|
|
if force:
|
|
_remove_link_or_dir(install_dir / "current-maven")
|
|
maven_home = _extract_tool(archive, mavens_dir, "bin/mvn")
|
|
_replace_symlink_or_copy(maven_home, install_dir / "current-maven")
|
|
|
|
jdk_home = _existing_link(install_dir / "current-jdk")
|
|
maven_home = _existing_link(install_dir / "current-maven")
|
|
env_path = install_dir / "env.sh"
|
|
summary_path = install_dir / "toolchain-summary.json"
|
|
probes = _probe_tools(jdk_home, maven_home)
|
|
status = "ready" if probes["java"]["available"] and probes["maven"]["available"] else "blocked"
|
|
if jdk_home is not None and maven_home is not None:
|
|
_write_env(env_path, jdk_home, maven_home)
|
|
|
|
summary = {
|
|
"id": "opencmis-local-toolchain",
|
|
"status": status,
|
|
"created_at": _now(),
|
|
"install_dir": str(install_dir),
|
|
"jdk_home": str(jdk_home) if jdk_home else None,
|
|
"maven_home": str(maven_home) if maven_home else None,
|
|
"env_path": str(env_path) if env_path.exists() else None,
|
|
"summary_path": str(summary_path),
|
|
"downloads": downloads,
|
|
"probes": probes,
|
|
}
|
|
summary_path.write_text(json.dumps(summary, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
return summary
|
|
|
|
|
|
def _download(url: str, destination: Path, expected_sha256: str | None = None) -> dict[str, Any]:
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
with _open_url(url, timeout=120) as response:
|
|
with destination.open("wb") as handle:
|
|
shutil.copyfileobj(response, handle)
|
|
digest = _sha256(destination)
|
|
if expected_sha256 and digest.lower() != expected_sha256.lower():
|
|
raise ValueError(f"SHA256 mismatch for {destination.name}")
|
|
return {
|
|
"url": url,
|
|
"path": str(destination),
|
|
"bytes": destination.stat().st_size,
|
|
"sha256": digest,
|
|
}
|
|
|
|
|
|
def _verify_maven_sha512(url: str, archive: Path) -> None:
|
|
with _open_url(url + ".sha512", timeout=60) as response:
|
|
expected = response.read().decode("utf-8").strip().split()[0]
|
|
actual = hashlib.sha512(archive.read_bytes()).hexdigest()
|
|
if actual.lower() != expected.lower():
|
|
raise ValueError(f"SHA512 mismatch for {archive.name}")
|
|
|
|
|
|
def _open_url(url: str, timeout: int):
|
|
request = urllib.request.Request(
|
|
url,
|
|
headers={"User-Agent": "open-cmis-tck-local-toolchain/0.1"},
|
|
)
|
|
return urllib.request.urlopen(request, timeout=timeout)
|
|
|
|
|
|
def _extract_tool(archive: Path, destination: Path, marker: str) -> Path:
|
|
before = {path.resolve() for path in destination.iterdir()} if destination.exists() else set()
|
|
with tarfile.open(archive, "r:gz") as handle:
|
|
_safe_extract(handle, destination)
|
|
after = {path.resolve() for path in destination.iterdir()}
|
|
candidates = sorted(after - before)
|
|
if not candidates:
|
|
candidates = sorted(after)
|
|
for candidate in candidates:
|
|
if (candidate / marker).exists():
|
|
return candidate
|
|
for candidate in sorted(after):
|
|
if (candidate / marker).exists():
|
|
return candidate
|
|
raise ValueError(f"Could not find extracted tool marker {marker!r} from {archive.name}")
|
|
|
|
|
|
def _safe_extract(handle: tarfile.TarFile, destination: Path) -> None:
|
|
destination = destination.resolve()
|
|
for member in handle.getmembers():
|
|
target = (destination / member.name).resolve()
|
|
try:
|
|
target.relative_to(destination)
|
|
except ValueError as exc:
|
|
raise ValueError(f"Archive member escapes destination: {member.name}") from exc
|
|
handle.extractall(destination)
|
|
|
|
|
|
def _replace_symlink_or_copy(source: Path, link: Path) -> None:
|
|
_remove_link_or_dir(link)
|
|
try:
|
|
link.symlink_to(source, target_is_directory=True)
|
|
except OSError:
|
|
shutil.copytree(source, link)
|
|
|
|
|
|
def _remove_link_or_dir(path: Path) -> None:
|
|
if path.is_symlink() or path.is_file():
|
|
path.unlink()
|
|
elif path.exists():
|
|
shutil.rmtree(path)
|
|
|
|
|
|
def _existing_link(path: Path) -> Path | None:
|
|
if not path.exists():
|
|
return None
|
|
return path.resolve()
|
|
|
|
|
|
def _probe_tools(jdk_home: Path | None, maven_home: Path | None) -> dict[str, Any]:
|
|
java = _probe([str(jdk_home / "bin" / "java"), "-version"]) if jdk_home else _missing_probe()
|
|
env = os.environ.copy()
|
|
if jdk_home:
|
|
env["JAVA_HOME"] = str(jdk_home)
|
|
env["PATH"] = f"{jdk_home / 'bin'}{os.pathsep}{env.get('PATH', '')}"
|
|
maven = _probe([str(maven_home / "bin" / "mvn"), "-version"], env=env) if maven_home else _missing_probe()
|
|
return {"java": java, "maven": maven}
|
|
|
|
|
|
def _probe(command: list[str], env: dict[str, str] | None = None) -> dict[str, Any]:
|
|
completed = subprocess.run(
|
|
command,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
check=False,
|
|
env=env,
|
|
)
|
|
output = "\n".join(part.strip() for part in [completed.stdout, completed.stderr] if part.strip())
|
|
return {
|
|
"available": completed.returncode == 0,
|
|
"command": command,
|
|
"returncode": completed.returncode,
|
|
"version_output": output[:4000],
|
|
}
|
|
|
|
|
|
def _missing_probe() -> dict[str, Any]:
|
|
return {
|
|
"available": False,
|
|
"command": None,
|
|
"returncode": None,
|
|
"version_output": None,
|
|
}
|
|
|
|
|
|
def _write_env(path: Path, jdk_home: Path, maven_home: Path) -> None:
|
|
path.write_text(
|
|
"\n".join(
|
|
[
|
|
"# Source this file to use the local OpenCMIS TCK toolchain.",
|
|
f"export JAVA_HOME='{jdk_home}'",
|
|
f"export MAVEN_HOME='{maven_home}'",
|
|
'export PATH="$JAVA_HOME/bin:$MAVEN_HOME/bin:$PATH"',
|
|
"",
|
|
]
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
digest = hashlib.sha256()
|
|
with path.open("rb") as handle:
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
digest.update(chunk)
|
|
return digest.hexdigest()
|
|
|
|
|
|
def _now() -> str:
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|