import copy from datetime import datetime from pathlib import Path from typing import Optional, List, Dict, Any, Iterator import pydantic from pydantic import Field, validator # type: ignore from ..common import ( LAUNCHER_MAVEN, serialize_datetime, replace_old_launchermeta_url, get_all_bases, merge_dict, ) META_FORMAT_VERSION = 1 class GradleSpecifier: """ A gradle specifier - a maven coordinate. Like one of these: "org.lwjgl.lwjgl:lwjgl:2.9.0" "net.java.jinput:jinput:2.0.5" "net.minecraft:launchwrapper:1.5" """ def __init__( self, group: str, artifact: str, version: str, classifier: Optional[str] = None, extension: Optional[str] = None, ): if extension is None: extension = "jar" self.group = group self.artifact = artifact self.version = version self.classifier = classifier self.extension = extension def __str__(self): ext = "" if self.extension != "jar": ext = "@%s" % self.extension if self.classifier: return "%s:%s:%s:%s%s" % ( self.group, self.artifact, self.version, self.classifier, ext, ) else: return "%s:%s:%s%s" % (self.group, self.artifact, self.version, ext) def filename(self): if self.classifier: return "%s-%s-%s.%s" % ( self.artifact, self.version, self.classifier, self.extension, ) else: return "%s-%s.%s" % (self.artifact, self.version, self.extension) def base(self): return "%s/%s/%s/" % (self.group.replace(".", "/"), self.artifact, self.version) def path(self): return self.base() + self.filename() def __repr__(self): return f"GradleSpecifier('{self}')" def is_lwjgl(self): return self.group in ( "org.lwjgl", "org.lwjgl.lwjgl", "net.java.jinput", "net.java.jutils", ) def is_log4j(self): return self.group == "org.apache.logging.log4j" def __eq__(self, other: Any): if isinstance(other, GradleSpecifier): return str(self) == str(other) else: return False def __lt__(self, other: "GradleSpecifier"): return str(self) < str(other) def __gt__(self, other: "GradleSpecifier"): return str(self) > str(other) def __hash__(self): return hash(str(self)) @classmethod def __get_validators__(cls): yield cls.validate @classmethod def from_string(cls, v: str): ext_split = v.split("@") components = ext_split[0].split(":") group = components[0] artifact = components[1] version = components[2] extension = None if len(ext_split) == 2: extension = ext_split[1] classifier = None if len(components) == 4: classifier = components[3] return cls(group, artifact, version, classifier, extension) @classmethod def validate(cls, v: "str | GradleSpecifier"): if isinstance(v, cls): return v if isinstance(v, str): return cls.from_string(v) raise TypeError("Invalid type") class MetaBase(pydantic.BaseModel): def dict(self, **kwargs: Any) -> Dict[str, Any]: for k in ["by_alias"]: if k in kwargs: del kwargs[k] return super(MetaBase, self).dict(by_alias=True, **kwargs) def json(self, **kwargs: Any) -> str: for k in ["exclude_none", "sort_keys", "indent"]: if k in kwargs: del kwargs[k] return super(MetaBase, self).json( exclude_none=True, sort_keys=True, by_alias=True, indent=4, **kwargs ) def write(self, file_path: str): Path(file_path).parent.mkdir(parents=True, exist_ok=True) with open(file_path, "w") as f: f.write(self.json()) def merge(self, other: "MetaBase"): """ Merge other object with self. - Concatenates lists - Combines sets - Merges dictionaries (other takes priority) - Recurses for all fields that are also MetaBase classes - Overwrites for any other field type (int, string, ...) """ assert type(other) is type(self) for key, field in self.__fields__.items(): ours = getattr(self, key) theirs = getattr(other, key) if theirs is None: continue if ours is None: setattr(self, key, theirs) continue if isinstance(ours, list): ours += theirs elif isinstance(ours, set): ours |= theirs elif isinstance(ours, dict): result = merge_dict(ours, copy.deepcopy(theirs)) # type: ignore setattr(self, key, result) elif MetaBase in get_all_bases(field.type_): ours.merge(theirs) else: setattr(self, key, theirs) def __hash__(self): # type: ignore return hash(self.json()) class Config: allow_population_by_field_name = True json_encoders = {datetime: serialize_datetime, GradleSpecifier: str} class Versioned(MetaBase): @validator("format_version") def format_version_must_be_supported(cls, v: int): assert v <= META_FORMAT_VERSION return v format_version: int = Field(META_FORMAT_VERSION, alias="formatVersion") class MojangArtifactBase(MetaBase): sha1: Optional[str] size: Optional[int] url: str class MojangAssets(MojangArtifactBase): @validator("url") def validate_url(cls, v: str): return replace_old_launchermeta_url(v) id: str totalSize: int class MojangArtifact(MojangArtifactBase): path: Optional[str] class MojangLibraryExtractRules(MetaBase): """ "rules": [ { "action": "allow" }, { "action": "disallow", "os": { "name": "osx" } } ] """ exclude: List[str] # TODO maybe drop this completely? class MojangLibraryDownloads(MetaBase): artifact: Optional[MojangArtifact] classifiers: Optional[Dict[Any, MojangArtifact]] class OSRule(MetaBase): @validator("name") def name_must_be_os(cls, v: str): assert v in [ "osx", "linux", "windows", "windows-arm64", "osx-arm64", "linux-arm64", "linux-arm32", ] return v name: str version: Optional[str] class MojangRule(MetaBase): @validator("action") def action_must_be_allow_disallow(cls, v: str): assert v in ["allow", "disallow"] return v action: str os: Optional[OSRule] class MojangRules(MetaBase): __root__: List[MojangRule] def __iter__(self) -> Iterator[MojangRule]: # type: ignore return iter(self.__root__) def __getitem__(self, item: int) -> MojangRule: return self.__root__[item] class MojangLoggingArtifact(MojangArtifactBase): id: str class MojangLogging(MetaBase): @validator("type") def validate_type(cls, v): assert v in ["log4j2-xml"] return v file: MojangLoggingArtifact argument: str type: str class Library(MetaBase): extract: Optional[MojangLibraryExtractRules] name: Optional[GradleSpecifier] downloads: Optional[MojangLibraryDownloads] natives: Optional[Dict[str, str]] rules: Optional[MojangRules] url: Optional[str] absoluteUrl: Optional[str] = Field(None, alias="MMC-absoluteUrl") mmcHint: Optional[str] = Field(None, alias="MMC-hint") class Dependency(MetaBase): uid: str equals: Optional[str] suggests: Optional[str] class Agent(Library): argument: Optional[str] class MetaVersion(Versioned): name: str version: str uid: str type: Optional[str] order: Optional[int] volatile: Optional[bool] requires: Optional[List[Dependency]] conflicts: Optional[List[Dependency]] libraries: Optional[List[Library]] asset_index: Optional[MojangAssets] = Field(alias="assetIndex") maven_files: Optional[List[Library]] = Field(alias="mavenFiles") main_jar: Optional[Library] = Field(alias="mainJar") jar_mods: Optional[List[Library]] = Field(alias="jarMods") main_class: Optional[str] = Field(alias="mainClass") applet_class: Optional[str] = Field(alias="appletClass") minecraft_arguments: Optional[str] = Field(alias="minecraftArguments") release_time: Optional[datetime] = Field(alias="releaseTime") compatible_java_majors: Optional[List[int]] = Field(alias="compatibleJavaMajors") compatible_java_name: Optional[str] = Field(alias="compatibleJavaName") additional_traits: Optional[List[str]] = Field(alias="+traits") additional_tweakers: Optional[List[str]] = Field(alias="+tweakers") additional_jvm_args: Optional[List[str]] = Field(alias="+jvmArgs") additional_agents: Optional[List[Agent]] = Field(alias="+agents") logging: Optional[MojangLogging] class MetaPackage(Versioned): name: str uid: str recommended: Optional[List[str]] authors: Optional[List[str]] description: Optional[str] project_url: Optional[str] = Field(alias="projectUrl") def make_launcher_library( name: GradleSpecifier, hash: str, size: int, maven=LAUNCHER_MAVEN ): artifact = MojangArtifact(url=maven % name.path(), sha1=hash, size=size) return Library(name=name, downloads=MojangLibraryDownloads(artifact=artifact))