mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 02:23:45 +02:00
Release v0.7 - Tools Hub, Engine Choice, NXM Link Handling, Native Component Install, NSF/CSF Support
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user