195 lines
6.2 KiB
Python
Executable File
195 lines
6.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
import hashlib
|
|
from typing import Any, Literal, TypedDict
|
|
from pathlib import Path
|
|
import urllib.request
|
|
import json
|
|
import pickle
|
|
import subprocess
|
|
import argparse
|
|
from datetime import datetime, timedelta
|
|
|
|
ReleaseType = Literal["release", "snapshot", "old_beta", "old_alpha"]
|
|
|
|
|
|
class LatestVersions(TypedDict):
|
|
release: str
|
|
snapshot: str
|
|
|
|
|
|
class Version(TypedDict):
|
|
id: str
|
|
type: ReleaseType
|
|
url: str
|
|
time: str
|
|
releaseTime: str
|
|
|
|
|
|
class JavaVersion(TypedDict):
|
|
component: str
|
|
majorVersion: int
|
|
|
|
|
|
class DownloadInfo(TypedDict):
|
|
sha1: str
|
|
size: int
|
|
url: str
|
|
|
|
|
|
class VersionDownloads(TypedDict):
|
|
client: DownloadInfo
|
|
client_mappings: DownloadInfo
|
|
server: DownloadInfo
|
|
server_mappings: DownloadInfo
|
|
|
|
|
|
class VersionManifestFull(TypedDict):
|
|
id: str
|
|
mainClass: str
|
|
type: ReleaseType
|
|
time: str
|
|
releaseTime: str
|
|
minimumLauncherVersion: int
|
|
javaVersion: JavaVersion
|
|
downloads: VersionDownloads
|
|
|
|
|
|
class DockerfileBuildArgs(TypedDict):
|
|
DOCKER_IMAGE: str
|
|
VERSION_ID: str
|
|
VERSION_SHA1: str
|
|
|
|
class VersionManifest(TypedDict):
|
|
latest: LatestVersions
|
|
versions: list[Version]
|
|
|
|
|
|
def load_version_manifest(version: Version):
|
|
# Download new manifest
|
|
with urllib.request.urlopen(version["url"]) as url:
|
|
data: VersionManifestFull = json.load(url)
|
|
return data
|
|
|
|
|
|
def load_manifest(reload: bool = False, max_age: timedelta = timedelta(hours=1)):
|
|
manifest_cache = "manifest.cache"
|
|
# Load cached manifest
|
|
if reload:
|
|
print("Forced manifest reload.")
|
|
else:
|
|
try:
|
|
with open(manifest_cache, "rb") as f:
|
|
time: datetime
|
|
data: VersionManifest
|
|
(time, data) = pickle.load(f)
|
|
cache_age = datetime.now() - time
|
|
if cache_age <= max_age:
|
|
print(f"Loaded cache file with age {cache_age}.")
|
|
return data
|
|
print(f"Cache is older than {max_age} and will be reloaded.")
|
|
except FileNotFoundError:
|
|
print("No cached manifest found!")
|
|
|
|
# Download new manifest
|
|
with urllib.request.urlopen("https://launchermeta.mojang.com/mc/game/version_manifest.json") as url:
|
|
data: VersionManifest = json.load(url)
|
|
|
|
# Save manifest to cache
|
|
with open(manifest_cache, "wb") as f:
|
|
pickle.dump((datetime.now(), data), f)
|
|
|
|
return data
|
|
|
|
|
|
BUF_SIZE = 65536
|
|
|
|
|
|
def calculate_hash(filename: Path):
|
|
with filename.open("rb") as f:
|
|
sha1 = hashlib.sha1()
|
|
while True:
|
|
data = f.read(BUF_SIZE)
|
|
if not data:
|
|
break
|
|
sha1.update(data)
|
|
return sha1
|
|
|
|
|
|
def download_file(file: DownloadInfo, filename: Path):
|
|
if not filename.exists():
|
|
filename.parent.mkdir(exist_ok=True, parents=True)
|
|
print(f"Downloading new file {file['url']}")
|
|
urllib.request.urlretrieve(file["url"], filename)
|
|
sha1 = calculate_hash(filename)
|
|
if sha1.hexdigest() == file["sha1"]:
|
|
print("Hashes match!")
|
|
else:
|
|
raise Exception(
|
|
f"File has sha1 {sha1.hexdigest()}, expected {file['sha1']}")
|
|
return filename
|
|
|
|
|
|
default_build_platforms = ["linux/arm64/v8", "linux/arm/v7", "linux/amd64", "linux/ppc64le"]
|
|
|
|
|
|
def docker_buildx(repository: str, tags: list[str], build_platforms: list[str] | None = None, dockerfile: str = "Dockerfile", build_args: DockerfileBuildArgs | dict[str, Any] | None = None, directory: str = "."):
|
|
if build_platforms is None:
|
|
build_platforms = default_build_platforms
|
|
if build_args is None:
|
|
build_args = dict()
|
|
labels = [f"{repository}:{tag}"for tag in tags]
|
|
command = ["docker", "buildx", "build",
|
|
"--platform", ",".join(build_platforms),
|
|
*[t for (key, value) in build_args.items()
|
|
for t in ("--build-arg", f"{key}={value}")],
|
|
"--file", dockerfile,
|
|
*[t for label in labels for t in ("--tag", label)],
|
|
"--pull", "--push", directory]
|
|
print(" ".join(command))
|
|
subprocess.run(command)
|
|
|
|
|
|
def build_version(manifest: VersionManifest, version_id: str, repository: str = "hub.cnml.de/minecraft"):
|
|
matching_version = list(
|
|
filter(lambda v: v["id"] == version_id, manifest["versions"]))
|
|
if len(matching_version) == 0:
|
|
raise Exception(f"Version {version_id} not found in manifest!")
|
|
version = load_version_manifest(matching_version[0])
|
|
java_version = version['javaVersion']
|
|
print(
|
|
f"Version [{version['type']}] {version['id']} requires java version {java_version['majorVersion']} ({java_version['component']})")
|
|
server_jar_file = Path("versions") / version['id'] / "server.jar"
|
|
server_jar = version["downloads"]["server"]
|
|
download_file(server_jar, server_jar_file)
|
|
|
|
build_args: DockerfileBuildArgs = {
|
|
"VERSION_ID": version['id'],
|
|
"VERSION_SHA1": server_jar['sha1'],
|
|
"DOCKER_IMAGE": ""
|
|
}
|
|
|
|
# Build GraalVM images
|
|
build_args["DOCKER_IMAGE"] = f"ghcr.io/graalvm/jdk:java{java_version['majorVersion']}"
|
|
docker_buildx(repository, [version["id"], f"{version['id']}-graalvm"], build_args=build_args, build_platforms=["linux/amd64", "linux/arm64"])
|
|
|
|
# Build Temurin
|
|
build_args["DOCKER_IMAGE"] = f"eclipse-temurin:{java_version['majorVersion']}-jre"
|
|
docker_buildx(repository, [f"{version['id']}-temurin"], build_args=build_args)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
"build", description="Utility script to build docker images for Minecraft servers.")
|
|
parser.add_argument("versions", nargs="*", type=str,
|
|
help="Versions to build for.")
|
|
parser.add_argument("--reload", action="store_true", help="Force reload the manifest.")
|
|
parser.add_argument("--all", action="store_true", help="Build all versions.")
|
|
args = parser.parse_args()
|
|
manifest = load_manifest(args.reload)
|
|
versions: list[str] = list(version["id"] for version in manifest["versions"] if version["type"] == "release") if args.all else args.versions
|
|
for version_id in versions:
|
|
try:
|
|
build_version(manifest, version_id)
|
|
except Exception as e:
|
|
print(f"Failed building images for version {version_id}:\n\t{e}")
|