mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 00:23:42 +02:00
184 lines
6.7 KiB
Python
184 lines
6.7 KiB
Python
"""Modlist fixup handler.
|
|
|
|
Applies known binary fixes to installed modlists during the configure phase.
|
|
Each fix is idempotent: it checks the current state before acting and skips
|
|
if the fix is already in place.
|
|
"""
|
|
import hashlib
|
|
import logging
|
|
import os
|
|
import subprocess
|
|
import tempfile
|
|
import urllib.request
|
|
from pathlib import Path
|
|
from typing import Callable, Optional
|
|
|
|
from jackify.shared.paths import get_jackify_data_dir
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# JContainers SE - Linux crash fix
|
|
# Nexus releases up to and including 4.2.9.0 crash on Linux.
|
|
# Fixed build from https://github.com/rfortier/JContainers-rwf (pending Nexus release).
|
|
# We replace the DLL in-place in whichever mod directory ships it.
|
|
_JCONTAINERS_FIXED_SHA256 = "4c00d7194c61097361e8f93521d79752a975fca5f54446e391debb0c56999590"
|
|
_JCONTAINERS_FIXED_URL = (
|
|
"https://github.com/rfortier/JContainers-rwf/releases/download/v4.2.13.2/"
|
|
"JContainers64-v4.2.13.2.for.1.6.1170.patch.luajit.with.gc64.7z"
|
|
)
|
|
_JCONTAINERS_DLL_NAME = "JContainers64.dll"
|
|
_JCONTAINERS_ARCHIVE_NAME = "JContainers64-v4.2.13.2.linux-fix.7z"
|
|
|
|
|
|
def _sha256(path: Path) -> str:
|
|
h = hashlib.sha256()
|
|
with open(path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(65536), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
def _get_7z() -> Optional[Path]:
|
|
candidates = []
|
|
appdir = os.environ.get("APPDIR")
|
|
if appdir:
|
|
candidates.append(Path(appdir) / "opt" / "jackify" / "tools" / "7z")
|
|
candidates.append(Path(__file__).parent.parent.parent / "tools" / "7z")
|
|
for p in candidates:
|
|
if p.exists() and os.access(p, os.X_OK):
|
|
return p
|
|
return None
|
|
|
|
|
|
def _find_dll_in_archive(seven_z: Path, archive: Path) -> Optional[str]:
|
|
"""Return the internal path of JContainers64.dll within the archive, or None."""
|
|
try:
|
|
result = subprocess.run(
|
|
[str(seven_z), "l", "-ba", "-slt", str(archive)],
|
|
capture_output=True, text=True, timeout=30,
|
|
)
|
|
for line in result.stdout.splitlines():
|
|
if line.startswith("Path = ") and line.lower().endswith(_JCONTAINERS_DLL_NAME.lower()):
|
|
return line[7:].strip()
|
|
except Exception as exc:
|
|
logger.debug("7z list failed: %s", exc)
|
|
return None
|
|
|
|
|
|
_JCONTAINERS_TARGET_SKSE = "skse64_1_6_1170.dll"
|
|
|
|
|
|
def _detect_skse_dll(mods_dir: Path) -> Optional[str]:
|
|
"""Return the SKSE loader DLL filename found in mods, or None."""
|
|
for dll in mods_dir.glob("*/Root/skse64_*.dll"):
|
|
return dll.name
|
|
return None
|
|
|
|
|
|
def check_jcontainers_needs_fix(modlist_dir: Path, game_type: Optional[str]) -> list:
|
|
"""Return list of JContainers64.dll paths that need replacing, or empty list."""
|
|
if not game_type or "skyrim" not in game_type.lower():
|
|
return []
|
|
mods_dir = Path(modlist_dir) / "mods"
|
|
if not mods_dir.is_dir():
|
|
return []
|
|
if _detect_skse_dll(mods_dir) != _JCONTAINERS_TARGET_SKSE:
|
|
return []
|
|
targets = list(mods_dir.glob(f"*/SKSE/Plugins/{_JCONTAINERS_DLL_NAME}"))
|
|
return [p for p in targets if _sha256(p) != _JCONTAINERS_FIXED_SHA256]
|
|
|
|
|
|
def apply_jcontainers_fix(
|
|
modlist_dir: Path,
|
|
game_var_full: Optional[str],
|
|
status_callback: Optional[Callable] = None,
|
|
) -> None:
|
|
"""Replace a broken JContainers64.dll with the Linux-compatible build.
|
|
|
|
Scoped to Skyrim-based modlists only. Skips silently if already fixed,
|
|
if no JContainers mod is found, or if required tools are unavailable.
|
|
"""
|
|
if not game_var_full or "skyrim" not in game_var_full.lower():
|
|
return
|
|
|
|
mods_dir = Path(modlist_dir) / "mods"
|
|
if not mods_dir.is_dir():
|
|
return
|
|
|
|
if _detect_skse_dll(mods_dir) != _JCONTAINERS_TARGET_SKSE:
|
|
return
|
|
|
|
targets = list(mods_dir.glob(f"*/SKSE/Plugins/{_JCONTAINERS_DLL_NAME}"))
|
|
if not targets:
|
|
logger.debug("JContainers fix: no %s found under %s", _JCONTAINERS_DLL_NAME, mods_dir)
|
|
return
|
|
|
|
needs_fix = [p for p in targets if _sha256(p) != _JCONTAINERS_FIXED_SHA256]
|
|
if not needs_fix:
|
|
logger.debug("JContainers fix: all instances already at known-good version")
|
|
return
|
|
|
|
seven_z = _get_7z()
|
|
if not seven_z:
|
|
logger.warning("JContainers fix: 7z not available, cannot apply fix")
|
|
return
|
|
|
|
if status_callback:
|
|
status_callback("Applying JContainers Linux compatibility fix")
|
|
logger.info("JContainers fix: %d instance(s) need replacement", len(needs_fix))
|
|
|
|
cache_dir = get_jackify_data_dir() / "component_cache" / "fixups"
|
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
archive_path = cache_dir / _JCONTAINERS_ARCHIVE_NAME
|
|
|
|
if not archive_path.exists():
|
|
logger.info("JContainers fix: downloading fixed archive from %s", _JCONTAINERS_FIXED_URL)
|
|
try:
|
|
urllib.request.urlretrieve(_JCONTAINERS_FIXED_URL, archive_path)
|
|
except Exception as exc:
|
|
logger.error("JContainers fix: download failed: %s", exc)
|
|
return
|
|
|
|
dll_internal_path = _find_dll_in_archive(seven_z, archive_path)
|
|
if not dll_internal_path:
|
|
logger.error("JContainers fix: could not locate %s inside archive", _JCONTAINERS_DLL_NAME)
|
|
archive_path.unlink(missing_ok=True)
|
|
return
|
|
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
try:
|
|
subprocess.run(
|
|
[str(seven_z), "e", str(archive_path), f"-o{tmp}", dll_internal_path, "-y"],
|
|
capture_output=True, timeout=60, check=True,
|
|
)
|
|
except subprocess.CalledProcessError as exc:
|
|
logger.error("JContainers fix: extraction failed: %s", exc)
|
|
archive_path.unlink(missing_ok=True)
|
|
return
|
|
|
|
extracted = Path(tmp) / _JCONTAINERS_DLL_NAME
|
|
if not extracted.exists():
|
|
logger.error("JContainers fix: %s not found after extraction", _JCONTAINERS_DLL_NAME)
|
|
archive_path.unlink(missing_ok=True)
|
|
return
|
|
|
|
actual_hash = _sha256(extracted)
|
|
if actual_hash != _JCONTAINERS_FIXED_SHA256:
|
|
logger.error(
|
|
"JContainers fix: extracted DLL hash %s does not match expected %s, aborting",
|
|
actual_hash, _JCONTAINERS_FIXED_SHA256,
|
|
)
|
|
archive_path.unlink(missing_ok=True)
|
|
return
|
|
|
|
import shutil
|
|
for target in needs_fix:
|
|
try:
|
|
backup = target.with_suffix(".dll.bak")
|
|
shutil.copy2(str(target), str(backup))
|
|
logger.info("JContainers fix: backed up %s -> %s", target.name, backup.name)
|
|
shutil.copy2(str(extracted), str(target))
|
|
logger.info("JContainers fix: replaced %s", target)
|
|
except Exception as exc:
|
|
logger.error("JContainers fix: failed to replace %s: %s", target, exc)
|