Add jvm search and download support (thanks KiteeLauncher)
This commit is contained in:
@@ -25,9 +25,13 @@ import click
|
|||||||
import yaml
|
import yaml
|
||||||
from utils.common import get_latest_version_minecraft, get_specific_version_paper_builds, \
|
from utils.common import get_latest_version_minecraft, get_specific_version_paper_builds, \
|
||||||
download_server_jar, download_latest_build_paper_jar, get_latest_paper_version, download_vanilla_server_jar, \
|
download_server_jar, download_latest_build_paper_jar, get_latest_paper_version, download_vanilla_server_jar, \
|
||||||
read_server_properties, save_server_properties, activate_eula_file
|
read_server_properties, save_server_properties, activate_eula_file, \
|
||||||
|
get_specific_version_minecraft_require_java_version, find_system_java_executables
|
||||||
|
from utils.explorer_library import get_java_version_by_execute, search_available_java_runtimes_in_directory
|
||||||
from utils.file_settings import FileSettings
|
from utils.file_settings import FileSettings
|
||||||
from utils.file_settings import required_list, required_value
|
from utils.file_settings import required_list, required_value
|
||||||
|
from utils.java.java_info import create_java_version_info
|
||||||
|
from utils.java.jvm_installer import install_azul_build_version_jvm
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.x509.oid import NameOID
|
from cryptography.x509.oid import NameOID
|
||||||
from cryptography.hazmat.primitives import hashes
|
from cryptography.hazmat.primitives import hashes
|
||||||
@@ -40,6 +44,7 @@ VERSION = "1.1"
|
|||||||
LOG_DIR_NAME = "logs"
|
LOG_DIR_NAME = "logs"
|
||||||
SERVERJAR_LOG_FILE = "serverjar.log"
|
SERVERJAR_LOG_FILE = "serverjar.log"
|
||||||
LOG_DOWNLOAD_CHUNK_SIZE = 4096
|
LOG_DOWNLOAD_CHUNK_SIZE = 4096
|
||||||
|
JVM_RUNTIME_DIR = ROOT_DIR / "data" / "jvm"
|
||||||
|
|
||||||
def exit(message):
|
def exit(message):
|
||||||
click.echo(click.style(message, fg='green'))
|
click.echo(click.style(message, fg='green'))
|
||||||
@@ -173,6 +178,159 @@ def parse_server_property_overrides(values):
|
|||||||
return overrides
|
return overrides
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_machine_arch(machine=None):
|
||||||
|
machine = (machine or platform.machine()).lower()
|
||||||
|
if machine in ("amd64", "x86_64", "x64"):
|
||||||
|
return "amd64"
|
||||||
|
if machine in ("arm64", "aarch64"):
|
||||||
|
return "arm64"
|
||||||
|
if machine in ("x86", "i386", "i686"):
|
||||||
|
return "i686"
|
||||||
|
return machine
|
||||||
|
|
||||||
|
|
||||||
|
def java_executable_names():
|
||||||
|
return ["java.exe", "javaw.exe"] if os.name == "nt" else ["java"]
|
||||||
|
|
||||||
|
|
||||||
|
def java_runtime_search_roots():
|
||||||
|
roots = [JVM_RUNTIME_DIR]
|
||||||
|
java_home = os.environ.get("JAVA_HOME")
|
||||||
|
if java_home:
|
||||||
|
roots.append(Path(java_home))
|
||||||
|
return roots
|
||||||
|
|
||||||
|
|
||||||
|
def collect_java_executable_candidates():
|
||||||
|
candidates = []
|
||||||
|
|
||||||
|
for root in java_runtime_search_roots():
|
||||||
|
if root.exists():
|
||||||
|
candidates.extend(search_available_java_runtimes_in_directory(str(root)))
|
||||||
|
|
||||||
|
candidates.extend(find_system_java_executables(java_executable_names()))
|
||||||
|
|
||||||
|
for java_name in java_executable_names():
|
||||||
|
candidates.append(java_name)
|
||||||
|
|
||||||
|
unique_candidates = []
|
||||||
|
seen = set()
|
||||||
|
for candidate in candidates:
|
||||||
|
candidate_key = str(candidate).lower()
|
||||||
|
if candidate_key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(candidate_key)
|
||||||
|
unique_candidates.append(str(candidate))
|
||||||
|
|
||||||
|
return unique_candidates
|
||||||
|
|
||||||
|
|
||||||
|
def inspect_java_candidate(java_executable_path):
|
||||||
|
status, major_version, error = get_java_version_by_execute(java_executable_path)
|
||||||
|
if not status:
|
||||||
|
return None, error
|
||||||
|
|
||||||
|
try:
|
||||||
|
major_version = int(major_version)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None, f"Invalid Java major version: {major_version}"
|
||||||
|
|
||||||
|
return major_version, None
|
||||||
|
|
||||||
|
|
||||||
|
def find_compatible_java(required_major_version):
|
||||||
|
required_major_version = int(required_major_version)
|
||||||
|
compatible = []
|
||||||
|
checked = []
|
||||||
|
|
||||||
|
for candidate in collect_java_executable_candidates():
|
||||||
|
major_version, error = inspect_java_candidate(candidate)
|
||||||
|
if major_version is None:
|
||||||
|
checked.append((candidate, None, error))
|
||||||
|
continue
|
||||||
|
|
||||||
|
checked.append((candidate, major_version, None))
|
||||||
|
if major_version >= required_major_version:
|
||||||
|
compatible.append((candidate, major_version))
|
||||||
|
|
||||||
|
compatible.sort(key=lambda item: (item[1], item[0]))
|
||||||
|
if compatible:
|
||||||
|
return compatible[0][0], compatible[0][1], checked
|
||||||
|
|
||||||
|
return None, None, checked
|
||||||
|
|
||||||
|
|
||||||
|
def install_compatible_java(required_major_version):
|
||||||
|
arch = normalize_machine_arch()
|
||||||
|
platform_name = platform.system().lower()
|
||||||
|
install_dir = JVM_RUNTIME_DIR / f"java_{required_major_version}_{arch}"
|
||||||
|
|
||||||
|
click.echo(f"Installing Azul Java {required_major_version} into {install_dir} ...")
|
||||||
|
status, error = install_azul_build_version_jvm(
|
||||||
|
str(required_major_version),
|
||||||
|
str(install_dir),
|
||||||
|
arch,
|
||||||
|
platform_name,
|
||||||
|
)
|
||||||
|
if not status:
|
||||||
|
raise click.ClickException(f"Unable to install Azul Java {required_major_version}: {error}")
|
||||||
|
|
||||||
|
create_java_version_info(str(required_major_version), arch, str(install_dir))
|
||||||
|
java_executable = install_dir / "bin" / ("java.exe" if os.name == "nt" else "java")
|
||||||
|
if not java_executable.exists():
|
||||||
|
raise click.ClickException(f"Installed JVM does not contain a Java executable: {java_executable}")
|
||||||
|
|
||||||
|
return java_executable.absolute().as_posix()
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_java_executable(java_exec_path, minecraft_version, release, auto_install_java):
|
||||||
|
required_major_version = get_specific_version_minecraft_require_java_version(minecraft_version, release=release)
|
||||||
|
if required_major_version is None:
|
||||||
|
click.echo(click.style("Unable to determine required Java version. Using configured Java executable.", fg="yellow"))
|
||||||
|
return java_exec_path or "java"
|
||||||
|
|
||||||
|
click.echo(f"Minecraft {minecraft_version} requires Java {required_major_version}.")
|
||||||
|
|
||||||
|
if java_exec_path:
|
||||||
|
major_version, error = inspect_java_candidate(java_exec_path)
|
||||||
|
if major_version is None:
|
||||||
|
raise click.ClickException(f"Unable to use Java executable '{java_exec_path}': {error}")
|
||||||
|
if major_version < int(required_major_version):
|
||||||
|
raise click.ClickException(
|
||||||
|
f"Java executable '{java_exec_path}' is Java {major_version}, "
|
||||||
|
f"but Minecraft {minecraft_version} requires Java {required_major_version}."
|
||||||
|
)
|
||||||
|
click.echo(f"Using configured Java executable: {java_exec_path} (Java {major_version})")
|
||||||
|
return java_exec_path
|
||||||
|
|
||||||
|
java_path, major_version, checked = find_compatible_java(required_major_version)
|
||||||
|
if java_path:
|
||||||
|
click.echo(f"Using compatible Java executable: {java_path} (Java {major_version})")
|
||||||
|
return java_path
|
||||||
|
|
||||||
|
click.echo(click.style(f"No compatible Java runtime found for Java {required_major_version}.", fg="yellow"))
|
||||||
|
if checked:
|
||||||
|
click.echo("Checked Java candidates:")
|
||||||
|
for candidate, candidate_major, error in checked:
|
||||||
|
if candidate_major is None:
|
||||||
|
click.echo(f"- {candidate}: unavailable ({error})")
|
||||||
|
else:
|
||||||
|
click.echo(f"- {candidate}: Java {candidate_major}")
|
||||||
|
|
||||||
|
should_install = auto_install_java
|
||||||
|
if not should_install:
|
||||||
|
result = input("Install a compatible Azul JVM now? [y/N] ")
|
||||||
|
should_install = result.strip().lower() == "y"
|
||||||
|
|
||||||
|
if should_install:
|
||||||
|
return install_compatible_java(required_major_version)
|
||||||
|
|
||||||
|
raise click.ClickException(
|
||||||
|
f"No compatible Java runtime is available. Install Java {required_major_version}+ "
|
||||||
|
"or rerun with --install-java."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@click.option("--name", "-d", default="Unnamed Server", show_default=True, help="Server name")
|
@click.option("--name", "-d", default="Unnamed Server", show_default=True, help="Server name")
|
||||||
@click.option("--mc-version", "-m",
|
@click.option("--mc-version", "-m",
|
||||||
@@ -196,7 +354,10 @@ def parse_server_property_overrides(values):
|
|||||||
@click.option("--custom-args", "-ce",
|
@click.option("--custom-args", "-ce",
|
||||||
help="Custom arguments (command)", type=str, default="")
|
help="Custom arguments (command)", type=str, default="")
|
||||||
@click.option("--java-exec-path", "-p", show_default=True,
|
@click.option("--java-exec-path", "-p", show_default=True,
|
||||||
help="The destination of the java executable", default="java")
|
help="The destination of the java executable. If omitted, ServerJar searches for a compatible Java.",
|
||||||
|
default=None)
|
||||||
|
@click.option("--install-java", is_flag=True,
|
||||||
|
help="Install a compatible Azul JVM automatically when no local compatible Java is found.")
|
||||||
@click.option("--x-memory-initial", "-xms", show_default=True,
|
@click.option("--x-memory-initial", "-xms", show_default=True,
|
||||||
help="Initial allocation size of the memory for server",
|
help="Initial allocation size of the memory for server",
|
||||||
type=str, default="1G")
|
type=str, default="1G")
|
||||||
@@ -213,7 +374,8 @@ def parse_server_property_overrides(values):
|
|||||||
@click.option("--server-property", "-sp", "server_properties", multiple=True,
|
@click.option("--server-property", "-sp", "server_properties", multiple=True,
|
||||||
help="server.properties override written after the first startup. Use key=value; can be repeated.")
|
help="server.properties override written after the first startup. Use key=value; can be repeated.")
|
||||||
def create_server(name, mc_version, build, server_type, snapshot, latest, list_builds, filename, extra_args, java_exec_path,
|
def create_server(name, mc_version, build, server_type, snapshot, latest, list_builds, filename, extra_args, java_exec_path,
|
||||||
x_memory_initial, x_memory_maximum, nogui, custom_args, server_port, server_host, server_properties):
|
install_java, x_memory_initial, x_memory_maximum, nogui, custom_args, server_port, server_host,
|
||||||
|
server_properties):
|
||||||
server_dir = Path("servers", name)
|
server_dir = Path("servers", name)
|
||||||
server_type = server_type.lower()
|
server_type = server_type.lower()
|
||||||
property_overrides = parse_server_property_overrides(server_properties)
|
property_overrides = parse_server_property_overrides(server_properties)
|
||||||
@@ -297,8 +459,12 @@ def create_server(name, mc_version, build, server_type, snapshot, latest, list_b
|
|||||||
exit("User aborted.")
|
exit("User aborted.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
selected_mc_version = latest_ver if latest_ver is not None else mc_version
|
||||||
|
if not custom_args:
|
||||||
|
java_exec_path = resolve_java_executable(java_exec_path, selected_mc_version, release, install_java)
|
||||||
|
|
||||||
args = [
|
args = [
|
||||||
java_exec_path,
|
java_exec_path or "java",
|
||||||
"-Xms{}".format(x_memory_initial),
|
"-Xms{}".format(x_memory_initial),
|
||||||
"-Xmx{}".format(x_memory_maximum),
|
"-Xmx{}".format(x_memory_maximum),
|
||||||
"-jar",
|
"-jar",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""
|
||||||
|
bk_core
|
||||||
|
|
||||||
|
Copyright (c) 2024~2025 Techarerm/TedKai
|
||||||
|
Copyright (c) 2026 Kitee Contributors. All rights reserved.
|
||||||
|
"""
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
|
||||||
|
def verify_checksum(file_path, expected_sha1):
|
||||||
|
sha1 = hashlib.sha1()
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
while True:
|
||||||
|
data = f.read(65536) # Read in 64KB chunks
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
sha1.update(data)
|
||||||
|
file_sha1 = sha1.hexdigest()
|
||||||
|
return file_sha1 == expected_sha1
|
||||||
|
|
||||||
|
|
||||||
|
def verify_checksum_v2(file_path, expected_hash, crypto_type):
|
||||||
|
hash_dict = {
|
||||||
|
'sha1': hashlib.sha1,
|
||||||
|
'sha256': hashlib.sha256,
|
||||||
|
'md5': hashlib.md5
|
||||||
|
}
|
||||||
|
|
||||||
|
if crypto_type not in hash_dict:
|
||||||
|
raise ValueError(f"Unsupported hash type: {crypto_type}")
|
||||||
|
|
||||||
|
hash_obj = hash_dict[crypto_type]()
|
||||||
|
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
for chunk in iter(lambda: f.read(65536), b""):
|
||||||
|
hash_obj.update(chunk)
|
||||||
|
|
||||||
|
file_sha1 = hash_obj.hexdigest()
|
||||||
|
return file_sha1 == expected_hash
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
"""
|
||||||
|
bk_core
|
||||||
|
|
||||||
|
Copyright (c) 2024~2025 Techarerm/TedKai
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def deduplicate_java_classpath(classpath, classpath_separator):
|
||||||
|
paths = classpath.split(classpath_separator)
|
||||||
|
|
||||||
|
cleaned_classpath = [str(Path(p.strip())) for p in paths if p.strip()]
|
||||||
|
|
||||||
|
return classpath_separator.join(cleaned_classpath)
|
||||||
|
|
||||||
|
|
||||||
|
def delete_missing_java_classpath(classpath, classpath_separator):
|
||||||
|
paths = classpath.split(classpath_separator)
|
||||||
|
existing_classpath = [p for p in paths if os.path.exists(p)]
|
||||||
|
return classpath_separator.join(existing_classpath)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_specified_value_to_target_string_in_java_classpath(classpath, classpath_separator, value, string):
|
||||||
|
paths = classpath.split(classpath_separator)
|
||||||
|
|
||||||
|
new_paths = [path.replace(value, string) for path in paths]
|
||||||
|
|
||||||
|
return classpath_separator.join(new_paths)
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
"""
|
||||||
|
bk_core
|
||||||
|
|
||||||
|
Copyright (c) 2024~2025 Techarerm/TedKai
|
||||||
|
Copyright (c) 2026 Kitee Contributors. All rights reserved.
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def validation_rule(
|
||||||
|
default=None,
|
||||||
|
*,
|
||||||
|
children=None,
|
||||||
|
write_back_if_not_exist=False,
|
||||||
|
recover_missing_items=False,
|
||||||
|
):
|
||||||
|
rule = {
|
||||||
|
"writeBackIfNotExist": write_back_if_not_exist,
|
||||||
|
"recoverMissingItems": recover_missing_items,
|
||||||
|
}
|
||||||
|
|
||||||
|
if children is not None:
|
||||||
|
rule["children"] = children
|
||||||
|
else:
|
||||||
|
rule["default"] = default
|
||||||
|
|
||||||
|
return rule
|
||||||
|
|
||||||
|
|
||||||
|
def required_value(default, *, recover_missing_items=False):
|
||||||
|
return validation_rule(
|
||||||
|
default,
|
||||||
|
write_back_if_not_exist=True,
|
||||||
|
recover_missing_items=recover_missing_items,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def required_section(children):
|
||||||
|
return validation_rule(children=children, write_back_if_not_exist=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FileSettings:
|
||||||
|
"""
|
||||||
|
Simple Settings Object
|
||||||
|
IMPORTANT: Settings always use self.data as the main operate data, NOT FROM disk settings file!!!
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
FileSettings.create() -> Create settings file (path is self.path)
|
||||||
|
FileSettings.load() - > Load settings file
|
||||||
|
FileSettings.save() -> Save settings file
|
||||||
|
FileSettings.edit() -> Edit settings file (Auto save when with block completes)
|
||||||
|
|
||||||
|
Example:
|
||||||
|
with FileSettings.edit() as settings:
|
||||||
|
settings["hello"] = "world"
|
||||||
|
"""
|
||||||
|
def __init__(self, path, default, validation_rules=None,
|
||||||
|
dumps_func=json.dumps, load_func=json.load,
|
||||||
|
settings_change_callback=None):
|
||||||
|
self.data = copy.deepcopy(default)
|
||||||
|
self.default = copy.deepcopy(default)
|
||||||
|
self.path: Path = Path(path)
|
||||||
|
self.validation_rules = validation_rules
|
||||||
|
self.dumps_func = dumps_func
|
||||||
|
self.load_func = load_func
|
||||||
|
self.settings_change_callback = settings_change_callback
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Replace self.data with self.default and save"""
|
||||||
|
self.data = copy.deepcopy(self.default)
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
if callable(self.settings_change_callback):
|
||||||
|
self.settings_change_callback(self.data)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<FileSettings At {self.path.as_posix()}>"
|
||||||
|
|
||||||
|
def create(self, exist_ok=False):
|
||||||
|
"""
|
||||||
|
Create settings file (path is self.path, data use default value)
|
||||||
|
:param exist_ok: If not True, raise exception if file already exists
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not exist_ok and self.path.exists():
|
||||||
|
raise FileExistsError(f'{self.path} already exists')
|
||||||
|
|
||||||
|
self.path.write_text(self._dumps(self.default))
|
||||||
|
|
||||||
|
def read_from_exist(self):
|
||||||
|
self.data = self.load()
|
||||||
|
|
||||||
|
def mload(self):
|
||||||
|
"""Get data from memory"""
|
||||||
|
return copy.deepcopy(self.data)
|
||||||
|
|
||||||
|
def load(self):
|
||||||
|
"""
|
||||||
|
Load settings file data into self.data (path is self.path)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not self.path.exists():
|
||||||
|
raise FileNotFoundError(f'{self.path} does not exist')
|
||||||
|
|
||||||
|
with self.path.open("rb") as settings_file:
|
||||||
|
data = self.load_func(settings_file)
|
||||||
|
|
||||||
|
if isinstance(self.validation_rules, dict):
|
||||||
|
data = self.validate_data(data, self.default, self.validation_rules)
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
"""
|
||||||
|
Save self.data (data in memory) into settings file
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not self.path.exists():
|
||||||
|
raise FileNotFoundError(f'{self.path} does not exist. Create it before saving.')
|
||||||
|
|
||||||
|
self.path.write_text(self._dumps(self.data))
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return self.data.get(key, default)
|
||||||
|
|
||||||
|
def dget(self, key, default=None): # dget -> get_default
|
||||||
|
return self.default.get(key, default)
|
||||||
|
|
||||||
|
def exists(self):
|
||||||
|
return self.path.exists()
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def edit(self):
|
||||||
|
"""
|
||||||
|
Edit settings (With auto save)
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not self.exists():
|
||||||
|
self.create()
|
||||||
|
yield self
|
||||||
|
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
if callable(self.settings_change_callback):
|
||||||
|
self.settings_change_callback(self)
|
||||||
|
|
||||||
|
def validate_data(self, data, default, rules):
|
||||||
|
"""
|
||||||
|
Validates data by validating rules.
|
||||||
|
:param data: dict data
|
||||||
|
:param default: default sample
|
||||||
|
:param rules: rules of all keys
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
data = {}
|
||||||
|
|
||||||
|
if not isinstance(default, dict):
|
||||||
|
default = {}
|
||||||
|
|
||||||
|
if not isinstance(rules, dict):
|
||||||
|
return copy.deepcopy(data)
|
||||||
|
|
||||||
|
validated = copy.deepcopy(data)
|
||||||
|
|
||||||
|
for key, rule in rules.items():
|
||||||
|
rule_default, options = self._parse_validation_rule(rule)
|
||||||
|
default_value = copy.deepcopy(default.get(key, rule_default))
|
||||||
|
|
||||||
|
if key not in validated:
|
||||||
|
if options.get("writeBackIfNotExist"):
|
||||||
|
validated[key] = self._validate_value(
|
||||||
|
default_value,
|
||||||
|
default_value,
|
||||||
|
rule_default,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
validated[key] = self._validate_value(
|
||||||
|
validated[key],
|
||||||
|
default_value,
|
||||||
|
rule_default,
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
|
||||||
|
return validated
|
||||||
|
|
||||||
|
def update(self, settings):
|
||||||
|
"""Update self.data with new settings values."""
|
||||||
|
if not isinstance(settings, dict):
|
||||||
|
raise TypeError("settings must be a dict")
|
||||||
|
|
||||||
|
def update_inner(new, old):
|
||||||
|
if not isinstance(old, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
for n_k, n_v in new.items():
|
||||||
|
if isinstance(n_v, dict) and (n_k in old and isinstance(old[n_k], dict)):
|
||||||
|
update_inner(n_v, old[n_k])
|
||||||
|
else:
|
||||||
|
old[n_k] = copy.deepcopy(n_v)
|
||||||
|
|
||||||
|
update_inner(settings, self.data)
|
||||||
|
|
||||||
|
if callable(self.settings_change_callback):
|
||||||
|
self.settings_change_callback(self)
|
||||||
|
|
||||||
|
def update_new_settings(self, new_default_settings):
|
||||||
|
"""Add missing settings to self.default and self.data."""
|
||||||
|
if not isinstance(new_default_settings, dict):
|
||||||
|
raise TypeError("new_default_settings must be a dict")
|
||||||
|
|
||||||
|
def add_missing(new, old):
|
||||||
|
if not isinstance(old, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
for n_k, n_v in new.items():
|
||||||
|
if isinstance(n_v, dict) and (n_k in old and isinstance(old[n_k], dict)):
|
||||||
|
add_missing(n_v, old[n_k])
|
||||||
|
if n_k not in old:
|
||||||
|
old[n_k] = copy.deepcopy(n_v)
|
||||||
|
|
||||||
|
add_missing(new_default_settings, self.default)
|
||||||
|
add_missing(new_default_settings, self.data)
|
||||||
|
|
||||||
|
if callable(self.settings_change_callback):
|
||||||
|
self.settings_change_callback(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _parse_validation_rule(rule):
|
||||||
|
if isinstance(rule, dict) and ("default" in rule or "children" in rule):
|
||||||
|
options = {
|
||||||
|
"writeBackIfNotExist": rule.get("writeBackIfNotExist", False),
|
||||||
|
"recoverMissingItems": rule.get("recoverMissingItems", False), # Recover missing item (only for list)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "children" in rule:
|
||||||
|
return rule["children"], options
|
||||||
|
|
||||||
|
return rule.get("default"), options
|
||||||
|
|
||||||
|
if (
|
||||||
|
isinstance(rule, (list, tuple))
|
||||||
|
and len(rule) == 2
|
||||||
|
and isinstance(rule[1], dict)
|
||||||
|
):
|
||||||
|
return rule[0], rule[1]
|
||||||
|
|
||||||
|
return rule, {}
|
||||||
|
|
||||||
|
def _validate_value(self, value, default_value, rule_default, options):
|
||||||
|
if isinstance(rule_default, dict):
|
||||||
|
if not isinstance(value, dict):
|
||||||
|
value = {}
|
||||||
|
|
||||||
|
if not isinstance(default_value, dict):
|
||||||
|
default_value = {}
|
||||||
|
|
||||||
|
return self.validate_data(value, default_value, rule_default)
|
||||||
|
|
||||||
|
if isinstance(default_value, list):
|
||||||
|
if not isinstance(value, list):
|
||||||
|
return copy.deepcopy(default_value)
|
||||||
|
|
||||||
|
validated = copy.deepcopy(value)
|
||||||
|
|
||||||
|
if options.get("recoverMissingItems"):
|
||||||
|
for item in default_value:
|
||||||
|
if item not in validated:
|
||||||
|
validated.append(copy.deepcopy(item))
|
||||||
|
|
||||||
|
return validated
|
||||||
|
|
||||||
|
if default_value is None:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if type(value) is not type(default_value):
|
||||||
|
return copy.deepcopy(default_value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _dumps(self, data):
|
||||||
|
if not callable(self.dumps_func):
|
||||||
|
raise TypeError(f"dumps_func must be callable")
|
||||||
|
|
||||||
|
return self.dumps_func(data)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.data[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
self.data[key] = value
|
||||||
|
|
||||||
|
if callable(self.settings_change_callback):
|
||||||
|
self.settings_change_callback(self)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, FileSettings):
|
||||||
|
return self.path == other.path and self.data == other.data
|
||||||
|
|
||||||
|
if isinstance(other, dict):
|
||||||
|
return self.data == other
|
||||||
|
|
||||||
|
return NotImplemented
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
"""
|
||||||
|
bk_core
|
||||||
|
|
||||||
|
Copyright (c) 2024~2025 Techarerm/TedKai
|
||||||
|
Copyright (c) 2026 Kitee Contributors. All rights reserved.
|
||||||
|
"""
|
||||||
|
import zipfile
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def create_download_task(
|
||||||
|
url,
|
||||||
|
dest_path,
|
||||||
|
sha1=None,
|
||||||
|
with_verify=True,
|
||||||
|
crypto_type="sha1",
|
||||||
|
chunk_size=8192,
|
||||||
|
):
|
||||||
|
return {
|
||||||
|
"url": url,
|
||||||
|
"dest": dest_path,
|
||||||
|
"sha1": sha1,
|
||||||
|
"with_verify": with_verify,
|
||||||
|
"crypto_type": crypto_type,
|
||||||
|
"chunk_size": chunk_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def flatten_download_queue(nested_urls_and_paths):
|
||||||
|
return [
|
||||||
|
create_download_task(url, dest_path)
|
||||||
|
for sublist in nested_urls_and_paths
|
||||||
|
for url, dest_path in sublist
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def download_file(url, dest_path, with_verify=True, sha1=None, no_output=False, custom_chunk_size=8192):
|
||||||
|
"""
|
||||||
|
Downloads a file from a URL and saves it to dest_path.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
create_download_task(
|
||||||
|
url,
|
||||||
|
dest_path,
|
||||||
|
sha1=sha1,
|
||||||
|
with_verify=with_verify,
|
||||||
|
chunk_size=custom_chunk_size,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
def n_download_file(url, dest_path, enable_hash_check=False, sha1=None, no_download_output=False, chunk_size=8192):
|
||||||
|
"""
|
||||||
|
Downloads a file from a URL and saves it to dest_path.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
create_download_task(
|
||||||
|
url,
|
||||||
|
dest_path,
|
||||||
|
sha1=sha1,
|
||||||
|
with_verify=enable_hash_check,
|
||||||
|
chunk_size=chunk_size,
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_zip(zip_path, extract_to):
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||||
|
zip_ref.extractall(extract_to)
|
||||||
|
print(f"Extracted {zip_path} to {extract_to}")
|
||||||
|
except zipfile.BadZipFile as e:
|
||||||
|
print(f"[ERR] Error extracting {zip_path}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def multi_thread_download(nested_urls_and_paths, name, max_workers=5, retries=1, download_with_progress_bar=True):
|
||||||
|
"""
|
||||||
|
Downloads multiple files using multiple threads with retry attempts.
|
||||||
|
nested_urls_and_paths should be a nested list where each element is a list containing a tuple of (url, dest_path).
|
||||||
|
"""
|
||||||
|
return flatten_download_queue(nested_urls_and_paths)
|
||||||
|
|
||||||
|
|
||||||
|
def multithread_download(
|
||||||
|
download_url_list,
|
||||||
|
file_dest_list,
|
||||||
|
progress_name,
|
||||||
|
max_workers=8,
|
||||||
|
with_verify_checksum=False,
|
||||||
|
file_hash_list=None,
|
||||||
|
download_with_progress_bar=False,
|
||||||
|
no_output=None,
|
||||||
|
crypto_type="sha1",
|
||||||
|
):
|
||||||
|
# parameters
|
||||||
|
if file_hash_list is None:
|
||||||
|
file_hash_list = []
|
||||||
|
if no_output is None:
|
||||||
|
no_output = download_with_progress_bar
|
||||||
|
|
||||||
|
if not with_verify_checksum:
|
||||||
|
file_hash_list = [None for _ in download_url_list]
|
||||||
|
|
||||||
|
return [
|
||||||
|
create_download_task(
|
||||||
|
file_url,
|
||||||
|
file_dest,
|
||||||
|
sha1=file_hash,
|
||||||
|
with_verify=with_verify_checksum,
|
||||||
|
crypto_type=crypto_type,
|
||||||
|
)
|
||||||
|
for file_url, file_dest, file_hash in zip(download_url_list, file_dest_list, file_hash_list)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def find_jar_file_main_class(jar_file_path):
|
||||||
|
manifest_path = 'META-INF/MANIFEST.MF'
|
||||||
|
try:
|
||||||
|
with zipfile.ZipFile(jar_file_path, 'r') as jar:
|
||||||
|
if not manifest_path in jar.namelist():
|
||||||
|
return None
|
||||||
|
|
||||||
|
manifest = jar.read(manifest_path).decode('utf-8')
|
||||||
|
for line in manifest.splitlines():
|
||||||
|
if line.startswith('Main-Class:'):
|
||||||
|
# Return the class name specified in the Main-Class entry
|
||||||
|
return line.split(':')[1].strip()
|
||||||
|
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_url_status(url):
|
||||||
|
try:
|
||||||
|
# Send a HEAD request to save bandwidth
|
||||||
|
response = requests.head(url, allow_redirects=True, timeout=5)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return True
|
||||||
|
elif response.status_code == 404:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def pause():
|
||||||
|
command = None
|
||||||
|
if os.name == "posix":
|
||||||
|
command = 'read -p "Press enter to continue..."'
|
||||||
|
elif os.name == "nt":
|
||||||
|
command = "pause"
|
||||||
|
|
||||||
|
if command is not None: os.system(command)
|
||||||
+76
-3
@@ -8,6 +8,7 @@ PAPER_VERSION_API = "https://api.papermc.io/v2/projects/paper/versions/{}"
|
|||||||
PAPER_SERVER_JAR_API = "https://api.papermc.io/v2/projects/paper/versions/{}/builds/{}/downloads/paper-{}-{}.jar"
|
PAPER_SERVER_JAR_API = "https://api.papermc.io/v2/projects/paper/versions/{}/builds/{}/downloads/paper-{}-{}.jar"
|
||||||
MOJANG_VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
|
MOJANG_VERSION_MANIFEST_V2 = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
|
||||||
|
|
||||||
|
|
||||||
def read_server_properties(server_dir: Path) -> Properties:
|
def read_server_properties(server_dir: Path) -> Properties:
|
||||||
if not server_dir.exists():
|
if not server_dir.exists():
|
||||||
raise FileNotFoundError("Server directory does not exist")
|
raise FileNotFoundError("Server directory does not exist")
|
||||||
@@ -25,6 +26,7 @@ def save_server_properties(properties: Properties, server_dir: Path) -> None:
|
|||||||
with path.open("wb") as config_file:
|
with path.open("wb") as config_file:
|
||||||
properties.store(config_file, encoding='utf-8')
|
properties.store(config_file, encoding='utf-8')
|
||||||
|
|
||||||
|
|
||||||
def activate_eula_file(server_dir: Path) -> bool:
|
def activate_eula_file(server_dir: Path) -> bool:
|
||||||
path = server_dir / 'eula.txt'
|
path = server_dir / 'eula.txt'
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ def activate_eula_file(server_dir: Path) -> bool:
|
|||||||
lines = file.readlines()
|
lines = file.readlines()
|
||||||
|
|
||||||
for line in lines:
|
for line in lines:
|
||||||
print(f"{line}\n")
|
print(f"{line}")
|
||||||
|
|
||||||
print("To activate eula file please read above text. If you agree this license, enter Y [not agree enter N]")
|
print("To activate eula file please read above text. If you agree this license, enter Y [not agree enter N]")
|
||||||
result = str(input("Agree? :"))
|
result = str(input("Agree? :"))
|
||||||
@@ -50,9 +52,9 @@ def activate_eula_file(server_dir: Path) -> bool:
|
|||||||
continue
|
continue
|
||||||
file.write(line)
|
file.write(line)
|
||||||
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def jar_filename(filename: str | None, default: str) -> str:
|
def jar_filename(filename: str | None, default: str) -> str:
|
||||||
if filename:
|
if filename:
|
||||||
name = filename
|
name = filename
|
||||||
@@ -179,6 +181,25 @@ def get_latest_version_minecraft(release=True):
|
|||||||
return ver
|
return ver
|
||||||
|
|
||||||
|
|
||||||
|
def get_specific_version_minecraft_require_java_version(minecraft_version, release=True):
|
||||||
|
version_list = get_version_list(release=release)
|
||||||
|
|
||||||
|
if release:
|
||||||
|
version_exists = minecraft_version in version_list
|
||||||
|
else:
|
||||||
|
version_exists = any(version.get("id") == minecraft_version for version in version_list)
|
||||||
|
|
||||||
|
if not version_exists:
|
||||||
|
raise Exception("Specified Minecraft version does not exist.")
|
||||||
|
|
||||||
|
metadata = get_minecraft_version_metadata(minecraft_version)
|
||||||
|
|
||||||
|
if not metadata:
|
||||||
|
raise Exception("Unable to get Minecraft version metadata for {}.\n".format(minecraft_version))
|
||||||
|
|
||||||
|
return metadata.get("javaVersion", {}).get("majorVersion", None)
|
||||||
|
|
||||||
|
|
||||||
def download_server_jar(minecraft_version: str, build_version: str, destination: Path, filename: str | None = None):
|
def download_server_jar(minecraft_version: str, build_version: str, destination: Path, filename: str | None = None):
|
||||||
"""
|
"""
|
||||||
Download server jar (paper server only)
|
Download server jar (paper server only)
|
||||||
@@ -194,7 +215,8 @@ def download_server_jar(minecraft_version: str, build_version: str, destination:
|
|||||||
download_file(url, destination)
|
download_file(url, destination)
|
||||||
return destination
|
return destination
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception("Unable to download server jar for version {}\nURL: {}\nError: {}".format(minecraft_version, url, e))
|
raise Exception(
|
||||||
|
"Unable to download server jar for version {}\nURL: {}\nError: {}".format(minecraft_version, url, e))
|
||||||
|
|
||||||
|
|
||||||
def get_latest_build_of_version(minecraft_version: str) -> str:
|
def get_latest_build_of_version(minecraft_version: str) -> str:
|
||||||
@@ -209,6 +231,7 @@ def download_latest_build_paper_jar(minecraft_version: str, destination_dir: Pat
|
|||||||
build = get_latest_build_of_version(minecraft_version)
|
build = get_latest_build_of_version(minecraft_version)
|
||||||
return download_server_jar(minecraft_version, build, destination_dir, filename=filename)
|
return download_server_jar(minecraft_version, build, destination_dir, filename=filename)
|
||||||
|
|
||||||
|
|
||||||
def version_exist_from_paper(minecraft_version: str) -> bool:
|
def version_exist_from_paper(minecraft_version: str) -> bool:
|
||||||
try:
|
try:
|
||||||
get_specific_version_paper_builds(minecraft_version)
|
get_specific_version_paper_builds(minecraft_version)
|
||||||
@@ -273,3 +296,53 @@ def download_vanilla_server_jar(minecraft_version: str, destination: Path, filen
|
|||||||
url,
|
url,
|
||||||
e,
|
e,
|
||||||
))
|
))
|
||||||
|
|
||||||
|
def find_system_java_executables(java_names):
|
||||||
|
"""
|
||||||
|
Find system Java executables
|
||||||
|
:param java_names:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
roots = []
|
||||||
|
if os.name == "nt":
|
||||||
|
for env_name in ("ProgramFiles", "ProgramFiles(x86)"):
|
||||||
|
root = os.environ.get(env_name)
|
||||||
|
if root:
|
||||||
|
roots.append(os.path.join(root, "Java"))
|
||||||
|
elif os.name == "posix":
|
||||||
|
roots.extend(("/Library/Java/JavaVirtualMachines", "/opt/java", "/usr/lib/jvm", "/usr/local/java"))
|
||||||
|
|
||||||
|
found = []
|
||||||
|
for root in roots:
|
||||||
|
if not os.path.isdir(root):
|
||||||
|
continue
|
||||||
|
|
||||||
|
for current_root, _, files in os.walk(root):
|
||||||
|
for java_name in java_names:
|
||||||
|
if java_name in files and os.path.basename(current_root).lower() == "bin":
|
||||||
|
found.append(os.path.join(current_root, java_name))
|
||||||
|
break
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
|
def major_version_from_runtime_dir(runtime_dir):
|
||||||
|
"""
|
||||||
|
Get major version from target runtime directory
|
||||||
|
(Only works if this runtime is created by launcher)
|
||||||
|
:param runtime_dir:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
name = runtime_dir.name
|
||||||
|
if name.lower().startswith("java_"):
|
||||||
|
return name.split("_", 1)[1]
|
||||||
|
|
||||||
|
info_path = runtime_dir / "java.version.info"
|
||||||
|
if info_path.exists():
|
||||||
|
try:
|
||||||
|
for line in info_path.read_text(encoding="utf-8").splitlines():
|
||||||
|
if line.strip().startswith("JavaMajorVersion") and "=" in line:
|
||||||
|
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return ""
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
"""
|
||||||
|
bk_core
|
||||||
|
|
||||||
|
Copyright (c) 2024~2025 Techarerm/TedKai
|
||||||
|
Copyright (c) 2026 Kitee Contributors. All rights reserved.
|
||||||
|
"""
|
||||||
|
import os.path
|
||||||
|
import textwrap
|
||||||
|
import requests
|
||||||
|
|
||||||
|
azul_packages_api = "https://api.azul.com/metadata/v1/zulu/packages"
|
||||||
|
java_manifest_url = 'https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json'
|
||||||
|
|
||||||
|
|
||||||
|
def get_java_build_download_url_from_azul(platform_name: str, full_arch: str, java_major_version: str):
|
||||||
|
full_arch = full_arch.lower()
|
||||||
|
platform_name = platform_name.lower()
|
||||||
|
|
||||||
|
arch_map = {
|
||||||
|
"arm64": "aarch64",
|
||||||
|
"i686": "i686",
|
||||||
|
"amd64": "amd64",
|
||||||
|
"i386": "i686"
|
||||||
|
}
|
||||||
|
|
||||||
|
platform_map = {
|
||||||
|
"macos": "macos",
|
||||||
|
"darwin": "macos",
|
||||||
|
"windows": "windows",
|
||||||
|
"linux": "linux"
|
||||||
|
}
|
||||||
|
|
||||||
|
real_arch = arch_map.get(full_arch, full_arch)
|
||||||
|
platform = platform_map.get(platform_name, platform_name)
|
||||||
|
|
||||||
|
version_type_list = ["jre", "jdk"]
|
||||||
|
|
||||||
|
jre_url = (
|
||||||
|
f"/?java_version={java_major_version}&os={platform}&arch={real_arch}&java_package_type=jre&javafx_bundled=true"
|
||||||
|
f"&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=100")
|
||||||
|
|
||||||
|
full_jre_url = azul_packages_api + jre_url
|
||||||
|
|
||||||
|
jdk_url = (f"/?java_version={java_major_version}&os={platform}&arch={real_arch}&java_package_type=jdk"
|
||||||
|
f"&javafx_bundled=true"
|
||||||
|
f"&release_status=ga&availability_types=CA&certifications=tck&page=1&page_size=100")
|
||||||
|
|
||||||
|
full_jdk_url = azul_packages_api + jdk_url
|
||||||
|
|
||||||
|
version_url_list = [full_jre_url, full_jdk_url]
|
||||||
|
for url, ver_type in zip(version_url_list, version_type_list):
|
||||||
|
try:
|
||||||
|
response = requests.get(url)
|
||||||
|
data = response.json()
|
||||||
|
java_ver_data = data[0]
|
||||||
|
download_url = java_ver_data.get("download_url", None)
|
||||||
|
|
||||||
|
if download_url is not None:
|
||||||
|
return True, download_url, ver_type
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print("[ERROR] Getting support jvm download url failed: ", e)
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
def get_java_version_manifest_data():
|
||||||
|
# Get java manifest
|
||||||
|
try:
|
||||||
|
response = requests.get(java_manifest_url)
|
||||||
|
if response.status_code == 200:
|
||||||
|
manifest_data = response.json()
|
||||||
|
return manifest_data
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_support_java_version(version_data):
|
||||||
|
"""
|
||||||
|
:param version_data: Special minecraft version data
|
||||||
|
:return Status, component, major_version
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
java_version_info = version_data['javaVersion']
|
||||||
|
component = java_version_info['component']
|
||||||
|
major_version = java_version_info['majorVersion']
|
||||||
|
return True, component, major_version
|
||||||
|
except KeyError:
|
||||||
|
return False, None, None
|
||||||
|
|
||||||
|
|
||||||
|
def get_support_java_version_from_java_version_manifest(platform, full_arch):
|
||||||
|
"""
|
||||||
|
:return SupportList
|
||||||
|
"""
|
||||||
|
|
||||||
|
platform_map = {
|
||||||
|
"darwin": "mac-os",
|
||||||
|
"windows": "windows",
|
||||||
|
"linux": "linux"
|
||||||
|
}
|
||||||
|
|
||||||
|
architecture_map = {
|
||||||
|
"amd64": ["x64"],
|
||||||
|
"i586": ["x86", "i386"],
|
||||||
|
"x86": ["i386", "x86"],
|
||||||
|
"arm64": ["arm64"],
|
||||||
|
"aarch64": ["arm64"]
|
||||||
|
}
|
||||||
|
|
||||||
|
platform_name = platform_map.get(platform.lower(), platform.lower())
|
||||||
|
|
||||||
|
architecture_list = architecture_map.get(full_arch.lower(), full_arch.lower())
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(java_manifest_url)
|
||||||
|
except Exception as e:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
java_manifest_data = response.json()
|
||||||
|
java_manifest_data_cleaned = {}
|
||||||
|
organized_manifest_data_list = []
|
||||||
|
|
||||||
|
# Get all platform data (except item "gamecore")
|
||||||
|
for arch_and_platform, support_java_data in java_manifest_data.items():
|
||||||
|
if arch_and_platform == "gamecore":
|
||||||
|
continue
|
||||||
|
|
||||||
|
java_manifest_data_cleaned.update({arch_and_platform: support_java_data})
|
||||||
|
|
||||||
|
# Separate platform name and arch from arch_and_platform
|
||||||
|
for arch_and_platform, support_java_data in java_manifest_data_cleaned.items():
|
||||||
|
if arch_and_platform == "mac-os":
|
||||||
|
# For mac os (x86-64)
|
||||||
|
java_platform = "mac-os"
|
||||||
|
java_arch = "amd64"
|
||||||
|
elif "-" in arch_and_platform:
|
||||||
|
parts = arch_and_platform.split("-")
|
||||||
|
java_platform = '-'.join(parts[:-1])
|
||||||
|
java_arch = parts[-1]
|
||||||
|
else:
|
||||||
|
java_platform = arch_and_platform
|
||||||
|
java_arch = "amd64"
|
||||||
|
|
||||||
|
for java_runtime_name, runtime_data in support_java_data.items():
|
||||||
|
if not java_runtime_name.startswith("java"):
|
||||||
|
if not java_runtime_name.startswith("jre"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Skip some empty data
|
||||||
|
if len(runtime_data) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If the platform and architecture are same, append data to list
|
||||||
|
if not java_platform.lower() == platform_name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if java_arch.lower() not in architecture_list:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Convert data to dict
|
||||||
|
result = {}
|
||||||
|
for item in runtime_data:
|
||||||
|
result.update(item)
|
||||||
|
organized_manifest_data_list.append(result)
|
||||||
|
|
||||||
|
return organized_manifest_data_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_support_java_runtime_version_data(organized_manifest_data_list, major_version):
|
||||||
|
"""
|
||||||
|
Warning: Require organized_manifest_data_list
|
||||||
|
"""
|
||||||
|
java_version_url = None
|
||||||
|
|
||||||
|
for data in organized_manifest_data_list:
|
||||||
|
name = data.get("version", {}).get("name", None)
|
||||||
|
url = data.get("manifest", {}).get("url", None)
|
||||||
|
if name is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if name.startswith(str(major_version)):
|
||||||
|
if url is not None:
|
||||||
|
java_version_url = url
|
||||||
|
|
||||||
|
if java_version_url is not None:
|
||||||
|
try:
|
||||||
|
response = requests.get(java_version_url)
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
return True, data
|
||||||
|
except Exception as e:
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
|
||||||
|
def create_java_version_info(java_major_version, java_arch, jvm_runtimes_path):
|
||||||
|
java_info_data = textwrap.dedent(f"""\
|
||||||
|
# Java Runtime Info
|
||||||
|
# This configuration is automatically generated by the BakeLauncher.
|
||||||
|
# Configuration stores Java for the launcher (Example: major_version, architecture).
|
||||||
|
# Do NOT edit this file or delete it!
|
||||||
|
|
||||||
|
JavaMajorVersion = "{java_major_version}"
|
||||||
|
Architecture = "{java_arch}"
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
if not os.path.exists(jvm_runtimes_path):
|
||||||
|
os.makedirs(jvm_runtimes_path)
|
||||||
|
|
||||||
|
java_info_path = os.path.join(jvm_runtimes_path, "java.version.info")
|
||||||
|
|
||||||
|
if not os.path.exists(java_info_path):
|
||||||
|
with open(java_info_path, 'w') as file:
|
||||||
|
file.write(java_info_data)
|
||||||
|
|
||||||
|
|
||||||
|
def read_java_info(info_file_path, item):
|
||||||
|
with open(info_file_path, "r") as file:
|
||||||
|
lines = file.readlines()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if item in line and '=' in line:
|
||||||
|
data = line.split('=', 1)[1].strip().strip("")
|
||||||
|
return str(data)
|
||||||
|
|
||||||
|
return None
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
"""
|
||||||
|
bk_core
|
||||||
|
|
||||||
|
Copyright (c) 2024~2025 Techarerm/TedKai
|
||||||
|
Copyright (c) 2026 Kitee Contributors. All rights reserved.
|
||||||
|
"""
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import requests
|
||||||
|
from ..bk_utils.utils import download_file, extract_zip
|
||||||
|
from ..java.java_info import get_java_build_download_url_from_azul
|
||||||
|
from ..bk_utils.crypto import verify_checksum
|
||||||
|
|
||||||
|
|
||||||
|
def download_java_file(file_info, file_path, destination_folder):
|
||||||
|
download_type = "raw"
|
||||||
|
file_url = file_info["downloads"][download_type]["url"]
|
||||||
|
full_file_path = os.path.join(destination_folder, file_path)
|
||||||
|
expected_sha1 = file_info["downloads"][download_type].get("sha1")
|
||||||
|
|
||||||
|
directory = os.path.join(destination_folder, os.path.dirname(file_path))
|
||||||
|
os.makedirs(directory, exist_ok=True)
|
||||||
|
|
||||||
|
if os.path.exists(full_file_path) and expected_sha1 and verify_checksum(full_file_path, expected_sha1):
|
||||||
|
return []
|
||||||
|
|
||||||
|
return download_file(
|
||||||
|
file_url,
|
||||||
|
full_file_path,
|
||||||
|
with_verify=True,
|
||||||
|
sha1=expected_sha1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def download_java_runtime_files(manifest, install_path):
|
||||||
|
if not os.path.exists(install_path):
|
||||||
|
return False, "InstallFolderAreNotExist"
|
||||||
|
|
||||||
|
files = manifest.get("files", {})
|
||||||
|
download_tasks = []
|
||||||
|
|
||||||
|
for file_path, file_info in files.items():
|
||||||
|
if file_info.get("type") == "directory":
|
||||||
|
os.makedirs(os.path.join(install_path, file_path), exist_ok=True)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "downloads" in file_info:
|
||||||
|
download_tasks.extend(download_java_file(file_info, file_path, install_path))
|
||||||
|
|
||||||
|
return True, download_tasks
|
||||||
|
|
||||||
|
|
||||||
|
def install_azul_build_version_jvm(java_major_version, install_dir, full_architecture, platform_name):
|
||||||
|
Status, download_url, version_type = get_java_build_download_url_from_azul(platform_name, full_architecture,
|
||||||
|
java_major_version)
|
||||||
|
|
||||||
|
if not Status:
|
||||||
|
return False, "Get download url failed. Unsupported platform"
|
||||||
|
|
||||||
|
tmp = os.path.join(os.path.dirname(install_dir), ".jvm_installer_tmp")
|
||||||
|
os.makedirs(tmp, exist_ok=True)
|
||||||
|
|
||||||
|
jvm_zip_file_path = os.path.join(tmp, f"jvm-azul-{java_major_version}.zip")
|
||||||
|
|
||||||
|
if os.path.exists(jvm_zip_file_path):
|
||||||
|
try:
|
||||||
|
os.remove(jvm_zip_file_path)
|
||||||
|
except Exception as e:
|
||||||
|
return False, "Cannot delete tmp file. ERR: {}".format(e)
|
||||||
|
|
||||||
|
jvm_unzip_dest = os.path.join(tmp, f"jvm-azul-{java_major_version}-unzipped")
|
||||||
|
if os.path.exists(jvm_unzip_dest):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(jvm_unzip_dest)
|
||||||
|
except Exception as e:
|
||||||
|
return False, "Cannot delete unzip tmp file. ERR: {}".format(e)
|
||||||
|
|
||||||
|
if not os.path.exists(jvm_zip_file_path):
|
||||||
|
try:
|
||||||
|
_download_file_now(download_url, jvm_zip_file_path)
|
||||||
|
except Exception as e:
|
||||||
|
return False, "Download file failed. ERR: {}".format(e)
|
||||||
|
|
||||||
|
extract_zip(jvm_zip_file_path, jvm_unzip_dest)
|
||||||
|
|
||||||
|
if os.path.exists(install_dir):
|
||||||
|
try:
|
||||||
|
shutil.rmtree(install_dir)
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Cleaning install dir failed. ERR: {e}"
|
||||||
|
|
||||||
|
if platform_name.lower() == "darwin":
|
||||||
|
unzip_list = os.listdir(jvm_unzip_dest)
|
||||||
|
jvm_app_folder_name = unzip_list[0]
|
||||||
|
home_folder_path = os.path.join(jvm_unzip_dest, jvm_app_folder_name, f"zulu-{java_major_version}.jre",
|
||||||
|
"Contents", "Home")
|
||||||
|
if not os.path.isdir(home_folder_path):
|
||||||
|
home_folder_path = _find_java_home(jvm_unzip_dest)
|
||||||
|
if home_folder_path is None:
|
||||||
|
return False, "JAVA_HOME not found in the unzip folder."
|
||||||
|
|
||||||
|
else:
|
||||||
|
unzip_list = os.listdir(jvm_unzip_dest)
|
||||||
|
home_folder_name = unzip_list[0]
|
||||||
|
home_folder_path = os.path.join(jvm_unzip_dest, home_folder_name)
|
||||||
|
if not os.path.isdir(home_folder_path):
|
||||||
|
home_folder_path = _find_java_home(jvm_unzip_dest)
|
||||||
|
if home_folder_path is None:
|
||||||
|
return False, "JAVA_HOME not found in the unzip folder."
|
||||||
|
|
||||||
|
install_root_dir = os.path.dirname(install_dir)
|
||||||
|
home_name = os.path.basename(home_folder_path)
|
||||||
|
dest_path = os.path.join(install_root_dir, home_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.move(home_folder_path, install_root_dir)
|
||||||
|
except Exception as e:
|
||||||
|
return False, f"Move home folder to install dir failed. ERR: {e}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.rename(dest_path, install_dir)
|
||||||
|
except Exception as e:
|
||||||
|
return False, "Rename dest_path to require name failed. ERR: {}".format(e)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_java_home(root):
|
||||||
|
for current_root, _, files in os.walk(root):
|
||||||
|
executable_names = {"java.exe", "javaw.exe"} if os.name == "nt" else {"java"}
|
||||||
|
if os.path.basename(current_root).lower() == "bin" and any(name in executable_names for name in files):
|
||||||
|
return os.path.dirname(current_root)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_java_home_in_extracted_runtime(unzip_dir):
|
||||||
|
candidates = []
|
||||||
|
for java_name in ("javaw.exe", "java.exe", "java"):
|
||||||
|
candidates.extend(unzip_dir.rglob(java_name))
|
||||||
|
|
||||||
|
for java_path in candidates:
|
||||||
|
parent = java_path.parent
|
||||||
|
if parent.name.lower() == "bin":
|
||||||
|
return parent.parent
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def runtime_java_executable(install_dir):
|
||||||
|
bin_dir = install_dir / "bin"
|
||||||
|
if os.name == "nt":
|
||||||
|
javaw = bin_dir / "javaw.exe"
|
||||||
|
if javaw.exists():
|
||||||
|
return javaw
|
||||||
|
return bin_dir / "java.exe"
|
||||||
|
return bin_dir / "java"
|
||||||
|
|
||||||
|
|
||||||
|
def find_java_runtime_candidates(java_manifest, manifest_platform, java_major_version):
|
||||||
|
platform_data = java_manifest.get(manifest_platform, {})
|
||||||
|
candidates = []
|
||||||
|
for runtime_entries in platform_data.values():
|
||||||
|
for runtime_entry in runtime_entries:
|
||||||
|
version_name = str(runtime_entry.get("version", {}).get("name") or "")
|
||||||
|
if version_name.startswith(str(java_major_version)):
|
||||||
|
candidates.append(runtime_entry)
|
||||||
|
return candidates
|
||||||
|
|
||||||
|
|
||||||
|
def mojang_java_platform_key():
|
||||||
|
system = platform.system().lower()
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
|
||||||
|
if system == "windows":
|
||||||
|
if "arm" in machine or "aarch64" in machine:
|
||||||
|
return "windows-arm64"
|
||||||
|
if "64" in machine or "amd64" in machine:
|
||||||
|
return "windows-x64"
|
||||||
|
return "windows-x86"
|
||||||
|
|
||||||
|
if system == "darwin":
|
||||||
|
return "mac-os-arm64" if "arm" in machine or "aarch64" in machine else "mac-os"
|
||||||
|
|
||||||
|
if system == "linux":
|
||||||
|
return "linux-arm64" if "arm" in machine or "aarch64" in machine else "linux"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _download_file_now(url, dest_path):
|
||||||
|
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
|
||||||
|
response = requests.get(url, stream=True, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
with open(dest_path, "wb") as file:
|
||||||
|
for chunk in response.iter_content(chunk_size=1024 * 128):
|
||||||
|
if chunk:
|
||||||
|
file.write(chunk)
|
||||||
Reference in New Issue
Block a user