docker-minecraft-server/build.py

246 lines
8.3 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
import hashlib
from typing import Any, Literal, NotRequired, 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"]
2022-11-15 09:37:31 +01:00
DockerPlatforms = Literal["linux/amd64", "linux/amd64/v2", "linux/amd64/v3", "linux/arm64", "linux/riscv64",
"linux/ppc64le", "linux/s390x", "linux/386", "linux/mips64le", "linux/mips64", "linux/arm/v7", "linux/arm/v6"]
supported_docker_platforms: list[DockerPlatforms] = ["linux/amd64", "linux/amd64/v2", "linux/amd64/v3", "linux/arm64",
"linux/riscv64", "linux/ppc64le", "linux/s390x", "linux/386", "linux/mips64le", "linux/mips64", "linux/arm/v7", "linux/arm/v6"]
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):
2022-11-15 09:29:32 +01:00
client: NotRequired[DownloadInfo]
client_mappings: NotRequired[DownloadInfo]
server: NotRequired[DownloadInfo]
server_mappings: NotRequired[DownloadInfo]
class VersionManifestFull(TypedDict):
id: str
mainClass: str
type: ReleaseType
time: str
releaseTime: str
minimumLauncherVersion: int
javaVersion: NotRequired[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():
2022-11-15 02:48:10 +01:00
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
2022-11-15 09:46:03 +01:00
def docker_buildx(repository: str, tags: list[str], build_platforms: list[DockerPlatforms], dockerfile: str = "Dockerfile", build_args: DockerfileBuildArgs | dict[str, Any] | None = None, directory: str = ".", write_command: bool = False):
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]
2022-11-15 09:46:03 +01:00
if write_command:
print(" ".join(command))
process = subprocess.run(
command, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
2022-11-15 09:42:05 +01:00
if process.returncode != 0:
2022-11-15 09:43:07 +01:00
output = process.stdout.decode('utf-8')
print(output)
2022-11-15 09:42:05 +01:00
error = process.stderr.decode('utf-8')
raise Exception(error)
default_java_version: JavaVersion = {
"component": "jre",
"majorVersion": 8
}
2022-11-15 09:37:31 +01:00
2022-11-15 09:29:32 +01:00
class NoServerException(Exception):
pass
2022-11-15 09:37:31 +01:00
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.get("javaVersion", default_java_version)
print(
f"Version [{version['type']}] {version['id']} requires java version {java_version['majorVersion']} ({java_version['component']})")
2022-11-15 02:48:10 +01:00
server_jar_file = Path("versions") / version['id'] / "server.jar"
2022-11-15 09:29:32 +01:00
server_jar = version["downloads"].get("server")
if server_jar is None:
2022-11-15 09:37:31 +01:00
raise NoServerException(
f"No server build exists for {version['id']} (Java {java_version['majorVersion']})")
2022-11-15 09:29:32 +01:00
download_file(server_jar, server_jar_file)
build_args: DockerfileBuildArgs = {
"VERSION_ID": version['id'],
"VERSION_SHA1": server_jar['sha1'],
"DOCKER_IMAGE": ""
}
2022-11-15 09:37:31 +01:00
print(
f"Building version {version['id']} (Java {java_version['majorVersion']})")
2022-11-15 09:46:03 +01:00
print("# Build GraalVM images")
if java_version['majorVersion'] == 17:
graal_platforms: list[DockerPlatforms] = ["linux/arm64", "linux/amd64"]
build_args["DOCKER_IMAGE"] = f"ghcr.io/graalvm/jdk:java{java_version['majorVersion']}"
2022-11-15 09:37:31 +01:00
docker_buildx(repository, [
f"{version['id']}-graalvm"], graal_platforms, build_args=build_args)
else:
2022-11-15 09:37:31 +01:00
print(
f"No GraalVM image can be built for {version['id']} (Java {java_version['majorVersion']})")
2022-11-15 09:46:03 +01:00
print("# Build Temurin images")
build_args["DOCKER_IMAGE"] = f"eclipse-temurin:{java_version['majorVersion']}-jre"
2022-11-15 09:37:31 +01:00
temurin_platforms: list[DockerPlatforms] = [
"linux/arm64", "linux/arm/v7", "linux/amd64", "linux/ppc64le"]
docker_buildx(
repository, [version["id"], f"{version['id']}-temurin"], temurin_platforms, 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.")
parser.add_argument("--force", action="store_true",
help="Force continue on single version build failure.")
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
2022-11-15 09:37:31 +01:00
errors: list[tuple[str, Exception]] = []
for version_id in versions:
2022-11-15 09:29:32 +01:00
print()
try:
build_version(manifest, version_id)
2022-11-15 09:29:32 +01:00
except NoServerException as e:
print(e)
2022-11-15 09:37:31 +01:00
errors.append((version_id, e))
except Exception as e:
print(f"Failed building images for version {version_id}")
if args.force:
print(e)
2022-11-15 09:37:31 +01:00
errors.append((version_id, e))
else:
raise
2022-11-15 09:37:31 +01:00
for (version_id, e) in errors:
2022-11-15 09:46:03 +01:00
print(f"Error building version {version_id}:\n\t{e}")