Code some ui and fix parse library will throw exception sometimes.

This commit is contained in:
wei
2026-07-23 00:47:44 +08:00
parent 77c22f5efe
commit 8d850408cd
11 changed files with 745 additions and 28 deletions
+16 -1
View File
@@ -19,16 +19,18 @@ THREADED_DOWNLOAD_MAX_WORKERS = 10
logger = logging.getLogger("Launcher.CoreLib")
class FileObject:
def __init__(self, path: Path, url=None, sha1=None, sha256=None, md5=None, size=None):
def __init__(self, path: Path, url=None, sha1=None, sha256=None, sha512=None, md5=None, size=None):
self.__path = path
self.expected_sha1 = sha1
self.expected_sha256 = sha256
self.expected_sha512 = sha512
self.expected_md5 = md5
self.expected_size = size
self.__sha1 = None
self.__sha256 = None
self.__sha512 = None
self.__md5 = None
self.url = url
@@ -73,6 +75,13 @@ class FileObject:
return self.__sha256
@property
def sha512(self):
if self.__sha512 is None:
self.__sha512 = self._calculate_hash("sha512")
return self.__sha512
@property
def md5(self):
if self.__md5 is None:
@@ -86,6 +95,9 @@ class FileObject:
def verify_sha256(self, expected_hash=None):
return self.sha256 == expected_hash if expected_hash else self.expected_sha256 == self.sha256
def verify_sha512(self, expected_hash=None):
return self.sha512 == expected_hash if expected_hash else self.expected_sha512 == self.sha512
def verify_md5(self, expected_hash=None):
return self.sha1 == expected_hash if expected_hash else self.expected_sha1 == self.sha1
@@ -96,6 +108,9 @@ class FileObject:
if algorithm == "sha256":
return self.verify_sha256(expected_hash if expected_hash else self.expected_sha256)
if algorithm == "sha512":
return self.verify_sha512(expected_hash if expected_hash else self.expected_sha512)
if algorithm == "md5":
return self.verify_md5(expected_hash if expected_hash else self.expected_md5)
+52 -1
View File
@@ -1,4 +1,5 @@
import logging
import re
import shlex
from core_lib.game.library import get_natives_platform_info
@@ -229,4 +230,54 @@ def generate_launch_command(arg_maps: ArgumentMappings, main_class: str, java_pa
cmd.append(main_class)
cmd.extend(parsed_game_args)
return cmd
return cmd
def is_a_argument(arg: str, startswith="--"):
pattern = re.compile(rf"{startswith}(.+)")
return pattern.match(arg) is not None
def generate_argument_details(arguments: list[str | dict], startswith="--") -> list[dict]:
items = []
for index, arg in enumerate(arguments):
if isinstance(arg, str) and is_a_argument(arg, startswith):
items.append({
"value": arg,
"type": "argument",
"dataType": "string",
})
if index > 0:
previous = items[index - 1]
if previous["type"] == "argument":
previous["isFlag"] = True
elif isinstance(arg, str):
items.append({
"value": arg,
"type": "value",
})
elif isinstance(arg, dict):
value = arg.get("value", None)
if not value:
raise ValueError("Dictionary argument must contain a value")
items.append({
"value": value,
"type": "argument",
"dataType": "dictionary",
"dictValue": arg,
})
return items
def is_same_argument(item: dict, another: dict) -> bool:
pass
+82
View File
@@ -4,6 +4,7 @@ import re
import zipfile
from pathlib import Path
from typing import Callable
from urllib.parse import urlsplit, unquote
from ..common.common import get_platform_info, FileObject, download_multiple_files, unzip
from .version_info import (
@@ -19,6 +20,87 @@ from ..common.exception import UnsupportedPlatformException, NativeResolveExcept
logger = logging.getLogger("Launcher.CoreLib")
def parse_maven_coordinate(value: str, default_extension="jar"):
value = value.strip()
coordinate, separator, extension = value.partition("@")
parts = coordinate.split(":")
if len(parts) not in (3, 4):
raise ValueError(f"Invalid maven coordinate {value}")
group, artifact, version = parts[:3]
classifier = parts[3] if len(parts) == 4 else None
if version.count("-") > 0:
version, extra = version.split("-", 1)
if not group or not artifact or not version:
raise ValueError(f"Coordinate key can not be empty: {value!r}")
if not extension:
extension = default_extension
return group, artifact, version, classifier, extension
def convert_maven_version(version: str, failback=None) -> tuple[int, int, int]:
try:
print(version)
items = version.split(".")
if len(items) >= 3:
major, minor, patch = items[0], items[1], items[2]
if len(items) > 3:
logger.debug(f"Found version tag: {".".join(items)}")
elif len(items) == 2:
major, minor, patch = 0, items[0], items[1]
else:
raise ValueError(f"Unable to parse version: {version!r}")
return int(major), int(minor), int(patch)
except ValueError:
return failback
def generate_filename_from_maven_coordinate(artifact: str, version, classifier="", extension="jar") -> str:
classifier = f"-{classifier}" if classifier else ""
return (
f"{artifact}-"
f"{version}"
f"{classifier}."
f"{extension}"
)
def generate_repo_path(group: str, artifact: str, version: str, filename) -> str:
group_path = group.replace(".", "/")
return (
f"{group_path}/"
f"{artifact}/"
f"{version}/"
f"{filename}"
)
def is_complete_maven_url(url: str, group: str, artifact: str, version: str, classifier: str="", extension: str=".jar") -> bool:
filename = generate_filename_from_maven_coordinate(artifact, version, classifier=classifier, extension=extension)
repository_path = generate_repo_path(group, artifact, version, filename)
parsed = urlsplit(url)
if parsed.scheme not in ("http", "https"):
return False
path = unquote(parsed.path).replace("\\", "/")
expected_path = "/" + repository_path
return path.endswith(expected_path)
def generate_repo_url(maven_url: str, group: str, artifact: str, version: str, classifier="", extension=".jar") -> str:
filename = generate_filename_from_maven_coordinate(artifact, version, classifier=classifier, extension=extension)
path = generate_repo_path(group, artifact, version, filename)
if not maven_url.endswith("/"):
maven_url += "/"
return f"{maven_url}{path}"
def is_library_allowed(rules, platform_rule_name, platform_arch_type, platform_version):
"""
+155
View File
@@ -0,0 +1,155 @@
import json
import logging
import requests
from core_lib.common.exception import VersionManifestFetchException, VersionDataKeyNotFoundException, \
VersionManifestException
logger = logging.getLogger("Launcher.CoreLib")
FABRIC_META_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v2/versions"
FABRIC_META_LOADER_VERSION_ENDPOINT_V2 = "https://meta.fabricmc.net/v1/versions/loader/{}/"
FABRIC_META_LOADER_MANIFEST_ENDPOINT_V2 = "https://meta.fabricmc.net//v2/versions/loader/{}/{}/profile/json"
FABRIC_MAVEN_URL = "https://maven.fabricmc.net/"
def fetch_fabric_version_list(version_manifest_url: str=FABRIC_META_VERSION_ENDPOINT_V2, timeout: int=5) -> list[dict]:
try:
r = requests.get(version_manifest_url, timeout=timeout)
except requests.exceptions.Timeout:
raise VersionManifestFetchException(
"Timeout while fetch from {}".format(version_manifest_url)
)
except requests.exceptions.ConnectionError as e:
raise VersionManifestFetchException(
"Connection error while fetch from {}: {}".format(version_manifest_url, e)
)
except requests.exceptions.HTTPError as e:
raise VersionManifestFetchException(
"HTTP error while fetch from {}: HTTP Status: {}".format(version_manifest_url, e.response.status_code)
)
except Exception as e:
raise VersionManifestFetchException(
"Unknown error while fetch from {}: {}".format(version_manifest_url, e)
)
try:
data = r.json()
except json.decoder.JSONDecodeError as e:
raise VersionManifestFetchException(
"JSON decode error while fetch from {}: {}".format(version_manifest_url, e)
)
versions = data.get("versions")
if not versions:
raise VersionManifestFetchException(
"Version this is empty or not set yet in mod version manifest"
)
return versions
def is_version_supported_and_stable(fabric_version_list: list[dict], target_version: str) -> tuple[bool, bool | None]:
if not fabric_version_list:
raise VersionManifestException(
"Provided fabric version list is empty."
)
for version in fabric_version_list:
if version["version"] == target_version:
is_stable = version.get("stable", None)
return True, is_stable
return False, None
def get_version_support_loader_list(target_version: str, loader_version_url: str=FABRIC_META_LOADER_VERSION_ENDPOINT_V2, timeout: int=5)\
-> list[dict]:
try:
r = requests.get(loader_version_url.format(target_version), timeout=timeout)
except requests.exceptions.Timeout:
raise VersionManifestFetchException(
"Timeout while fetch from {}".format(loader_version_url)
)
except requests.exceptions.ConnectionError as e:
raise VersionManifestFetchException(
"Connection error while fetch from {}: {}".format(loader_version_url, e)
)
except requests.exceptions.HTTPError as e:
raise VersionManifestFetchException(
"HTTP error while fetch from {}: HTTP Status: {}".format(loader_version_url, e.response.status_code)
)
except Exception as e:
raise VersionManifestFetchException(
"Unknown error while fetch from {}: {}".format(loader_version_url, e)
)
try:
versions = r.json()
except json.decoder.JSONDecodeError as e:
raise VersionManifestFetchException(
"JSON decode error while fetch from {}: {}".format(loader_version_url, e)
)
loaders = []
for version in versions:
loader = version.get("loader", None)
if not loader:
logger.warning("Found corrupted or unsupported loader manifest")
continue
loaders.append(loader)
if not loaders:
raise VersionManifestException(
"No available loader found in {}".format(loader_version_url)
)
return loaders
def get_loader_manifest_from_loader_data(target_version: str, loader_data: dict,
loader_manifest_url: str=FABRIC_META_LOADER_MANIFEST_ENDPOINT_V2) -> dict:
loader_ver = loader_data.get("version", None)
if not loader_ver:
raise VersionDataKeyNotFoundException(
"Version key not found in loader manifest {}".format(loader_manifest_url)
)
# Create url
url = loader_manifest_url.format(target_version, loader_ver)
logger.debug("Full manifest url: {}".format(url))
try:
r = requests.get(url, timeout=5)
except requests.exceptions.Timeout:
raise VersionManifestFetchException(
"Timeout while fetch from {}".format(url)
)
except requests.exceptions.ConnectionError as e:
raise VersionManifestFetchException(
"Connection error while fetch from {}: {}".format(url, e)
)
except requests.exceptions.HTTPError as e:
raise VersionManifestFetchException(
"HTTP error while fetch from {}: {}".format(url, e.response.status_code)
)
except Exception as e:
raise VersionManifestFetchException(
"Unknown error while fetch from {}: {}".format(url, e)
)
# We don't need verify data, fabric didn't provide manifest hash.
try:
return r.json()
except json.decoder.JSONDecodeError as e:
raise VersionManifestFetchException(
"JSON decode error while fetch from {}: {}".format(url, e)
)
except Exception as e:
raise VersionManifestFetchException(
"Unknown error while fetch from {}: {}".format(url, e)
)
+243
View File
@@ -0,0 +1,243 @@
import logging
from copy import deepcopy
import shlex
from core_lib.common.exception import VersionDataKeyNotFoundException, DependencyException
from core_lib.game.library import parse_maven_coordinate, convert_maven_version, generate_repo_url, \
is_complete_maven_url
from core_lib.game.version_info import MOJANG_LIBRARIES_ENDPOINT
logger = logging.getLogger("Launcher.CoreLib")
def find_useful_part_in_mod_version_manifest(mod_version_manifest: dict):
ver_id = mod_version_manifest.get("id", None)
inherits_from = mod_version_manifest.get("inheritsFrom", None)
ver_type = mod_version_manifest.get("type", None)
main_class = mod_version_manifest.get("mainClass", None)
arguments = mod_version_manifest.get("arguments", {})
old_arguments = mod_version_manifest.get("minecraftArguments", None)
libraries = mod_version_manifest.get("libraries", [])
if not ver_id:
raise VersionDataKeyNotFoundException(
"Provided mod loader version manifest does not contain key \"id\"."
)
if not ver_type:
raise VersionDataKeyNotFoundException(
"Provided mod loader version manifest does not contain key \"type\"."
)
if not inherits_from:
raise VersionDataKeyNotFoundException(
"Provided mod loader version manifest does not contain key \"inheritsFrom\"."
)
if not main_class:
raise VersionDataKeyNotFoundException(
"Provided mod loader version manifest does not contain key \"mainClass\"."
)
if not arguments and not old_arguments:
raise VersionDataKeyNotFoundException(
"Provided mod loader version manifest does not contain key \"arguments\" or \"minecraftArguments\"."
)
if not libraries:
raise VersionDataKeyNotFoundException(
"Provided mod loader version manifest does not contain key \"libraries\"."
)
return {
"id": ver_id,
"type": ver_type,
"inheritsFrom": inherits_from,
"minecraftArguments": old_arguments,
"libraries": libraries,
"arguments": arguments,
"mainClass": main_class,
}
def merge_necessary_dependencies(inherits_libraries: list, mod_libraries: list, mod_maven_url=None,
inherits_maven_url=MOJANG_LIBRARIES_ENDPOINT, default_extension="jar") -> list[dict]:
inherits = {}
result = []
# ensure below step won't modify original data
inherits_libraries = deepcopy(inherits_libraries)
mod_libraries = deepcopy(mod_libraries)
def check_lib(lib: dict):
url: str = lib.get("url", "")
source_from = lib["sourceFrom"]
lib_group=lib["parsed"]["group"]
lib_artifact=lib["parsed"]["artifact"]
lib_version=lib["parsed"]["version"]
lib_classifier=lib["parsed"]["classifier"]
lib_extension=lib["parsed"]["extension"]
recreated = False
if not url:
if source_from == "official":
new_url = generate_repo_url(inherits_maven_url, lib_group, lib_artifact, lib_version, lib_classifier,
lib_extension)
else:
new_url = generate_repo_url(mod_maven_url, lib_group, lib_artifact, lib_version, lib_classifier,
lib_extension)
lib["url"] = new_url
recreated = True
if not recreated:
url_result = is_complete_maven_url(url, lib_group, lib_artifact, lib_version, lib_classifier, lib_extension)
if not url_result:
# deal with some weird maven url that don't contain repo path
# Some mod loader (such as fabric), did not contain a full path inside the library. We will need to
# generate it from maven coordinate (aka lib_name)
target_maven = mod_maven_url or url
repo_url = generate_repo_url(target_maven, lib_group, lib_artifact, lib_version, lib_classifier,
lib_extension)
lib["url"] = repo_url
for library in inherits_libraries:
name = library.get("name", None)
if not name:
raise DependencyException(
"Provided inherited library does not contain key \"name\"."
)
group, artifact, version, classifier, extension = parse_maven_coordinate(name,
default_extension=default_extension)
# Save into dict for next step to check if library is existing
if not f"{group}:{artifact}" in inherits:
inherits[f"{group}:{artifact}"] = library
inherits[f"{group}:{artifact}"]["parsed"] = {
"group": group,
"artifact": artifact,
"version": version,
"classifier": classifier,
"extension": extension,
}
inherits[f"{group}:{artifact}"]["sourceFrom"] = "official"
else:
logger.warning("Duplicate library: {}".format(name))
for mod_library in mod_libraries:
name = mod_library.get("name", None)
group, artifact, version, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension)
ver_converted = convert_maven_version(version)
if not name:
raise DependencyException(
"Provided mod library does not contain key \"name\"."
)
if ver_converted is None:
raise DependencyException(
"Unable to convert inherits library version {} to tuple".format(version),
)
group, artifact, version, classifier, extension = parse_maven_coordinate(name, default_extension=default_extension)
mod_library["sourceFrom"] = "mod"
mod_library["parsed"] = {
"group": group,
"artifact": artifact,
"version": version,
"classifier": classifier,
"extension": extension,
}
if f"{group}:{artifact}" in inherits:
found_lib = inherits.pop(f"{group}:{artifact}")
found_lib_ver = convert_maven_version(found_lib["parsed"]["version"], None)
if found_lib_ver is None:
raise DependencyException(
"Unable to convert mod library version {} to tuple".format(found_lib["version"])
)
if found_lib_ver > ver_converted or found_lib_ver == ver_converted:
result.append(found_lib)
elif found_lib_ver < ver_converted:
result.append(mod_library)
else:
result.append(mod_library)
# Add the remaining dependencies back to result
for key in inherits:
value = inherits[key]
result.append(value)
for library in result:
check_lib(library)
return result
def merge_arguments(inherits_arguments: str | list, mod_arguments: str | list) -> list[str | dict]:
def str_to_list(args):
p = shlex.split(args)
return p
if isinstance(inherits_arguments, str):
inherits_arguments = str_to_list(inherits_arguments)
if isinstance(mod_arguments, str):
mod_arguments = str_to_list(mod_arguments)
result = []
result.extend(inherits_arguments)
result.extend(mod_arguments)
return result
def merge_game_and_jvm_args(inherits_args: dict[str, list[str]] | str, mod_args: dict[str, list[str]] | str) -> dict:
inherits_game = inherits_args.get("game", []) if isinstance(inherits_args, dict) else inherits_args
inherits_jvm = inherits_args.get("jvm", []) if isinstance(inherits_args, dict) else []
mod_game = mod_args.get("game", []) if isinstance(mod_args, dict) else mod_args
mod_jvm = mod_args.get("jvm", []) if isinstance(mod_args, dict) else []
merged = {
"game": merge_arguments(inherits_game, mod_game),
"jvm": merge_arguments(inherits_jvm, mod_jvm),
}
return merged
def merge_manifest(inherits_version_manifest: dict, mod_version_manifest: dict, mod_maven_url=None) -> dict:
merged_manifest = deepcopy(inherits_version_manifest)
# main class
main_class = mod_version_manifest.get("mainClass", None)
# Apply main class change
if main_class:
merged_manifest["mainClass"] = main_class
# Libraries
inherits_libraries = inherits_version_manifest.get("libraries", [])
mod_libraries = mod_version_manifest.get("libraries", [])
if not inherits_libraries or not mod_libraries:
raise VersionDataKeyNotFoundException(
"Inherited (or mod) version manifest does not contain key \"libraries\"."
)
merged_manifest["libraries"] = merge_necessary_dependencies(inherits_libraries, mod_libraries,
mod_maven_url=mod_maven_url)
# arguments
inherits_arguments = inherits_version_manifest.get("arguments", []) or inherits_version_manifest.get("minecraftArguments", "")
mod_arguments = mod_version_manifest.get("arguments", {}) or mod_version_manifest.get("minecraftArguments", {})
merged_manifest["arguments"] = merge_game_and_jvm_args(inherits_arguments, mod_arguments)
return merged_manifest
-1
View File
@@ -1,6 +1,5 @@
import json
import logging
import re
import warnings
from collections.abc import Callable
from pathlib import Path
+5
View File
@@ -0,0 +1,5 @@
class Launcher:
def __init__(self):
pass