Files
escape-from-funcraft-client/tools/build_beta_release.py
T
2026-07-18 19:58:41 +05:00

164 lines
5.9 KiB
Python

from __future__ import annotations
import argparse
import os
import shutil
import subprocess
import tomllib
import urllib.request
from pathlib import Path
try:
from .beta_release_lib import (
create_zip,
create_tar_gz,
copy_classified_tree,
load_path_rules,
load_side_manifest,
sanitize_client_stage,
validate_complete_mapping,
validate_client_stage,
validate_server_stage,
write_keybind_options,
)
except ImportError:
from beta_release_lib import (
create_zip,
create_tar_gz,
copy_classified_tree,
load_path_rules,
load_side_manifest,
sanitize_client_stage,
validate_complete_mapping,
validate_client_stage,
validate_server_stage,
write_keybind_options,
)
def version_directory(output_root: Path, version: str) -> Path:
resolved_root = output_root.resolve()
candidate = (resolved_root / version).resolve()
if candidate.parent != resolved_root:
raise ValueError(f"invalid release version: {version}")
return candidate
def source_mod_names(source: Path) -> set[str]:
return {path.name for path in (source / "minecraft/mods").glob("*.jar")}
def forge_request(url: str) -> urllib.request.Request:
return urllib.request.Request(
url,
headers={"User-Agent": "EscapeFromFuncraftBetaBuilder/1.0"},
)
def forge_install_env(compatibility_library_path: Path | None) -> dict[str, str] | None:
if compatibility_library_path is None:
return None
env = os.environ.copy()
existing = env.get("LD_LIBRARY_PATH")
env["LD_LIBRARY_PATH"] = (
f"{compatibility_library_path}:{existing}" if existing else str(compatibility_library_path)
)
return env
def download_forge_installer(
server_stage: Path, compatibility_library_path: Path | None = None
) -> None:
version = "1.20.1-47.4.10"
base = f"https://maven.minecraftforge.net/net/minecraftforge/forge/{version}/forge-{version}-installer.jar"
installer = server_stage / f"forge-{version}-installer.jar"
expected_sha1 = (
urllib.request.urlopen(forge_request(base + ".sha1"), timeout=30)
.read()
.decode("utf-8")
.strip()
.split()[0]
)
with urllib.request.urlopen(forge_request(base), timeout=60) as response:
installer.write_bytes(response.read())
import hashlib
actual_sha1 = hashlib.sha1(installer.read_bytes()).hexdigest()
if actual_sha1 != expected_sha1:
raise ValueError(f"Forge installer checksum mismatch: {actual_sha1} != {expected_sha1}")
subprocess.run(
["java", "-jar", installer.name, "--installServer", "."],
cwd=server_stage,
check=True,
env=forge_install_env(compatibility_library_path),
)
installer.unlink()
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--source", required=True, type=Path)
parser.add_argument("--client-output", required=True, type=Path)
parser.add_argument("--server-output", required=True, type=Path)
parser.add_argument("--version", required=True)
parser.add_argument("--manifest", type=Path, default=Path("packaging/mod-classification.toml"))
parser.add_argument("--path-manifest", type=Path, default=Path("packaging/path-classification.toml"))
parser.add_argument("--release-config", type=Path, default=Path("packaging/beta-release.toml"))
parser.add_argument(
"--forge-installer-lib-path",
type=Path,
help="directory containing a zlib implementation compatible with Forge's processors",
)
args = parser.parse_args()
source = args.source.resolve()
client_version_root = version_directory(args.client_output, args.version)
server_version_root = version_directory(args.server_output, args.version)
for root in (client_version_root, server_version_root):
if root.exists():
shutil.rmtree(root)
client_stage = client_version_root / "client-stage"
server_stage = server_version_root / "server-stage"
mod_map = load_side_manifest(args.manifest)
validate_complete_mapping(source_mod_names(source), mod_map)
path_rules = load_path_rules(args.path_manifest)
release = tomllib.loads(args.release_config.read_text(encoding="utf-8"))
world_source = source / release["source"]["world"]
world_name = world_source.name
copy_classified_tree(source, client_stage, "client", mod_map, path_rules)
copy_classified_tree(source, server_stage, "server", mod_map, path_rules)
write_keybind_options(source / "minecraft/options.txt", client_stage / "minecraft/options.txt")
sanitize_client_stage(client_stage)
validate_client_stage(client_stage, mod_map)
client_archive = client_version_root / f"escape-from-funcraft-client-{args.version}.zip"
digest = create_zip(client_stage, client_archive)
(client_version_root / "SHA256SUMS").write_text(
f"{digest} {client_archive.name}\n", encoding="utf-8"
)
shutil.copytree(world_source, server_stage / world_name)
(server_stage / "eula.txt").write_text("eula=false\n", encoding="utf-8")
(server_stage / "server.properties").write_text(
"\n".join(
(
f"level-name={world_name}",
"online-mode=true",
"enable-command-block=true",
"spawn-protection=0",
"",
)
),
encoding="utf-8",
)
validate_server_stage(server_stage, mod_map, world_name)
download_forge_installer(server_stage, args.forge_installer_lib_path)
server_archive = server_version_root / f"escape-from-funcraft-server-{args.version}.tar.gz"
server_digest = create_tar_gz(server_stage, server_archive)
(server_version_root / "SHA256SUMS").write_text(
f"{server_digest} {server_archive.name}\n", encoding="utf-8"
)
if __name__ == "__main__":
main()