diff --git a/pkgs/games/papermc/default.nix b/pkgs/games/papermc/default.nix index 0fd50af5b1c2..20c4a349c3db 100644 --- a/pkgs/games/papermc/default.nix +++ b/pkgs/games/papermc/default.nix @@ -1,13 +1,23 @@ let versions = builtins.fromJSON (builtins.readFile ./versions.json); in -{ callPackage, lib, ... }: +{ + callPackage, + javaPackages, + lib, + ... +}: let latestVersion = lib.last (builtins.sort lib.versionOlder (builtins.attrNames versions)); escapeVersion = builtins.replaceStrings [ "." ] [ "_" ]; + getJavaVersion = v: (builtins.getAttr "openjdk${toString v}" javaPackages.compiler).headless; + packages = lib.mapAttrs' (version: value: { name = "papermc-${escapeVersion version}"; - value = callPackage ./derivation.nix { inherit (value) version hash; }; + value = callPackage ./derivation.nix { + inherit (value) url version hash; + jre = getJavaVersion value.javaVersion; + }; }) versions; in lib.recurseIntoAttrs ( diff --git a/pkgs/games/papermc/derivation.nix b/pkgs/games/papermc/derivation.nix index 33171a1ab10e..7e4ffa6eebe8 100644 --- a/pkgs/games/papermc/derivation.nix +++ b/pkgs/games/papermc/derivation.nix @@ -6,6 +6,7 @@ jre, version, hash, + url, udev, }: @@ -13,16 +14,9 @@ stdenvNoCC.mkDerivation (finalAttrs: { pname = "papermc"; inherit version hash; - src = - let - version-split = lib.strings.splitString "-" finalAttrs.version; - mcVersion = builtins.elemAt version-split 0; - buildNum = builtins.elemAt version-split 1; - in - fetchurl { - url = "https://api.papermc.io/v2/projects/paper/versions/${mcVersion}/builds/${buildNum}/downloads/paper-${mcVersion}-${buildNum}.jar"; - inherit (finalAttrs) hash; - }; + src = fetchurl { + inherit url hash; + }; installPhase = '' runHook preInstall @@ -45,7 +39,10 @@ stdenvNoCC.mkDerivation (finalAttrs: { allowSubstitutes = false; passthru = { - updateScript = ./update.py; + updateScript = { + command = [ ./update.py ]; + supportedFeatures = [ "commit" ]; + }; }; meta = { diff --git a/pkgs/games/papermc/update.py b/pkgs/games/papermc/update.py index 7bfc8ee3a0b7..ace0a6fb0469 100755 --- a/pkgs/games/papermc/update.py +++ b/pkgs/games/papermc/update.py @@ -1,19 +1,25 @@ #!/usr/bin/env nix-shell #! nix-shell -i python -p "python3.withPackages (ps: with ps; [ps.requests ])" -import os -import hashlib import base64 import json +import os +import re +from pathlib import Path +from typing import Any import requests +HEADERS = {"User-Agent": "nixpkgs/1.0.0 https://github.com/nixos/nixpkgs"} + class Version: def __init__(self, name: str): self.name: str = name self.hash: str | None = None self.build_number: int | None = None + self.url: str | None = None + self.java_version: int | None = None @property def full_name(self): @@ -27,7 +33,7 @@ class Version: class VersionManager: - def __init__(self, base_url: str = "https://api.papermc.io/v2/projects/paper"): + def __init__(self, base_url: str = "https://fill.papermc.io/v3/projects/paper"): self.versions: list[Version] = [] self.base_url: str = base_url @@ -36,7 +42,8 @@ class VersionManager: Fetch all versions after given minor release """ - response = requests.get(self.base_url) + url = f"{self.base_url}/versions" + response = requests.get(url, headers=HEADERS) try: response.raise_for_status() @@ -45,19 +52,24 @@ class VersionManager: print(e) return - # we only want versions that are no pre-releases - release_versions = filter( - lambda v_name: all(s not in v_name for s in ["pre", "rc"]), response.json()["versions"]) + release_versions = response.json()["versions"] for version_name in release_versions: + version_id = version_name["version"]["id"] + + # we only want versions that are not pre-releases + if ("pre" in version_id) or ("rc" in version_id): + continue # split version string, convert to list ot int - version_split = version_name.split(".") + version_split = version_id.split(".") version_split = list(map(int, version_split)) # check if version is higher than 1. - if (version_split[0] > 1) or (version_split[0] == 1 and version_split[1] >= not_before_minor_version): - self.versions.append(Version(version_name)) + if (version_split[0] > 1) or ( + version_split[0] == 1 and version_split[1] >= not_before_minor_version + ): + self.versions.append(Version(version_id)) def fetch_latest_version_builds(self): """ @@ -65,8 +77,60 @@ class VersionManager: """ for version in self.versions: - url = f"{self.base_url}/versions/{version.name}" - response = requests.get(url) + body = self.fetch_latest_build(version.name) + + # the latest build from the api + latest_build = body["id"] + version.build_number = latest_build + + version.java_version = self.fetch_java_version(version.name) + + # Grab the url from the api + download_info = body["downloads"]["server:default"] + version.url = download_info["url"] + + hex_hash = download_info["checksums"]["sha256"] + raw_bytes = bytes.fromhex(hex_hash) + + base64_encoded = base64.b64encode(raw_bytes).decode("utf-8") + + version.hash = f"sha256-{base64_encoded}" + + def fetch_latest_build(self, name: str) -> dict[str, Any]: + url = f"{self.base_url}/versions/{name}/builds/latest" + response = requests.get(url, headers=HEADERS) + + # check that we've got a good response + try: + response.raise_for_status() + return response.json() + except requests.exceptions.HTTPError as e: + print(e) + return {} + + def fetch_java_version(self, name: str): + url = f"{self.base_url}/versions/{name}" + response = requests.get(url, headers=HEADERS) + + # check that we've got a good response + try: + response.raise_for_status() + return response.json()["version"]["java"]["version"]["minimum"] + except requests.exceptions.HTTPError as e: + print(e) + return + + def generate_version_hashes(self): + """ + Fetch and set the hashes for all registered versions (versions are downloaded to memory) + """ + + print("Fetching version hashes") + for version in self.versions: + url = ( + f"{self.base_url}/versions/{version.name}/builds/{version.build_number}" + ) + response = requests.get(url, headers=HEADERS) # check that we've got a good response try: @@ -76,77 +140,129 @@ class VersionManager: print(e) return - # the highest build in response.json()['builds']: - latest_build = response.json()['builds'][-1] - version.build_number = latest_build + def versions_to_dict(self) -> dict[str, Any]: + return { + version.name: { + "hash": version.hash, + "version": version.full_name, + "url": version.url, + "javaVersion": version.java_version, + } + for version in self.versions + } - def generate_version_hashes(self): - """ - Generate and set the hashes for all registered versions (versions will are downloaded to memory) - """ - - for version in self.versions: - url = f"{self.base_url}/versions/{version.name}/builds/{version.build_number}/downloads/paper-{version.full_name}.jar" - version.hash = self.download_and_generate_sha256_hash(url) - - def versions_to_json(self): - return json.dumps( - {version.name: {'hash': version.hash, 'version': version.full_name} - for version in self.versions}, - indent=4 - ) + def generate_version_dict(self) -> dict[str, Any]: + self.fetch_versions() + self.fetch_latest_version_builds() + return self.versions_to_dict() + @staticmethod def find_version_json() -> str: """ Find the versions.json file in the same directory as this script """ - return os.path.join(os.path.dirname(os.path.realpath(__file__)), "versions.json") - - def write_versions(self, file_name: str = find_version_json()): - """ write all processed versions to json """ - # save json to versions.json - with open(file_name, 'w') as f: - f.write(self.versions_to_json() + "\n") - - @staticmethod - def download_and_generate_sha256_hash(url: str) -> str | None: - """ - Fetch the tarball from the given URL. - Then generate a sha256 hash of the tarball. - """ - - try: - # Download the file from the URL - response = requests.get(url) - response.raise_for_status() - - except requests.exceptions.RequestException as e: - print(f"Error: {e}") - return None - - # Create a new SHA-256 hash object - sha256_hash = hashlib.sha256() - - # Update the hash object with chunks of the downloaded content - for byte_block in response.iter_content(4096): - sha256_hash.update(byte_block) - - # Get the hexadecimal representation of the hash - hash_value = sha256_hash.digest() - - # Encode the hash value in base64 - base64_hash = base64.b64encode(hash_value).decode('utf-8') - - # Format it as "sha256-{base64_hash}" - sri_representation = f"sha256-{base64_hash}" - - return sri_representation + return os.path.join( + os.path.dirname(os.path.realpath(__file__)), "versions.json" + ) -if __name__ == '__main__': +def get_latest(servers: dict[str, Any]) -> str | None: + return max( + (v.get("version") for v in servers.values()), + key=lambda x: tuple(map(int, re.split("[\\.|-]", x))) if x is not None else (), + ) + + +def slugify(version: str) -> str: + return version.replace(".", "_") + + +def generate_commit( + previous_servers: dict[str, Any], + servers: dict[str, Any], + versions_file: Path, +) -> list[dict[str, str | list[str]]]: + actions = [] + commit_body_lines = [] + + old_latest = get_latest(previous_servers) + new_latest = get_latest(servers) + + for major_version, server in servers.items(): + version = server.get("version") + previous_server = previous_servers.get(major_version) + + if version is None: + continue + + attribute = f"papermcServers.papermc-{slugify(major_version)}" + + if not previous_server: + # this version didn't exist before + # check if its now the latest version + if version == new_latest: + action = f"{old_latest} -> {new_latest}" + attribute = "papermc" + else: + action = f"init {version}" + + else: + previous_version = previous_server.get("version") + if previous_version == version: + continue + + action = f"{previous_version} -> {version}" + + actions.append(action) + + commit_body_lines.append(f"{attribute}: {action}") + + if not commit_body_lines: + return [] + + if len(actions) == 1: + commit_message = commit_body_lines[0] + + # the body should only be the release notes to avoid repetition + # if the release notes don't exist this will be blank + commit_body = "\n".join(commit_body_lines[1:]).strip() + else: + detailed_message = f"papermc: {', '.join(actions)}" + + commit_message = ( + detailed_message + if len(detailed_message) <= 72 + else "papermc: update multiple versions" + ) + + commit_body = "\n".join(commit_body_lines).strip() + + commit_json = { + "attrPath": "papermcServers.papermc", + "files": [str(versions_file)], + "commitMessage": commit_message, + } + + if commit_body: + commit_json["commitBody"] = commit_body + + return [commit_json] + + +if __name__ == "__main__": + versions_file = Path(__file__).parent / "versions.json" + + with open(versions_file, "r") as f: + old_versions = json.load(f) + version_manager = VersionManager() - version_manager.fetch_versions() - version_manager.fetch_latest_version_builds() - version_manager.generate_version_hashes() - version_manager.write_versions() + new_versions = version_manager.generate_version_dict() + + commit_json = generate_commit(old_versions, new_versions, versions_file) + + with open(versions_file, "w") as f: + json.dump(new_versions, f, indent=4) + f.write("\n") + + print(json.dumps(commit_json))