docker-minecraft-server/build.py

195 lines
6.2 KiB
Python

#!/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)
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(f"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}")