Skip to content

Commit 36c9bee

Browse files
committed
Fix some broken deploy sites
1 parent 59f2b01 commit 36c9bee

6 files changed

Lines changed: 290 additions & 465 deletions

File tree

src/Utils/deploy_pipeline.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""
2+
Shared deploy orchestration used by the Deploy button, Run EXE (Play),
3+
the BodySlide / DynDOLOD wizards, and the CLI.
4+
5+
`run_deploy_pipeline` performs the full restore → build_filemap → deploy →
6+
wine-dll → root-folder → root-flagged → swap_launcher sequence. UI-specific
7+
concerns (button enable/disable, status bar, mod panel reload) stay at the
8+
call site.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import traceback
14+
from pathlib import Path
15+
from typing import Callable, Optional
16+
17+
from Utils.deploy import (
18+
LinkMode,
19+
deploy_root_folder,
20+
deploy_root_flagged_mods,
21+
load_per_mod_strip_prefixes,
22+
restore_root_folder,
23+
)
24+
from Utils.filemap import build_filemap
25+
from Utils.profile_backup import create_backup
26+
from Utils.profile_state import read_excluded_mod_files
27+
from Utils.ui_config import load_normalize_folder_case
28+
from Utils.wine_dll_config import deploy_game_wine_dll_overrides
29+
30+
31+
LogFn = Callable[[str], None]
32+
ProgressFn = Callable[[int, int, Optional[str]], None]
33+
34+
35+
def _build_filemap_for_game(game, profile, *, log_fn: LogFn) -> None:
36+
"""Rebuild filemap.txt + filemap_root.txt for *profile* of *game*.
37+
38+
Mirrors the call in top_bar._run_deploy: pulls excluded-files, root-flagged
39+
mods (Nexus), folder-case normalization toggle, UE5 conflict-key resolver.
40+
Errors are logged but not raised — partial filemap is still useful.
41+
"""
42+
profile_root = game.get_profile_root()
43+
staging = game.get_effective_mod_staging_path()
44+
modlist_path = profile_root / "profiles" / profile / "modlist.txt"
45+
filemap_out = staging.parent / "filemap.txt"
46+
if not modlist_path.is_file():
47+
return
48+
49+
try:
50+
from Nexus.nexus_meta import collect_root_flagged_mods
51+
from Games.ue5_game import UE5Game
52+
53+
exc_raw = read_excluded_mod_files(modlist_path.parent, None)
54+
exc = {k: set(v) for k, v in exc_raw.items()} if exc_raw else None
55+
rf_mods = collect_root_flagged_mods(modlist_path, staging, log_fn=log_fn)
56+
norm_case = (
57+
getattr(game, "normalize_folder_case", True)
58+
and load_normalize_folder_case()
59+
)
60+
if isinstance(game, UE5Game):
61+
def conflict_key_fn(rk: str, _g=game) -> str:
62+
dest, final = _g._resolve_entry(rk)
63+
return (dest + "/" + final) if dest else final
64+
else:
65+
conflict_key_fn = getattr(game, "filemap_conflict_key_fn", None)
66+
67+
build_filemap(
68+
modlist_path, staging, filemap_out,
69+
strip_prefixes=game.mod_folder_strip_prefixes or None,
70+
per_mod_strip_prefixes=load_per_mod_strip_prefixes(modlist_path.parent),
71+
allowed_extensions=game.mod_install_extensions or None,
72+
root_deploy_folders=game.mod_root_deploy_folders or None,
73+
excluded_mod_files=exc,
74+
conflict_ignore_filenames=getattr(game, "conflict_ignore_filenames", None) or None,
75+
exclude_dirs=getattr(game, "filemap_exclude_dirs", None) or None,
76+
normalize_folder_case=norm_case,
77+
filemap_casing=getattr(game, "filemap_casing", "upper"),
78+
conflict_key_fn=conflict_key_fn,
79+
root_folder_mods=rf_mods or None,
80+
)
81+
except Exception as fm_err:
82+
log_fn(f"Filemap rebuild warning: {fm_err}")
83+
84+
85+
def run_deploy_pipeline(
86+
game,
87+
profile: str,
88+
*,
89+
log_fn: LogFn,
90+
progress_fn: Optional[ProgressFn] = None,
91+
root_folder_enabled: bool = True,
92+
confirm_cet: Optional[Callable[[], bool]] = None,
93+
do_backup: bool = True,
94+
on_pre_filemap: Optional[Callable[[], None]] = None,
95+
) -> bool:
96+
"""Run the standard deploy sequence for *game* / *profile*.
97+
98+
Parameters
99+
----------
100+
log_fn / progress_fn
101+
Sinks for human-readable log lines and progress ticks. Callers supply
102+
thread-safe wrappers when invoked from a worker thread.
103+
root_folder_enabled
104+
Honors the Mod List panel's Root_Folder toggle; always True off the GUI.
105+
confirm_cet
106+
Optional blocking confirmation prompt (Cyberpunk CET symlink check).
107+
Return False to abort the deploy. None means "always proceed".
108+
do_backup
109+
If True, run `create_backup` for the profile dir before deploy.
110+
on_pre_filemap
111+
Optional hook fired *after* the profile switch but *before* the
112+
filemap rebuild. Used by wizards (e.g. BodySlide output redirect)
113+
to materialize a placeholder mod that needs to be in the filemap.
114+
115+
Returns True on success, False on user-cancel / error. The active profile
116+
is always reset to *profile* before returning, even on error.
117+
"""
118+
game_root = game.get_game_path()
119+
120+
try:
121+
# Restore against the last-deployed profile so runtime files (saves,
122+
# ShaderCache, etc.) land in *that* profile's overwrite/ folder.
123+
last_deployed = game.get_last_deployed_profile()
124+
if last_deployed:
125+
game.set_active_profile_dir(
126+
game.get_profile_root() / "profiles" / last_deployed
127+
)
128+
if getattr(game, "restore_before_deploy", True) and hasattr(game, "restore"):
129+
try:
130+
if progress_fn is not None:
131+
game.restore(log_fn=log_fn, progress_fn=progress_fn)
132+
else:
133+
game.restore(log_fn=log_fn)
134+
except RuntimeError:
135+
pass
136+
last_root_folder_dir = game.get_effective_root_folder_path()
137+
if last_root_folder_dir.is_dir() and game_root:
138+
restore_root_folder(last_root_folder_dir, game_root, log_fn=log_fn)
139+
140+
# Switch to the target profile before filemap + deploy.
141+
game.set_active_profile_dir(
142+
game.get_profile_root() / "profiles" / profile
143+
)
144+
145+
if on_pre_filemap is not None:
146+
on_pre_filemap()
147+
148+
_build_filemap_for_game(game, profile, log_fn=log_fn)
149+
150+
if confirm_cet is not None and not confirm_cet():
151+
log_fn("Deploy: cancelled — CET requires Hardlink mode.")
152+
return False
153+
154+
profile_dir = game.get_profile_root() / "profiles" / profile
155+
if do_backup:
156+
try:
157+
create_backup(profile_dir, log_fn)
158+
except Exception as backup_err:
159+
log_fn(f"Backup skipped: {backup_err}")
160+
161+
deploy_mode = (
162+
game.get_deploy_mode()
163+
if hasattr(game, "get_deploy_mode")
164+
else LinkMode.HARDLINK
165+
)
166+
if progress_fn is not None:
167+
game.deploy(log_fn=log_fn, profile=profile, progress_fn=progress_fn,
168+
mode=deploy_mode)
169+
else:
170+
game.deploy(log_fn=log_fn, profile=profile, mode=deploy_mode)
171+
172+
pfx = game.get_prefix_path()
173+
if pfx and pfx.is_dir():
174+
deploy_game_wine_dll_overrides(
175+
game.name, pfx, game.wine_dll_overrides, log_fn=log_fn
176+
)
177+
178+
game.save_last_deployed_profile(profile)
179+
180+
target_rf = game.get_effective_root_folder_path()
181+
rf_allowed = getattr(game, "root_folder_deploy_enabled", True)
182+
183+
# Step A: shared Root_Folder must run first — its log file is what
184+
# Step B's root-flagged-mods deploy merges into.
185+
if rf_allowed and root_folder_enabled and target_rf.is_dir() and game_root:
186+
count = deploy_root_folder(
187+
target_rf, game_root, mode=deploy_mode, log_fn=log_fn
188+
)
189+
if count:
190+
log_fn("Root Folder: transferred files to game root.")
191+
192+
if game_root:
193+
filemap_root_path = (
194+
game.get_effective_filemap_path().parent / "filemap_root.txt"
195+
)
196+
staging = game.get_effective_mod_staging_path()
197+
strip = getattr(game, "mod_folder_strip_prefixes", None)
198+
per_mod_strip = load_per_mod_strip_prefixes(profile_dir)
199+
rf_count = deploy_root_flagged_mods(
200+
filemap_root_path, game_root, staging,
201+
mode=deploy_mode, strip_prefixes=strip,
202+
per_mod_strip_prefixes=per_mod_strip or None,
203+
log_fn=log_fn,
204+
)
205+
if rf_count:
206+
log_fn(f"Root-flagged mods: {rf_count} file(s) deployed to game root.")
207+
208+
# Launcher swap last so SE/SKSE/etc. dlls are present first.
209+
if hasattr(game, "swap_launcher"):
210+
game.swap_launcher(log_fn)
211+
212+
return True
213+
except Exception as e:
214+
log_fn(f"Deploy error: {e}\n{traceback.format_exc()}")
215+
return False
216+
finally:
217+
game.set_active_profile_dir(
218+
game.get_profile_root() / "profiles" / profile
219+
)

src/cli.py

Lines changed: 5 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -115,108 +115,16 @@ def cmd_deploy(games: dict, key: str, profile: str):
115115
print(f"Error: game '{game.name}' is not configured (game path not set).", file=sys.stderr)
116116
sys.exit(1)
117117

118-
from Utils.deploy import deploy_root_folder, restore_root_folder, LinkMode, load_per_mod_strip_prefixes, deploy_root_flagged_mods
119-
from Utils.filemap import build_filemap
120-
from Utils.profile_backup import create_backup
121-
from Utils.ui_config import load_normalize_folder_case as _load_norm_case
118+
from Utils.deploy_pipeline import run_deploy_pipeline
122119

123-
profile_root = game.get_profile_root()
124-
profile_dir = profile_root / "profiles" / profile
120+
profile_dir = game.get_profile_root() / "profiles" / profile
125121
if not profile_dir.is_dir():
126122
print(f"Error: profile '{profile}' does not exist at {profile_dir}", file=sys.stderr)
127123
sys.exit(1)
128124

129-
game_root = game.get_game_path()
130-
131-
# Restore using the last-deployed profile first
132-
last_deployed = game.get_last_deployed_profile()
133-
if last_deployed:
134-
game.set_active_profile_dir(profile_root / "profiles" / last_deployed)
135-
if getattr(game, "restore_before_deploy", True) and hasattr(game, "restore"):
136-
try:
137-
game.restore(log_fn=_log)
138-
except RuntimeError:
139-
pass
140-
# Restore Root_Folder for the last-deployed profile
141-
last_root_folder_dir = game.get_effective_root_folder_path()
142-
if last_root_folder_dir.is_dir() and game_root:
143-
restore_root_folder(last_root_folder_dir, game_root, log_fn=_log)
144-
145-
# Switch to target profile
146-
game.set_active_profile_dir(profile_dir)
147-
148-
# Rebuild filemap
149-
staging = game.get_effective_mod_staging_path()
150-
modlist_path = profile_dir / "modlist.txt"
151-
filemap_out = staging.parent / "filemap.txt"
152-
if modlist_path.is_file():
153-
try:
154-
from Utils.profile_state import read_excluded_mod_files as _read_exc
155-
from Nexus.nexus_meta import collect_root_flagged_mods as _collect_rf
156-
_exc_raw = _read_exc(profile_dir, None)
157-
_exc = {k: set(v) for k, v in _exc_raw.items()} if _exc_raw else None
158-
_rf_mods = _collect_rf(modlist_path, staging, log_fn=_log)
159-
build_filemap(
160-
modlist_path, staging, filemap_out,
161-
strip_prefixes=game.mod_folder_strip_prefixes or None,
162-
per_mod_strip_prefixes=load_per_mod_strip_prefixes(profile_dir),
163-
allowed_extensions=game.mod_install_extensions or None,
164-
root_deploy_folders=game.mod_root_deploy_folders or None,
165-
excluded_mod_files=_exc,
166-
conflict_ignore_filenames=getattr(game, "conflict_ignore_filenames", None) or None,
167-
normalize_folder_case=getattr(game, "normalize_folder_case", True) and _load_norm_case(),
168-
filemap_casing=getattr(game, "filemap_casing", "upper"),
169-
conflict_key_fn=getattr(game, "filemap_conflict_key_fn", None),
170-
exclude_dirs=getattr(game, "filemap_exclude_dirs", None) or None,
171-
root_folder_mods=_rf_mods or None,
172-
)
173-
except Exception as fm_err:
174-
_log(f"Filemap rebuild warning: {fm_err}")
175-
176-
# Backup before deploy
177-
try:
178-
create_backup(profile_dir, _log)
179-
except Exception as backup_err:
180-
_log(f"Backup skipped: {backup_err}")
181-
182-
# Deploy mods
183-
deploy_mode = game.get_deploy_mode() if hasattr(game, "get_deploy_mode") else LinkMode.HARDLINK
184-
game.deploy(log_fn=_log, profile=profile, mode=deploy_mode)
185-
186-
# Apply Wine DLL overrides (user-added + handler-defined)
187-
from Utils.wine_dll_config import deploy_game_wine_dll_overrides
188-
_pfx = game.get_prefix_path()
189-
if _pfx and _pfx.is_dir():
190-
deploy_game_wine_dll_overrides(game.name, _pfx, game.wine_dll_overrides, log_fn=_log)
191-
192-
game.save_last_deployed_profile(profile)
193-
194-
# Deploy Root_Folder
195-
target_root_folder_dir = game.get_effective_root_folder_path()
196-
rf_allowed = getattr(game, "root_folder_deploy_enabled", True)
197-
198-
# Step A: shared Root_Folder first so its log exists before root-flagged mods run.
199-
if rf_allowed and target_root_folder_dir.is_dir() and game_root:
200-
count = deploy_root_folder(target_root_folder_dir, game_root,
201-
mode=deploy_mode, log_fn=_log)
202-
if count:
203-
_log("Root Folder: transferred files to game root.")
204-
205-
# Step B: root-flagged mods, merges into Step A log (Root_Folder wins conflicts).
206-
if game_root:
207-
_filemap_root_path = staging.parent / "filemap_root.txt"
208-
_strip = getattr(game, "mod_folder_strip_prefixes", None)
209-
_rfc = deploy_root_flagged_mods(
210-
_filemap_root_path, game_root, staging,
211-
mode=deploy_mode, strip_prefixes=_strip,
212-
per_mod_strip_prefixes=load_per_mod_strip_prefixes(profile_dir) or None,
213-
log_fn=_log,
214-
)
215-
if _rfc:
216-
_log(f"Root-flagged mods: {_rfc} file(s) deployed to game root.")
217-
218-
if hasattr(game, "swap_launcher"):
219-
game.swap_launcher(_log)
125+
success = run_deploy_pipeline(game, profile, log_fn=_log)
126+
if not success:
127+
sys.exit(1)
220128

221129
_log(f"Deploy complete: {game.name} / {profile}")
222130

0 commit comments

Comments
 (0)