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()