mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 00:23:42 +02:00
578 lines
24 KiB
Python
578 lines
24 KiB
Python
"""Native Wine component installer.
|
|
|
|
Direct-source replacements for winetricks components.
|
|
Falls back to winetricks -> protontricks for unsupported or failed components.
|
|
"""
|
|
|
|
import datetime
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
import urllib.request
|
|
import zipfile
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Tuple
|
|
|
|
from jackify.shared.paths import get_jackify_data_dir
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_D3DCOMPILER_47_X86_URL = "https://github.com/mozilla/fxc2/raw/master/dll/d3dcompiler_47_32.dll"
|
|
_D3DCOMPILER_47_X64_URL = "https://github.com/mozilla/fxc2/raw/master/dll/d3dcompiler_47.dll"
|
|
_D3DCOMPILER_47_X86_SHA256 = "2ad0d4987fc4624566b190e747c9d95038443956ed816abfd1e2d389b5ec0851"
|
|
_D3DCOMPILER_47_X64_SHA256 = "4432bbd1a390874f3f0a503d45cc48d346abc3a8c0213c289f4b615bf0ee84f3"
|
|
|
|
_DIRECTX_CAB_URL = "https://files.holarse-linuxgaming.de/mirrors/microsoft/directx_Jun2010_redist.exe"
|
|
_DIRECTX_CAB_SHA256 = "8746ee1a84a083a90e37899d71d50d5c7c015e69688a466aa80447f011780c0d"
|
|
|
|
_VCRUN2022_X86_URL = "https://aka.ms/vs/17/release/vc_redist.x86.exe"
|
|
_VCRUN2022_X64_URL = "https://aka.ms/vs/17/release/vc_redist.x64.exe"
|
|
|
|
_VCRUN2022_DLLS_X86 = ["concrt140.dll", "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll",
|
|
"msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll",
|
|
"vcamp140.dll", "vccorlib140.dll", "vcomp140.dll", "vcruntime140.dll"]
|
|
_VCRUN2022_DLLS_X64 = _VCRUN2022_DLLS_X86 + ["vcruntime140_1.dll"]
|
|
|
|
_VCRUN2012_X86_URL = "https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe"
|
|
_VCRUN2012_X64_URL = "https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe"
|
|
|
|
|
|
# (x86_stage1_patterns, x64_stage1_patterns, x86_dll_filters, x64_dll_filters, regsvr32)
|
|
_DX_CFG: Dict[str, tuple] = {
|
|
"d3dcompiler_43": (
|
|
["*d3dcompiler_43*x86*"], ["*d3dcompiler_43*x64*"],
|
|
["d3dcompiler_43.dll"], ["d3dcompiler_43.dll"], False),
|
|
"d3dx9": (
|
|
["*d3dx9*x86*"], ["*d3dx9*x64*"],
|
|
["d3dx9_*.dll"], ["d3dx9_*.dll"], False),
|
|
"d3dx9_43": (
|
|
["*d3dx9*x86*"], ["*d3dx9*x64*"],
|
|
["d3dx9_43.dll"], ["d3dx9_43.dll"], False),
|
|
"d3dx11_43": (
|
|
["*d3dx11_43*x86*"], ["*d3dx11_43*x64*"],
|
|
["d3dx11_43.dll"], ["d3dx11_43.dll"], False),
|
|
"xact": (
|
|
["*_xact_*x86*", "*_x3daudio_*x86*", "*_xaudio_*x86*"], [],
|
|
["xactengine*.dll", "xaudio*.dll", "x3daudio*.dll", "xapofx*.dll"], [], True),
|
|
"xact_x64": (
|
|
[], ["*_xact_*x64*", "*_x3daudio_*x64*", "*_xaudio_*x64*"],
|
|
[], ["xactengine*.dll", "xaudio*.dll", "x3daudio*.dll", "xapofx*.dll"], True),
|
|
}
|
|
|
|
_REGISTRY_WRITE_COMPONENTS = {"fontsmooth=rgb"}
|
|
_DLL_COPY_COMPONENTS = {"d3dcompiler_47"}
|
|
_DIRECTX_CAB_COMPONENTS = set(_DX_CFG.keys())
|
|
_WINE_INSTALLER_COMPONENTS = {"vcrun2022", "vcrun2012", "dotnet6", "dotnet7", "dotnet8", "dotnetdesktop6", "dotnet9", "dotnetdesktop9", "dotnet10", "dotnetdesktop10"}
|
|
|
|
# dotnet48 is handled by the bundled winetricks verb (see winetricks_handler), not natively:
|
|
# the ndp48 in-place servicing corrupts mscorlib under Wine.
|
|
SUPPORTED_COMPONENTS = (
|
|
_REGISTRY_WRITE_COMPONENTS
|
|
| _DLL_COPY_COMPONENTS
|
|
| _DIRECTX_CAB_COMPONENTS
|
|
| _WINE_INSTALLER_COMPONENTS
|
|
)
|
|
|
|
|
|
class NativeComponentInstaller:
|
|
"""Direct-source Wine component installer. Handles Groups 1-4 from the native install spec."""
|
|
|
|
def __init__(self, wineprefix: str, wine_binary: str, wine_env: dict, log=None):
|
|
self.wineprefix = wineprefix
|
|
self.wine_binary = wine_binary
|
|
self.wine_env = wine_env
|
|
self.logger = log or logging.getLogger(__name__)
|
|
|
|
def _emit_status(self, msg: str) -> None:
|
|
cb = getattr(self, '_status_callback', None)
|
|
if cb:
|
|
cb(msg)
|
|
|
|
def _wine_env_base(self, **extra) -> dict:
|
|
base = {**self.wine_env, 'WINEPREFIX': self.wineprefix}
|
|
parts = []
|
|
for src in (base, extra):
|
|
v = src.pop('WINEDLLOVERRIDES', None)
|
|
if v:
|
|
parts.append(v)
|
|
parts.append('winemenubuilder.exe=d')
|
|
base.update(extra)
|
|
base['WINEDLLOVERRIDES'] = ','.join(parts)
|
|
return base
|
|
|
|
def install_components(
|
|
self,
|
|
components: List[str],
|
|
status_callback=None,
|
|
) -> Tuple[List[str], List[str]]:
|
|
"""Attempt native install. Returns (succeeded, remaining); remaining goes to winetricks."""
|
|
self._status_callback = status_callback
|
|
succeeded = []
|
|
remaining = []
|
|
|
|
native_candidates = [c for c in components if c in SUPPORTED_COMPONENTS]
|
|
|
|
for component in components:
|
|
if component not in SUPPORTED_COMPONENTS:
|
|
remaining.append(component)
|
|
continue
|
|
|
|
self._current_component = component
|
|
if status_callback:
|
|
status_callback(f"[NATIVE_INSTALL] {component}")
|
|
|
|
try:
|
|
ok = self._install_component(component)
|
|
except Exception as exc:
|
|
self.logger.error("Native install of %s raised: %s", component, exc, exc_info=True)
|
|
ok = False
|
|
|
|
if ok:
|
|
self.logger.info("Native install succeeded: %s", component)
|
|
succeeded.append(component)
|
|
self._write_winetricks_log(component)
|
|
else:
|
|
self.logger.warning("Native install failed for %s, falling through to winetricks", component)
|
|
remaining.append(component)
|
|
|
|
if succeeded:
|
|
self._apply_dll_overrides()
|
|
|
|
return succeeded, remaining
|
|
|
|
def _install_component(self, component: str) -> bool:
|
|
if component in _REGISTRY_WRITE_COMPONENTS:
|
|
return self._install_registry_write(component)
|
|
if component in _DLL_COPY_COMPONENTS:
|
|
return self._install_dll_copy(component)
|
|
if component in _DIRECTX_CAB_COMPONENTS:
|
|
return self._install_directx_cab(component)
|
|
if component == "vcrun2022":
|
|
return self._install_vcrun2022()
|
|
if component == "vcrun2012":
|
|
return self._install_vcrun2012()
|
|
return self._install_dotnet_modern(component)
|
|
|
|
def _write_winetricks_log(self, component: str) -> None:
|
|
log_path = Path(self.wineprefix) / 'winetricks.log'
|
|
try:
|
|
with open(log_path, 'a', encoding='utf-8') as f:
|
|
f.write(component + '\n')
|
|
except Exception as exc:
|
|
self.logger.warning("Could not write winetricks.log: %s", exc)
|
|
self._write_jackify_component_record(component)
|
|
|
|
def _write_jackify_component_record(self, component: str) -> None:
|
|
record_path = Path(self.wineprefix) / 'jackify_components.json'
|
|
try:
|
|
record = json.loads(record_path.read_text(encoding='utf-8')) if record_path.is_file() else {}
|
|
except Exception:
|
|
record = {}
|
|
record[component] = {"method": "native", "timestamp": datetime.datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%S')}
|
|
try:
|
|
record_path.write_text(json.dumps(record, indent=2), encoding='utf-8')
|
|
except Exception as exc:
|
|
self.logger.warning("Could not write jackify_components.json: %s", exc)
|
|
|
|
def _apply_dll_overrides(self) -> None:
|
|
reg_file = Path(__file__).parent.parent / 'data' / 'dll_overrides.reg'
|
|
if not reg_file.is_file():
|
|
self.logger.warning("dll_overrides.reg not found at %s", reg_file)
|
|
return
|
|
overrides: Dict[str, str] = {}
|
|
in_target = False
|
|
for line in reg_file.read_text(encoding='utf-8').splitlines():
|
|
s = line.strip()
|
|
if s.startswith('['):
|
|
in_target = 'Wine\\DllOverrides' in s
|
|
elif in_target and s.startswith('"'):
|
|
try:
|
|
q = s.index('"', 1)
|
|
rest = s[q + 1:]
|
|
if rest.startswith('='):
|
|
overrides[s[1:q]] = rest[1:]
|
|
except (ValueError, IndexError):
|
|
pass
|
|
if overrides:
|
|
self._direct_reg_write(r'Software\Wine\DllOverrides', overrides)
|
|
self.logger.debug("DLL overrides written directly to user.reg (%d entries)", len(overrides))
|
|
|
|
def _download_file(self, url: str, dest: Path, sha256: str = "") -> bool:
|
|
if dest.is_file():
|
|
if not sha256:
|
|
return True
|
|
if self._verify_sha256(dest, sha256):
|
|
return True
|
|
self.logger.warning("SHA256 mismatch on cached %s, re-downloading", dest.name)
|
|
dest.unlink()
|
|
component = getattr(self, '_current_component', dest.stem)
|
|
self.logger.info("Downloading %s ...", dest.name)
|
|
self._emit_status(f"Downloading {dest.name}...")
|
|
try:
|
|
req = urllib.request.Request(url, headers={'User-Agent': 'Jackify/1.0'})
|
|
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
total = int(resp.headers.get('Content-Length', 0) or 0)
|
|
downloaded = 0
|
|
start = time.monotonic()
|
|
last_emit = start
|
|
chunk_size = 65536
|
|
with open(dest, 'wb') as f:
|
|
while True:
|
|
chunk = resp.read(chunk_size)
|
|
if not chunk:
|
|
break
|
|
f.write(chunk)
|
|
downloaded += len(chunk)
|
|
now = time.monotonic()
|
|
if total > 0 and now - last_emit >= 0.5:
|
|
pct = downloaded / total * 100.0
|
|
elapsed = now - start
|
|
speed = downloaded / elapsed / 1048576.0 if elapsed > 0.05 else 0.0
|
|
self._emit_status(f"[NATIVE_DL] {component} {pct:.1f} {speed:.1f}")
|
|
last_emit = now
|
|
return True
|
|
except Exception as exc:
|
|
self.logger.error("Download failed for %s: %s", url, exc)
|
|
if dest.is_file():
|
|
dest.unlink()
|
|
return False
|
|
|
|
def _verify_sha256(self, path: Path, expected: str) -> bool:
|
|
h = hashlib.sha256()
|
|
try:
|
|
with open(path, 'rb') as f:
|
|
for chunk in iter(lambda: f.read(65536), b''):
|
|
h.update(chunk)
|
|
return h.hexdigest().lower() == expected.lower()
|
|
except Exception:
|
|
return False
|
|
|
|
def _get_cabextract(self) -> Optional[str]:
|
|
if os.environ.get('APPDIR'):
|
|
candidate = os.path.join(os.environ['APPDIR'], 'opt', 'jackify', 'tools', 'cabextract')
|
|
if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
|
|
return candidate
|
|
dev_candidate = str(Path(__file__).parent.parent.parent / 'tools' / 'cabextract')
|
|
if os.path.isfile(dev_candidate) and os.access(dev_candidate, os.X_OK):
|
|
return dev_candidate
|
|
return shutil.which('cabextract')
|
|
|
|
def _install_registry_write(self, component: str) -> bool:
|
|
if component != "fontsmooth=rgb":
|
|
return False
|
|
return self._direct_reg_write(
|
|
r'Control Panel\Desktop',
|
|
{
|
|
'FontSmoothing': '"2"',
|
|
'FontSmoothingType': 'dword:00000002',
|
|
'FontSmoothingGamma': 'dword:00000578',
|
|
},
|
|
)
|
|
|
|
def _install_dll_copy(self, component: str) -> bool:
|
|
if component != "d3dcompiler_47":
|
|
return False
|
|
cache_dir = get_jackify_data_dir() / 'component_cache' / 'd3dcompiler'
|
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
syswow64 = Path(self.wineprefix) / 'drive_c' / 'windows' / 'syswow64'
|
|
system32 = Path(self.wineprefix) / 'drive_c' / 'windows' / 'system32'
|
|
syswow64.mkdir(parents=True, exist_ok=True)
|
|
system32.mkdir(parents=True, exist_ok=True)
|
|
|
|
for url, sha256, dest, fname in [
|
|
(_D3DCOMPILER_47_X86_URL, _D3DCOMPILER_47_X86_SHA256, syswow64, 'd3dcompiler_47_32.dll'),
|
|
(_D3DCOMPILER_47_X64_URL, _D3DCOMPILER_47_X64_SHA256, system32, 'd3dcompiler_47.dll'),
|
|
]:
|
|
cached = cache_dir / fname
|
|
if not self._download_file(url, cached, sha256):
|
|
return False
|
|
if not self._verify_sha256(cached, sha256):
|
|
self.logger.error("SHA256 mismatch on %s after download", fname)
|
|
cached.unlink()
|
|
return False
|
|
shutil.copy2(cached, dest / 'd3dcompiler_47.dll')
|
|
return True
|
|
|
|
def _install_directx_cab(self, component: str) -> bool:
|
|
cfg = _DX_CFG.get(component)
|
|
if not cfg:
|
|
return False
|
|
cabextract = self._get_cabextract()
|
|
if not cabextract:
|
|
self.logger.warning("cabextract not available for %s", component)
|
|
return False
|
|
|
|
cache_dir = get_jackify_data_dir() / 'component_cache' / 'directx'
|
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
redist = cache_dir / 'directx_Jun2010_redist.exe'
|
|
if not self._download_file(_DIRECTX_CAB_URL, redist, _DIRECTX_CAB_SHA256):
|
|
return False
|
|
if not self._verify_sha256(redist, _DIRECTX_CAB_SHA256):
|
|
self.logger.error("SHA256 mismatch on DirectX redistributable")
|
|
redist.unlink()
|
|
return False
|
|
|
|
x86_patterns, x64_patterns, x86_dll_filters, x64_dll_filters, needs_regsvr32 = cfg
|
|
syswow64, system32 = self._get_system_dirs()
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
tmpdir_path = Path(tmpdir)
|
|
for arch_tag, stage1_patterns, dll_filters, dest_dir in [
|
|
('x86', x86_patterns, x86_dll_filters, syswow64),
|
|
('x64', x64_patterns, x64_dll_filters, system32),
|
|
]:
|
|
if not stage1_patterns:
|
|
continue
|
|
arch_tmp = tmpdir_path / arch_tag
|
|
arch_tmp.mkdir()
|
|
|
|
for pattern in stage1_patterns:
|
|
subprocess.run(
|
|
[cabextract, '-d', str(arch_tmp), '-L', '-F', pattern, str(redist)],
|
|
capture_output=True,
|
|
)
|
|
|
|
inner_cabs = list(arch_tmp.glob('*.cab'))
|
|
if not inner_cabs:
|
|
self.logger.error("No inner cabs found for %s %s", component, arch_tag)
|
|
return False
|
|
|
|
for dll_filter in dll_filters:
|
|
for inner_cab in inner_cabs:
|
|
subprocess.run(
|
|
[cabextract, '-d', str(dest_dir), '-L', '-F', dll_filter, str(inner_cab)],
|
|
capture_output=True,
|
|
)
|
|
|
|
if needs_regsvr32:
|
|
com_dlls = list(syswow64.glob('xactengine*.dll')) + list(syswow64.glob('xaudio*.dll'))
|
|
com_dlls += list(system32.glob('xactengine*.dll')) + list(system32.glob('xaudio*.dll'))
|
|
self._register_xact_com(com_dlls)
|
|
return True
|
|
|
|
def _install_vcrun2022(self) -> bool:
|
|
cabextract = self._get_cabextract()
|
|
if not cabextract:
|
|
self.logger.warning("cabextract not available for vcrun2022")
|
|
return False
|
|
|
|
cache_dir = get_jackify_data_dir() / 'component_cache' / 'vcrun'
|
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
x86 = cache_dir / 'vc_redist.x86.exe'
|
|
x64 = cache_dir / 'vc_redist.x64.exe'
|
|
if not self._download_file(_VCRUN2022_X86_URL, x86):
|
|
return False
|
|
if not self._download_file(_VCRUN2022_X64_URL, x64):
|
|
return False
|
|
|
|
syswow64, system32 = self._get_system_dirs()
|
|
|
|
# DLL overrides must be written before the installer runs so Wine picks them up
|
|
self._apply_dll_overrides()
|
|
|
|
env = self._wine_env_base()
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
tmpdir_path = Path(tmpdir)
|
|
|
|
# x86: pre-extract msvcp140.dll before running the installer.
|
|
# Wine's builtin msvcp140 reports a higher version number so the installer
|
|
# skips it without this manual extraction (Wine bug #57518).
|
|
win32_tmp = tmpdir_path / 'win32'
|
|
win32_tmp.mkdir()
|
|
subprocess.run(
|
|
[cabextract, '-d', str(win32_tmp), '-F', 'a10', str(x86)],
|
|
capture_output=True,
|
|
)
|
|
inner_x86 = win32_tmp / 'a10'
|
|
if inner_x86.is_file():
|
|
subprocess.run(
|
|
[cabextract, '-d', str(syswow64), '-F', 'msvcp140.dll', str(inner_x86)],
|
|
capture_output=True,
|
|
)
|
|
else:
|
|
self.logger.warning("vcrun2022: x86 inner cab 'a10' not found, msvcp140.dll pre-extraction skipped")
|
|
|
|
self.logger.info("vcrun2022: running x86 installer")
|
|
r = subprocess.run(
|
|
[self.wine_binary, str(x86), '/q'],
|
|
env=env,
|
|
capture_output=True,
|
|
timeout=600,
|
|
)
|
|
# 3010 = reboot required - normal for VC redist, treat as success
|
|
if r.returncode not in (0, 3010):
|
|
self.logger.error("vcrun2022: x86 installer failed (rc=%d)", r.returncode)
|
|
self.logger.debug("vcrun2022 x86 stderr: %s", r.stderr.decode(errors='replace'))
|
|
return False
|
|
|
|
# x64: same msvcp140.dll pre-extraction workaround
|
|
win64_tmp = tmpdir_path / 'win64'
|
|
win64_tmp.mkdir()
|
|
subprocess.run(
|
|
[cabextract, '-d', str(win64_tmp), '-F', 'a12', str(x64)],
|
|
capture_output=True,
|
|
)
|
|
inner_x64 = win64_tmp / 'a12'
|
|
if inner_x64.is_file():
|
|
subprocess.run(
|
|
[cabextract, '-d', str(system32), '-F', 'msvcp140.dll', str(inner_x64)],
|
|
capture_output=True,
|
|
)
|
|
else:
|
|
self.logger.warning("vcrun2022: x64 inner cab 'a12' not found, msvcp140.dll pre-extraction skipped")
|
|
|
|
self.logger.info("vcrun2022: running x64 installer")
|
|
r = subprocess.run(
|
|
[self.wine_binary, str(x64), '/q'],
|
|
env=env,
|
|
capture_output=True,
|
|
timeout=600,
|
|
)
|
|
if r.returncode not in (0, 3010):
|
|
self.logger.error("vcrun2022: x64 installer failed (rc=%d)", r.returncode)
|
|
self.logger.debug("vcrun2022 x64 stderr: %s", r.stderr.decode(errors='replace'))
|
|
return False
|
|
|
|
critical = [
|
|
(syswow64 / 'msvcp140.dll', 'msvcp140.dll (x86)'),
|
|
(syswow64 / 'vcruntime140.dll', 'vcruntime140.dll (x86)'),
|
|
(system32 / 'msvcp140.dll', 'msvcp140.dll (x64)'),
|
|
(system32 / 'vcruntime140.dll', 'vcruntime140.dll (x64)'),
|
|
(system32 / 'vcruntime140_1.dll', 'vcruntime140_1.dll (x64)'),
|
|
]
|
|
for path, label in critical:
|
|
if not path.is_file():
|
|
self.logger.error("vcrun2022: %s missing after install", label)
|
|
return False
|
|
|
|
return True
|
|
|
|
def _install_vcrun2012(self) -> bool:
|
|
cache_dir = get_jackify_data_dir() / 'component_cache' / 'vcrun2012'
|
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
x86 = cache_dir / 'vcredist_x86.exe'
|
|
x64 = cache_dir / 'vcredist_x64.exe'
|
|
if not self._download_file(_VCRUN2012_X86_URL, x86):
|
|
return False
|
|
if not self._download_file(_VCRUN2012_X64_URL, x64):
|
|
return False
|
|
|
|
self._apply_dll_overrides()
|
|
|
|
env = self._wine_env_base()
|
|
syswow64, system32 = self._get_system_dirs()
|
|
|
|
self.logger.info("vcrun2012: running x86 installer")
|
|
r = subprocess.run(
|
|
[self.wine_binary, str(x86), '/q'],
|
|
env=env,
|
|
capture_output=True,
|
|
timeout=300,
|
|
)
|
|
if r.returncode not in (0, 3010):
|
|
self.logger.error("vcrun2012: x86 installer failed (rc=%d)", r.returncode)
|
|
self.logger.debug("vcrun2012 x86 stderr: %s", r.stderr.decode(errors='replace'))
|
|
return False
|
|
|
|
self.logger.info("vcrun2012: running x64 installer")
|
|
r = subprocess.run(
|
|
[self.wine_binary, str(x64), '/q'],
|
|
env=env,
|
|
capture_output=True,
|
|
timeout=300,
|
|
)
|
|
if r.returncode not in (0, 3010):
|
|
self.logger.error("vcrun2012: x64 installer failed (rc=%d)", r.returncode)
|
|
self.logger.debug("vcrun2012 x64 stderr: %s", r.stderr.decode(errors='replace'))
|
|
return False
|
|
|
|
if not (syswow64 / 'msvcr110.dll').is_file():
|
|
self.logger.error("vcrun2012: msvcr110.dll missing after install")
|
|
return False
|
|
return True
|
|
|
|
def _install_dotnet_modern(self, component: str) -> bool:
|
|
urls = self._get_dotnet_urls(component)
|
|
if not urls:
|
|
return False
|
|
x86_url, x64_url = urls
|
|
cache_dir = get_jackify_data_dir() / 'component_cache' / 'dotnet'
|
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
x86_zip = cache_dir / Path(x86_url).name
|
|
x64_zip = cache_dir / Path(x64_url).name
|
|
if not self._download_file(x86_url, x86_zip):
|
|
return False
|
|
if not self._download_file(x64_url, x64_zip):
|
|
return False
|
|
pfx = Path(self.wineprefix) / 'drive_c'
|
|
for zip_path, dest in [(x86_zip, pfx / 'Program Files (x86)' / 'dotnet'),
|
|
(x64_zip, pfx / 'Program Files' / 'dotnet')]:
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
try:
|
|
with zipfile.ZipFile(zip_path) as zf:
|
|
zf.extractall(dest)
|
|
except Exception as exc:
|
|
self.logger.error("%s zip extraction failed for %s: %s", component, zip_path.name, exc)
|
|
return False
|
|
return True
|
|
|
|
def _get_dotnet_urls(self, component: str) -> Optional[Tuple[str, str]]:
|
|
manifest = Path(__file__).parent.parent / 'data' / 'native_components_versions.json'
|
|
try:
|
|
entry = json.loads(manifest.read_text()).get(component, {})
|
|
x86 = entry.get('x86_zip_url', '')
|
|
x64 = entry.get('x64_zip_url', '')
|
|
if x86 and x64:
|
|
return x86, x64
|
|
except Exception as exc:
|
|
self.logger.error("Could not load dotnet URLs for %s: %s", component, exc)
|
|
self.logger.error("No zip URLs for %s in versions manifest", component)
|
|
return None
|
|
|
|
def _get_system_dirs(self) -> Tuple[Path, Path]:
|
|
s64 = Path(self.wineprefix) / 'drive_c' / 'windows' / 'syswow64'
|
|
s32 = Path(self.wineprefix) / 'drive_c' / 'windows' / 'system32'
|
|
s64.mkdir(parents=True, exist_ok=True)
|
|
s32.mkdir(parents=True, exist_ok=True)
|
|
return s64, s32
|
|
|
|
def _register_xact_com(self, dlls: List[Path]) -> None:
|
|
manifest = Path(__file__).parent.parent / 'data' / 'native_components_versions.json'
|
|
try:
|
|
clsid_map = json.loads(manifest.read_text()).get('xact_clsids', {})
|
|
except Exception:
|
|
clsid_map = {}
|
|
for dll_path in dlls:
|
|
for clsid in clsid_map.get(dll_path.name.lower(), []):
|
|
dir_name = dll_path.parent.name.lower()
|
|
win_path = f'"C:\\\\windows\\\\{dir_name}\\\\{dll_path.name}"'
|
|
self._direct_reg_write(
|
|
f'Software\\Classes\\CLSID\\{clsid}\\InprocServer32',
|
|
{'@': win_path, 'ThreadingModel': '"Both"'},
|
|
)
|
|
|
|
def _direct_reg_write(self, key_hkcu: str, values: Dict[str, str]) -> bool:
|
|
"""Write values to user.reg without spawning Wine. Later sections take precedence, so append is correct."""
|
|
user_reg = Path(self.wineprefix) / 'user.reg'
|
|
if not user_reg.is_file():
|
|
self.logger.warning("user.reg not found at %s", user_reg)
|
|
return False
|
|
key_fmted = key_hkcu.replace('\\', '\\\\')
|
|
try:
|
|
with open(user_reg, 'a', encoding='utf-8') as f:
|
|
f.write(f'\n[{key_fmted}] {int(time.time())}\n')
|
|
for name, val in values.items():
|
|
f.write(f'@={val}\n' if name == '@' else f'"{name}"={val}\n')
|
|
return True
|
|
except Exception as exc:
|
|
self.logger.error("Direct registry write failed for %s: %s", key_hkcu, exc)
|
|
return False
|
|
|
|
|