Files
2026-08-30 23:09:37 +02:00

242 lines
7.8 KiB
Python

#!/usr/bin/env python3
"""Build a Paper Docker image locally.
Usage: python build.py <MC_VERSION>
"""
import json
import subprocess
import sys
import traceback
import urllib.request
import zipfile
from pathlib import Path
DOCKER_TAG_BASE = "cr.pandacube.fr/paper"
URL_BASE = "https://fill.papermc.io/v3/projects/paper"
WORKSPACE = Path(__file__).resolve().parent
LIBRARIES_TO_INCLUDE_IN_POM = [
("io.netty", "netty-buffer")
]
def run_command(*command: str, capture_output: bool = False) -> str:
"""Run a command and return its output when requested."""
result = subprocess.run(command, check=True, cwd=WORKSPACE, text=True,
stdout=subprocess.PIPE if capture_output else sys.stdout,
stderr=sys.stderr
)
return result.stdout.strip() if capture_output else ""
app_git_commit = run_command("git", "rev-parse", "--short", "HEAD", capture_output=True)
USER_AGENT = f"PaperDockerBuilder/{app_git_commit} (https://github.com/PandacubeFr/PaperDockerBuilder)"
def download(url: str, destination: Path) -> None:
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(request) as response:
if response.status != 200:
raise RuntimeError(f"Failed to download {url}: HTTP {response.status}")
with destination.open("wb") as output:
while chunk := response.read(1024 * 1024 * 16):
output.write(chunk)
print(f"\rDownloaded {output.tell()} bytes", end="", flush=True)
print()
def getURL(url: str) -> str:
request = urllib.request.Request(url, headers={"User-Agent": USER_AGENT})
with urllib.request.urlopen(request) as response:
if response.status != 200:
raise RuntimeError(f"Failed to get {url}: HTTP {response.status}")
return response.read().decode(response.headers.get_content_charset() or "utf-8")
def get_file_content_in_zip(zip_path: Path, file_name: str) -> str:
"""Extract the content of a file inside a zip archive."""
with zipfile.ZipFile(zip_path) as jar:
return jar.read(file_name).decode("utf-8")
class Library:
def __init__(self, group: str, artifact: str, version: str, path: str):
self.group = group
self.artifact = artifact
self.version = version
self.path = path
@staticmethod
def from_string(library_line: str) -> Library:
lib_params = library_line.split()
parts = lib_params[1].split(":", 2)
if len(parts) != 3:
raise ValueError(f"Invalid library string format: {library_line}")
return Library(parts[0], parts[1], parts[2], lib_params[2])
@staticmethod
def list_from_string(libraries_list_content: str) -> list[Library]:
return [Library.from_string(line) for line in libraries_list_content.splitlines()]
@staticmethod
def find_library(libraries: list[Library], group: str, artifact: str) -> Library | None:
for library in libraries:
if library.group == group and library.artifact == artifact:
return library
return None
def generate_pom_xml(group_id: str, artifact_id: str, version: str, dependencies: list[Library]) -> str:
dependencies_xml = "".join(
f"""
<dependency>
<groupId>{dep.group}</groupId>
<artifactId>{dep.artifact}</artifactId>
<version>{dep.version}</version>
</dependency>
"""
for dep in dependencies
)
return f"""
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>{group_id}</groupId>
<artifactId>{artifact_id}</artifactId>
<version>{version}</version>
<dependencies>
{dependencies_xml}
</dependencies>
</project>
"""
def main() -> int:
if len(sys.argv) != 2:
print(f"Usage: {Path(sys.argv[0]).name} <MC_VERSION>")
print(f"Example: {Path(sys.argv[0]).name} 1.20.1")
return 1
mc_version = sys.argv[1]
version_url = f"{URL_BASE}/versions/{mc_version}"
build_url = f"{version_url}/builds/latest"
print("=== Getting build data ===")
version_infos = json.loads(getURL(version_url))
build_infos = json.loads(getURL(build_url))
app_build = build_infos["id"]
download_url = build_infos["downloads"]["server:default"]["url"]
app_filename = f"Paper-{mc_version}-{app_build}.jar"
app_path = WORKSPACE / app_filename
jdk_version = str(version_infos["version"]["java"]["version"]["minimum"])
with (WORKSPACE / "jdk_versions.json").open() as jdk_versions_file:
jdk_tag = json.load(jdk_versions_file).get(jdk_version)
if not jdk_tag:
print(f"Error: JDK version {jdk_version} is not listed in jdk_versions.json.", file=sys.stderr)
print(
f"Please update jdk_versions.json with a Docker base image for JDK {jdk_version}.",
file=sys.stderr,
)
return 1
docker_tag = f"{DOCKER_TAG_BASE}:{mc_version}-{app_build}"
docker_tag_version = f"{DOCKER_TAG_BASE}:{mc_version}"
print(f"Paper version {mc_version} build #{app_build}")
print("\n=== Downloading jar ===")
download(download_url, app_path)
print(f"Downloaded: {app_filename}")
print(f"\n=== Extracting libraries info from Paper jar ===")
libraries_str = get_file_content_in_zip(app_path, "META-INF/libraries.list")
libraries = Library.list_from_string(libraries_str)
api_lib = Library.find_library(libraries, "io.papermc.paper", "paper-api")
if not api_lib:
raise RuntimeError("Unable to find Paper API library in the extracted libraries.")
print(f"\nPaper API Version is: {api_lib.version}")
sever_libs_to_include_in_pom: list[Library] = [lib
for l in LIBRARIES_TO_INCLUDE_IN_POM
if (lib := Library.find_library(libraries, l[0], l[1]))]
for lib in sever_libs_to_include_in_pom:
print(f"Library data {lib.group}:{lib.artifact}:{lib.version}.")
print("\n=== Building Docker image ===")
print(f"Using base image: {jdk_tag} (for JDK {jdk_version})")
run_command("docker", "build", "-t", docker_tag,
"--build-arg", f"RUNNABLE_SERVER_JAR={app_filename}",
"--build-arg", f"JDK_TAG={jdk_tag}",
"."
)
run_command("docker", "tag", docker_tag, docker_tag_version)
print("=== Extracting Paper patched jar for local mvn install ===")
container_id = run_command("docker", "create", docker_tag, capture_output=True)
paper_server_filename = f"paper-server-{api_lib.version}.jar"
try:
run_command(
"docker", "cp",
f"{container_id}:/data/bundle/versions/{mc_version}/paper-{mc_version}.jar",
paper_server_filename,
)
finally:
run_command("docker", "rm", container_id)
print("\n=== Generating POM file for Paper patched jar ===")
pom_content = generate_pom_xml(
group_id="io.papermc.paper",
artifact_id="paper-server",
version=api_lib.version,
dependencies=sever_libs_to_include_in_pom
)
pom_name = f"generated_pom-{api_lib.version}.xml"
with open(pom_name, "w") as f:
f.write(pom_content)
print("\n=== Installing Paper patched jar on local Maven repository ===")
run_command("mvn.cmd" if sys.platform == "win32" else "mvn",
"install:install-file", f"-Dfile=./{paper_server_filename}", f"-DpomFile=./{pom_name}")
print("\nDocker images built successfully:")
print(f" - {docker_tag}")
print(f" - {docker_tag_version}")
print("\nPaper patched jar installed successfully on local Maven repository:")
print(f" - io.papermc.paper:paper-server:{api_lib.version}")
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except Exception as e:
print(f"Error: {traceback.format_exc()}", file=sys.stderr)
raise SystemExit(1)