From ad88921f1c470cc2472aa1178c155d72b72d1659 Mon Sep 17 00:00:00 2001 From: Michael Chen Date: Tue, 15 Nov 2022 02:27:22 +0100 Subject: [PATCH] Added build script for graal and temurin builds --- .gitignore | 2 + Dockerfile | 19 ++++++ build.py | 194 +++++++++++++++++++++++++++++++++++++++++++++++++++++ eula.txt | 3 + 4 files changed, 218 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 build.py create mode 100644 eula.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..984b156 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +versions/ +*.cache \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7e90566 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +ARG DOCKER_IMAGE +FROM ${DOCKER_IMAGE} + +ARG VERSION_ID +ARG VERSION_SHA1 +ENV MINECRAFT_VERSION=${VERSION_ID} +ENV INIT_MEMORY="512M" +ENV MAX_MEMORY="2G" +WORKDIR /server + +# Copy server file and verify checksum +VOLUME [ "/server" ] +COPY versions/${VERSION_ID}/server.jar /server.jar +COPY eula.txt /server/ +RUN echo -n "${VERSION_SHA1} /server.jar" | sha1sum -c - + +# Run server +EXPOSE 25565 +ENTRYPOINT ["java", "-jar", "/server.jar", "--nogui", "-Xms${INIT_MEMORY}", "-Xmx${MAX_MEMORY}"] \ No newline at end of file diff --git a/build.py b/build.py new file mode 100644 index 0000000..26d3892 --- /dev/null +++ b/build.py @@ -0,0 +1,194 @@ +#!/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}") diff --git a/eula.txt b/eula.txt new file mode 100644 index 0000000..0be04d8 --- /dev/null +++ b/eula.txt @@ -0,0 +1,3 @@ +#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://account.mojang.com/documents/minecraft_eula). +#Mon Nov 14 14:28:07 UTC 2022 +eula=true