Use offline bundles for 3rd party tools for security
This commit is contained in:
parent
1fb7a21bdd
commit
9ad65488da
8 changed files with 442 additions and 327 deletions
|
|
@ -4,7 +4,7 @@ An AI-powered game translation tool with a GUI. Translate RPG Maker, Ren'Py, Tyr
|
|||
|
||||
## Credits
|
||||
|
||||
- **[Sinflower](https://github.com/Sinflower)** — [RV2JSON](https://github.com/Sinflower/RV2JSON) — enables RPGMaker Ace games to be translated the same way as MV/MZ by converting rvdata2 files to JSON and back. A copy is bundled offline in `util/ace/offline/`; newer builds are downloaded when online.
|
||||
- **[Sinflower](https://github.com/Sinflower)** — [RV2JSON](https://github.com/Sinflower/RV2JSON) — enables RPGMaker Ace games to be translated the same way as MV/MZ by converting rvdata2 files to JSON and back. A curated copy is bundled offline in `util/ace/offline/` and updates with DazedMTLTool.
|
||||
- **Sakura & Kao_SSS** — TL Inspector (`util/tl_inspector/`) — in-game translation source inspector and live-edit plugin for RPG Maker MV/MZ playtesting.
|
||||
- **Len** — [Forge](https://gitgud.io/zero64801/forge-mvmz) MV/MZ playtest plugin (`util/forge/`), Mistral API support (provider integration and adaptive rate limiting), and batch translation mode (Anthropic Message Batches API).
|
||||
|
||||
|
|
@ -333,7 +333,7 @@ Open the **Workflow** tab and choose **Wolf RPG (WolfDawn)** from the engine sel
|
|||
| **6 Package** | Run from loose `Data/` (backs up `Data.wolf` → `.bak`) or repack `Data.wolf`. |
|
||||
| **7 Saves** | *(Optional)* Update existing `.sav` files for the translated build. |
|
||||
|
||||
> **`wolf` binary:** Prebuilt `wolf` CLIs for Windows and Linux are bundled offline under `util/wolfdawn/bin/<platform>/`, so no toolchain or build step is needed. If your platform's binary is missing, the tool downloads a prebuilt one from the [WolfDawn release page](https://gitgud.io/zero64801/wolfdawn) and caches it into that same folder for offline reuse. A clear error is shown if no bundled binary is present and the download can't be completed (e.g. no published binary for your platform).
|
||||
> **`wolf` binary:** Prebuilt `wolf` CLIs for Windows and Linux are bundled offline under `util/wolfdawn/bin/<platform>/`, so no toolchain or build step is needed. They update when you update DazedMTLTool. If your platform's binary is missing, update the tool or ask the maintainer to refresh the bundled copy.
|
||||
|
||||
> **Legacy modules:** The older `Wolf RPG` / `Wolf RPG 2` modules (configured in the Engine Config tab) still exist for edge cases, but the WolfDawn workflow above is the recommended path.
|
||||
|
||||
|
|
|
|||
199
gui/main.py
199
gui/main.py
|
|
@ -55,87 +55,13 @@ def check_tool_update() -> str | None:
|
|||
return None
|
||||
|
||||
|
||||
def check_all_updates() -> dict:
|
||||
"""Check tool, Ace, Forge, and WolfDawn for available updates."""
|
||||
results = {"tool": None, "ace": False, "forge": False, "wolf": False}
|
||||
results["tool"] = check_tool_update()
|
||||
try:
|
||||
from util.ace.update_tools import check_ace_tools_update
|
||||
results["ace"] = check_ace_tools_update()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from util.forge.update_tools import check_forge_plugins_update
|
||||
results["forge"] = check_forge_plugins_update()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from util.wolfdawn.update_tools import check_wolfdawn_update
|
||||
results["wolf"] = check_wolfdawn_update()
|
||||
except Exception:
|
||||
pass
|
||||
return results
|
||||
|
||||
|
||||
def any_updates_available(results: dict) -> bool:
|
||||
return (
|
||||
bool(results.get("tool"))
|
||||
or results.get("ace")
|
||||
or results.get("forge")
|
||||
or results.get("wolf")
|
||||
)
|
||||
|
||||
|
||||
class BackgroundUpdateCheckThread(QThread):
|
||||
"""Checks for tool, Ace, Forge, and WolfDawn updates without blocking the UI."""
|
||||
"""Checks for DazedMTLTool updates without blocking the UI."""
|
||||
|
||||
finished = pyqtSignal(dict)
|
||||
finished = pyqtSignal(object) # str | None — pending tool SHA when available
|
||||
|
||||
def run(self):
|
||||
self.finished.emit(check_all_updates())
|
||||
|
||||
|
||||
class AuxToolsUpdateThread(QThread):
|
||||
"""Download Ace, Forge, and/or WolfDawn updates without updating the main tool."""
|
||||
|
||||
progress = pyqtSignal(str)
|
||||
finished = pyqtSignal(bool, str)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
update_ace: bool = False,
|
||||
update_forge: bool = False,
|
||||
update_wolf: bool = False,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.update_ace = update_ace
|
||||
self.update_forge = update_forge
|
||||
self.update_wolf = update_wolf
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
if self.update_ace:
|
||||
from util.ace.update_tools import ensure_ace_tools
|
||||
self.progress.emit("Updating RPG Maker Ace tools…")
|
||||
if not ensure_ace_tools(log_fn=lambda m: self.progress.emit(m)):
|
||||
self.finished.emit(False, "Ace tool update failed.")
|
||||
return
|
||||
if self.update_forge:
|
||||
from util.forge.update_tools import ensure_forge_plugins
|
||||
self.progress.emit("Updating Forge plugins…")
|
||||
if not ensure_forge_plugins(force=True, log_fn=lambda m: self.progress.emit(m)):
|
||||
self.finished.emit(False, "Forge plugin update failed.")
|
||||
return
|
||||
if self.update_wolf:
|
||||
from util.wolfdawn.update_tools import ensure_wolfdawn_binary
|
||||
self.progress.emit("Updating WolfDawn…")
|
||||
if not ensure_wolfdawn_binary(force=True, log_fn=lambda m: self.progress.emit(m)):
|
||||
self.finished.emit(False, "WolfDawn update failed.")
|
||||
return
|
||||
self.finished.emit(True, "components_updated")
|
||||
except Exception as exc:
|
||||
self.finished.emit(False, str(exc))
|
||||
self.finished.emit(check_tool_update())
|
||||
|
||||
|
||||
class UpdateThread(QThread):
|
||||
|
|
@ -157,9 +83,8 @@ class UpdateThread(QThread):
|
|||
progress = pyqtSignal(str) # status message
|
||||
finished = pyqtSignal(bool, str) # (success, message)
|
||||
|
||||
def __init__(self, check_only=False, parent=None):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.check_only = check_only
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
|
|
@ -172,10 +97,6 @@ class UpdateThread(QThread):
|
|||
self.finished.emit(True, "already_up_to_date")
|
||||
return
|
||||
|
||||
if self.check_only:
|
||||
self.finished.emit(True, f"update_available:{latest_sha}")
|
||||
return
|
||||
|
||||
self._download_and_apply(latest_sha)
|
||||
|
||||
except Exception as exc:
|
||||
|
|
@ -249,31 +170,13 @@ class UpdateThread(QThread):
|
|||
dst.chmod(mode)
|
||||
|
||||
Path(self.SHA_FILE).write_text(latest_sha)
|
||||
try:
|
||||
from util.ace.update_tools import ensure_ace_tools
|
||||
self.progress.emit("Updating RPG Maker Ace tools…")
|
||||
ensure_ace_tools(log_fn=lambda m: self.progress.emit(m))
|
||||
except Exception as exc:
|
||||
self.progress.emit(f"Ace tool update skipped: {exc}")
|
||||
try:
|
||||
from util.forge.update_tools import ensure_forge_plugins
|
||||
self.progress.emit("Updating Forge plugins…")
|
||||
ensure_forge_plugins(log_fn=lambda m: self.progress.emit(m))
|
||||
except Exception as exc:
|
||||
self.progress.emit(f"Forge update skipped: {exc}")
|
||||
try:
|
||||
from util.wolfdawn.update_tools import ensure_wolfdawn_binary
|
||||
self.progress.emit("Updating WolfDawn…")
|
||||
ensure_wolfdawn_binary(log_fn=lambda m: self.progress.emit(m))
|
||||
except Exception as exc:
|
||||
self.progress.emit(f"WolfDawn update skipped: {exc}")
|
||||
self.finished.emit(True, f"updated:{latest_sha[:8]}")
|
||||
|
||||
|
||||
class UpdateDialog(QDialog):
|
||||
"""Modal dialog that shows update progress and result."""
|
||||
|
||||
def __init__(self, parent=None, pending_updates: dict | None = None):
|
||||
def __init__(self, parent=None, pending_tool_sha: str | None = None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("Tool Update")
|
||||
self.setMinimumWidth(420)
|
||||
|
|
@ -295,10 +198,10 @@ class UpdateDialog(QDialog):
|
|||
layout.addWidget(self.close_btn)
|
||||
|
||||
self._thread = None
|
||||
self._pending = pending_updates or {}
|
||||
self._pending_tool_sha = pending_tool_sha
|
||||
|
||||
def start(self, check_only=False):
|
||||
if self._pending:
|
||||
def start(self):
|
||||
if self._pending_tool_sha:
|
||||
self._show_pending_updates()
|
||||
return
|
||||
|
||||
|
|
@ -306,44 +209,28 @@ class UpdateDialog(QDialog):
|
|||
self._thread.finished.connect(self._on_check_finished)
|
||||
self._thread.start()
|
||||
|
||||
def _update_labels(self) -> list[str]:
|
||||
labels = []
|
||||
if self._pending.get("tool"):
|
||||
labels.append("DazedMTLTool")
|
||||
if self._pending.get("ace"):
|
||||
labels.append("RPG Maker Ace tools")
|
||||
if self._pending.get("forge"):
|
||||
labels.append("Forge plugins")
|
||||
if self._pending.get("wolf"):
|
||||
labels.append("WolfDawn")
|
||||
return labels
|
||||
|
||||
def _show_pending_updates(self):
|
||||
self.progress_bar.setRange(0, 1)
|
||||
self.progress_bar.setValue(1)
|
||||
self.close_btn.setText("Close")
|
||||
|
||||
labels = self._update_labels()
|
||||
if not labels:
|
||||
tool_sha = self._pending_tool_sha
|
||||
if not tool_sha:
|
||||
self.status_label.setText("✅ Already up to date.")
|
||||
return
|
||||
|
||||
detail = "\n".join(f" • {label}" for label in labels)
|
||||
tool_sha = self._pending.get("tool")
|
||||
if tool_sha:
|
||||
detail = f" • DazedMTLTool ({tool_sha[:8]})\n" + "\n".join(
|
||||
f" • {label}" for label in labels if label != "DazedMTLTool"
|
||||
)
|
||||
self.status_label.setText(
|
||||
f"🆕 Updates available:\n{detail}\n\nClick 'Update Now' to install."
|
||||
f"🆕 Update available:\n"
|
||||
f" • DazedMTLTool ({tool_sha[:8]})\n\n"
|
||||
f"Click 'Update Now' to install."
|
||||
)
|
||||
self.close_btn.setText("Cancel")
|
||||
update_btn = QPushButton("Update Now")
|
||||
update_btn.clicked.connect(self._do_update)
|
||||
self.layout().insertWidget(self.layout().count() - 1, update_btn)
|
||||
|
||||
def _on_check_finished(self, results: dict):
|
||||
self._pending = results
|
||||
def _on_check_finished(self, tool_sha: str | None):
|
||||
self._pending_tool_sha = tool_sha
|
||||
self._show_pending_updates()
|
||||
|
||||
def _on_finished(self, success, message):
|
||||
|
|
@ -357,23 +244,9 @@ class UpdateDialog(QDialog):
|
|||
|
||||
if message == "already_up_to_date":
|
||||
self.status_label.setText("✅ Already up to date.")
|
||||
elif message == "components_updated":
|
||||
self._pending = {"tool": None, "ace": False, "forge": False, "wolf": False}
|
||||
self.status_label.setText("✅ Component updates installed.")
|
||||
if isinstance(self.parent(), DazedMTLGUI):
|
||||
self.parent().clear_update_indicator()
|
||||
elif message.startswith("update_available:"):
|
||||
sha = message.split(":", 1)[1]
|
||||
self.status_label.setText(
|
||||
f"🆕 Update available ({sha[:8]}).\nClick 'Update Now' to install."
|
||||
)
|
||||
self.close_btn.setText("Cancel")
|
||||
update_btn = QPushButton("Update Now")
|
||||
update_btn.clicked.connect(self._do_update)
|
||||
self.layout().insertWidget(self.layout().count() - 1, update_btn)
|
||||
elif message.startswith("updated:"):
|
||||
sha = message.split(":", 1)[1]
|
||||
self._pending = {"tool": None, "ace": False, "forge": False, "wolf": False}
|
||||
self._pending_tool_sha = None
|
||||
self.status_label.setText(
|
||||
f"✅ Updated to {sha}.\n\nPlease restart the tool for changes to take effect."
|
||||
)
|
||||
|
|
@ -395,25 +268,9 @@ class UpdateDialog(QDialog):
|
|||
self.progress_bar.setRange(0, 0)
|
||||
self.close_btn.setText("Cancel")
|
||||
|
||||
if self._pending.get("tool"):
|
||||
if self._pending_tool_sha:
|
||||
self.status_label.setText("Downloading update…")
|
||||
self._thread = UpdateThread(check_only=False, parent=self)
|
||||
self._thread.progress.connect(self.status_label.setText)
|
||||
self._thread.finished.connect(self._on_finished)
|
||||
self._thread.start()
|
||||
return
|
||||
|
||||
update_ace = bool(self._pending.get("ace"))
|
||||
update_forge = bool(self._pending.get("forge"))
|
||||
update_wolf = bool(self._pending.get("wolf"))
|
||||
if update_ace or update_forge or update_wolf:
|
||||
self.status_label.setText("Downloading component updates…")
|
||||
self._thread = AuxToolsUpdateThread(
|
||||
update_ace=update_ace,
|
||||
update_forge=update_forge,
|
||||
update_wolf=update_wolf,
|
||||
parent=self,
|
||||
)
|
||||
self._thread = UpdateThread(parent=self)
|
||||
self._thread.progress.connect(self.status_label.setText)
|
||||
self._thread.finished.connect(self._on_finished)
|
||||
self._thread.start()
|
||||
|
|
@ -435,7 +292,7 @@ class DazedMTLGUI(QMainWindow):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
self.settings = QSettings("DazedTranslations", "DazedMTLTool")
|
||||
self._pending_updates: dict = {}
|
||||
self._pending_tool_sha: str | None = None
|
||||
self._update_check_thread = None
|
||||
self._shutdown_started = False
|
||||
self._update_icon = "🔄"
|
||||
|
|
@ -780,16 +637,16 @@ class DazedMTLGUI(QMainWindow):
|
|||
return container
|
||||
|
||||
def start_background_update_check(self):
|
||||
"""Check for tool, Ace, and Forge updates after the GUI is visible."""
|
||||
"""Check for DazedMTLTool updates after the GUI is visible."""
|
||||
if self._update_check_thread and self._update_check_thread.isRunning():
|
||||
return
|
||||
self._update_check_thread = BackgroundUpdateCheckThread(self)
|
||||
self._update_check_thread.finished.connect(self._on_background_update_check)
|
||||
self._update_check_thread.start()
|
||||
|
||||
def _on_background_update_check(self, results: dict):
|
||||
self._pending_updates = results
|
||||
if any_updates_available(results):
|
||||
def _on_background_update_check(self, tool_sha: str | None):
|
||||
self._pending_tool_sha = tool_sha
|
||||
if tool_sha:
|
||||
self.set_update_indicator()
|
||||
else:
|
||||
self.clear_update_indicator()
|
||||
|
|
@ -806,7 +663,7 @@ class DazedMTLGUI(QMainWindow):
|
|||
|
||||
def clear_update_indicator(self):
|
||||
"""Restore the default update button appearance."""
|
||||
self._pending_updates = {}
|
||||
self._pending_tool_sha = None
|
||||
if not self.btn_update:
|
||||
return
|
||||
self.btn_update.setToolTip("Check for Updates")
|
||||
|
|
@ -817,11 +674,11 @@ class DazedMTLGUI(QMainWindow):
|
|||
|
||||
def show_update_dialog(self):
|
||||
"""Open the update dialog and check for a newer version."""
|
||||
dlg = UpdateDialog(self, pending_updates=self._pending_updates.copy())
|
||||
dlg.start(check_only=True)
|
||||
dlg = UpdateDialog(self, pending_tool_sha=self._pending_tool_sha)
|
||||
dlg.start()
|
||||
dlg.exec_()
|
||||
self._pending_updates = dlg._pending
|
||||
if any_updates_available(dlg._pending):
|
||||
self._pending_tool_sha = dlg._pending_tool_sha
|
||||
if self._pending_tool_sha:
|
||||
self.set_update_indicator()
|
||||
else:
|
||||
self.clear_update_indicator()
|
||||
|
|
|
|||
312
tests/test_bundled_updates.py
Normal file
312
tests/test_bundled_updates.py
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
"""Integration tests for bundled-only utility updates and tool-only auto-update checks.
|
||||
|
||||
These tests use temporary directories and mocks — no live network or GUI window.
|
||||
They guard the security model: third-party utilities must not fetch upstream unless
|
||||
a maintainer explicitly uses --force / --refresh-offline / --refresh-all.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from util.wolfdawn import WolfDawnError, bundled_binary_path, ensure_wolf_binary
|
||||
|
||||
|
||||
class CheckToolUpdateTests(unittest.TestCase):
|
||||
"""Tool update check compares remote SHA to data/last_update_sha.txt only."""
|
||||
|
||||
def test_returns_sha_when_remote_differs(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
sha_file = Path(raw) / "last_update_sha.txt"
|
||||
sha_file.write_text("aaaaaaaa", encoding="utf-8")
|
||||
from gui.main import UpdateThread, check_tool_update
|
||||
|
||||
with patch.object(UpdateThread, "SHA_FILE", str(sha_file)):
|
||||
with patch.object(
|
||||
UpdateThread, "fetch_latest_sha", return_value="bbbbbbbb"
|
||||
):
|
||||
self.assertEqual(check_tool_update(), "bbbbbbbb")
|
||||
|
||||
def test_returns_none_when_up_to_date(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
sha_file = Path(raw) / "last_update_sha.txt"
|
||||
sha_file.write_text("same1234", encoding="utf-8")
|
||||
from gui.main import UpdateThread, check_tool_update
|
||||
|
||||
with patch.object(UpdateThread, "SHA_FILE", str(sha_file)):
|
||||
with patch.object(
|
||||
UpdateThread, "fetch_latest_sha", return_value="same1234"
|
||||
):
|
||||
self.assertIsNone(check_tool_update())
|
||||
|
||||
def test_returns_none_on_fetch_failure(self):
|
||||
from gui.main import UpdateThread, check_tool_update
|
||||
|
||||
with patch.object(
|
||||
UpdateThread, "fetch_latest_sha", side_effect=OSError("offline")
|
||||
):
|
||||
self.assertIsNone(check_tool_update())
|
||||
|
||||
|
||||
class AceBundledOnlyTests(unittest.TestCase):
|
||||
def _ace_paths(self, base: Path) -> dict:
|
||||
offline = base / "offline"
|
||||
offline.mkdir(parents=True, exist_ok=True)
|
||||
return {
|
||||
"OFFLINE_DIR": offline,
|
||||
"RV2JSON_LOCAL": base / "RV2JSON.exe",
|
||||
"DECRYPTER_LOCAL": base / "RPGMakerDecrypter-cli.exe",
|
||||
"DECRYPTER_LEGACY": base / "RPGMakerDecrypter.exe",
|
||||
"VERSION_FILE": base / ".tools_version.json",
|
||||
}
|
||||
|
||||
def test_seed_overwrites_stale_runtime_copy(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
paths = self._ace_paths(base)
|
||||
(paths["OFFLINE_DIR"] / "RV2JSON.exe").write_bytes(b"OFFLINE")
|
||||
paths["RV2JSON_LOCAL"].write_bytes(b"STALE")
|
||||
|
||||
import util.ace.update_tools as ace
|
||||
|
||||
with patch.multiple("util.ace.update_tools", **paths):
|
||||
ace._seed_from_offline(paths["RV2JSON_LOCAL"], "RV2JSON.exe", None)
|
||||
self.assertEqual(paths["RV2JSON_LOCAL"].read_bytes(), b"OFFLINE")
|
||||
|
||||
def test_ensure_rv2json_does_not_contact_upstream(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
paths = self._ace_paths(base)
|
||||
(paths["OFFLINE_DIR"] / "RV2JSON.exe").write_bytes(b"OK")
|
||||
|
||||
import util.ace.update_tools as ace
|
||||
|
||||
with patch.multiple("util.ace.update_tools", **paths):
|
||||
with patch.object(
|
||||
ace,
|
||||
"_rv2json_upstream",
|
||||
side_effect=AssertionError("must not fetch upstream"),
|
||||
):
|
||||
self.assertTrue(ace.ensure_rv2json(force=False))
|
||||
self.assertEqual(paths["RV2JSON_LOCAL"].read_bytes(), b"OK")
|
||||
|
||||
def test_ensure_rv2json_fails_when_nothing_bundled(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
paths = self._ace_paths(base)
|
||||
|
||||
import util.ace.update_tools as ace
|
||||
|
||||
with patch.multiple("util.ace.update_tools", **paths):
|
||||
with patch.object(
|
||||
ace,
|
||||
"_rv2json_upstream",
|
||||
side_effect=AssertionError("must not fetch upstream"),
|
||||
):
|
||||
self.assertFalse(ace.ensure_rv2json(force=False))
|
||||
|
||||
def test_force_delegates_to_upstream_fetch(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
paths = self._ace_paths(base)
|
||||
|
||||
import util.ace.update_tools as ace
|
||||
|
||||
with patch.multiple("util.ace.update_tools", **paths):
|
||||
with patch.object(ace, "_fetch_rv2json_upstream", return_value=True) as fetch:
|
||||
self.assertTrue(ace.ensure_rv2json(force=True))
|
||||
fetch.assert_called_once()
|
||||
|
||||
|
||||
class ForgeBundledOnlyTests(unittest.TestCase):
|
||||
def _forge_paths(self, base: Path, *, with_plugins: bool) -> dict:
|
||||
upstream = base / "upstream"
|
||||
upstream.mkdir(parents=True, exist_ok=True)
|
||||
plugin_body = b"// forge plugin\n"
|
||||
paths = {
|
||||
"_PKG_ROOT": base,
|
||||
"UPSTREAM_DIR": upstream,
|
||||
"VERSION_FILE": base / ".forge_version.json",
|
||||
}
|
||||
if with_plugins:
|
||||
for name in ("Forge_MV.js", "Forge_MZ.js"):
|
||||
(base / name).write_bytes(plugin_body)
|
||||
(upstream / name).write_bytes(plugin_body)
|
||||
return paths
|
||||
|
||||
def test_ensure_forge_plugins_does_not_contact_upstream(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
paths = self._forge_paths(base, with_plugins=True)
|
||||
|
||||
import util.forge.update_tools as forge
|
||||
|
||||
with patch.multiple("util.forge.update_tools", **paths):
|
||||
with patch.object(
|
||||
forge,
|
||||
"_upstream_commit",
|
||||
side_effect=AssertionError("must not fetch upstream"),
|
||||
):
|
||||
with patch.object(
|
||||
forge,
|
||||
"refresh_forge_plugins",
|
||||
side_effect=AssertionError("must not refresh upstream"),
|
||||
):
|
||||
self.assertTrue(forge.ensure_forge_plugins(force=False))
|
||||
|
||||
def test_ensure_forge_plugins_seeds_from_upstream_folder(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
paths = self._forge_paths(base, with_plugins=False)
|
||||
(paths["UPSTREAM_DIR"] / "Forge_MV.js").write_bytes(b"// mv")
|
||||
(paths["UPSTREAM_DIR"] / "Forge_MZ.js").write_bytes(b"// mz")
|
||||
|
||||
import util.forge.update_tools as forge
|
||||
|
||||
with patch.multiple("util.forge.update_tools", **paths):
|
||||
self.assertTrue(forge.ensure_forge_plugins(force=False))
|
||||
self.assertTrue((base / "Forge_MV.js").is_file())
|
||||
self.assertTrue((base / "Forge_MZ.js").is_file())
|
||||
|
||||
def test_ensure_forge_plugins_fails_when_missing_everywhere(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
paths = self._forge_paths(base, with_plugins=False)
|
||||
|
||||
import util.forge.update_tools as forge
|
||||
|
||||
with patch.multiple("util.forge.update_tools", **paths):
|
||||
self.assertFalse(forge.ensure_forge_plugins(force=False))
|
||||
|
||||
def test_force_calls_refresh_forge_plugins(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
paths = self._forge_paths(base, with_plugins=True)
|
||||
|
||||
import util.forge.update_tools as forge
|
||||
|
||||
with patch.multiple("util.forge.update_tools", **paths):
|
||||
with patch.object(forge, "refresh_forge_plugins", return_value=True) as refresh:
|
||||
self.assertTrue(forge.ensure_forge_plugins(force=True))
|
||||
refresh.assert_called_once()
|
||||
|
||||
|
||||
class WolfBundledOnlyTests(unittest.TestCase):
|
||||
def test_ensure_wolf_binary_uses_bundled_path(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
wolf = Path(raw) / "wolf"
|
||||
wolf.write_bytes(b"\x7fELF")
|
||||
with patch("util.wolfdawn.bundled_binary_path", return_value=wolf):
|
||||
self.assertEqual(ensure_wolf_binary(), wolf)
|
||||
|
||||
def test_ensure_wolf_binary_raises_when_missing(self):
|
||||
missing = Path("/nonexistent/wolf/missing")
|
||||
with patch("util.wolfdawn.bundled_binary_path", return_value=missing):
|
||||
with self.assertRaises(WolfDawnError) as ctx:
|
||||
ensure_wolf_binary()
|
||||
self.assertIn("Update DazedMTLTool", str(ctx.exception))
|
||||
|
||||
def test_ensure_wolf_binary_never_downloads(self):
|
||||
missing = Path("/nonexistent/wolf/missing")
|
||||
with patch("util.wolfdawn.bundled_binary_path", return_value=missing):
|
||||
with patch(
|
||||
"util.wolfdawn.download_wolf_binary",
|
||||
side_effect=AssertionError("must not download at runtime"),
|
||||
):
|
||||
with self.assertRaises(WolfDawnError):
|
||||
ensure_wolf_binary()
|
||||
|
||||
def test_ensure_wolfdawn_binary_does_not_refresh_by_default(self):
|
||||
import util.wolfdawn.update_tools as wd_update
|
||||
|
||||
wolf = bundled_binary_path()
|
||||
if not wolf.is_file():
|
||||
self.skipTest(f"no bundled wolf binary at {wolf}")
|
||||
|
||||
with patch.object(
|
||||
wd_update,
|
||||
"refresh_wolfdawn_binary",
|
||||
side_effect=AssertionError("must not refresh upstream"),
|
||||
):
|
||||
self.assertTrue(wd_update.ensure_wolfdawn_binary(force=False))
|
||||
|
||||
def test_ensure_wolfdawn_binary_force_calls_refresh(self):
|
||||
import util.wolfdawn.update_tools as wd_update
|
||||
|
||||
with patch.object(wd_update, "refresh_wolfdawn_binary", return_value=True) as refresh:
|
||||
with patch.object(wd_update, "bundled_binary_path", return_value=Path("/wolf")):
|
||||
with patch("pathlib.Path.is_file", return_value=True):
|
||||
self.assertTrue(wd_update.ensure_wolfdawn_binary(force=True))
|
||||
refresh.assert_called_once()
|
||||
|
||||
|
||||
class MaintainerRefreshTests(unittest.TestCase):
|
||||
"""Maintainer CLI paths should still wire upstream fetch when explicitly invoked."""
|
||||
|
||||
def test_refresh_offline_bundle_downloads_into_offline_dir(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
offline = base / "offline"
|
||||
offline.mkdir()
|
||||
|
||||
import util.ace.update_tools as ace
|
||||
|
||||
paths = {
|
||||
"OFFLINE_DIR": offline,
|
||||
"RV2JSON_LOCAL": base / "RV2JSON.exe",
|
||||
"DECRYPTER_LOCAL": base / "RPGMakerDecrypter-cli.exe",
|
||||
"DECRYPTER_LEGACY": base / "RPGMakerDecrypter.exe",
|
||||
"VERSION_FILE": base / ".tools_version.json",
|
||||
}
|
||||
|
||||
def fake_download(url: str, dest: Path) -> None:
|
||||
dest.write_bytes(b"downloaded")
|
||||
|
||||
with patch.multiple("util.ace.update_tools", **paths):
|
||||
with patch.object(
|
||||
ace,
|
||||
"_rv2json_upstream",
|
||||
return_value=("sha1", "https://example/rv2json"),
|
||||
):
|
||||
with patch.object(
|
||||
ace,
|
||||
"_decrypter_upstream",
|
||||
return_value=("v1", "https://example/dec"),
|
||||
):
|
||||
with patch.object(ace, "_download", side_effect=fake_download):
|
||||
self.assertTrue(ace.refresh_offline_bundle(log_fn=None))
|
||||
|
||||
self.assertEqual((offline / "RV2JSON.exe").read_bytes(), b"downloaded")
|
||||
self.assertEqual(
|
||||
(offline / ace.DECRYPTER_ASSET).read_bytes(), b"downloaded"
|
||||
)
|
||||
|
||||
def test_refresh_forge_plugins_installs_downloaded_bytes(self):
|
||||
with tempfile.TemporaryDirectory() as raw:
|
||||
base = Path(raw)
|
||||
upstream = base / "upstream"
|
||||
upstream.mkdir()
|
||||
|
||||
import util.forge.update_tools as forge
|
||||
|
||||
paths = {
|
||||
"_PKG_ROOT": base,
|
||||
"UPSTREAM_DIR": upstream,
|
||||
"VERSION_FILE": base / ".forge_version.json",
|
||||
}
|
||||
plugin = b"// downloaded forge\n"
|
||||
|
||||
with patch.multiple("util.forge.update_tools", **paths):
|
||||
with patch.object(forge, "_upstream_commit", return_value="abc123"):
|
||||
with patch.object(forge, "_fetch_plugins", return_value=plugin):
|
||||
self.assertTrue(forge.refresh_forge_plugins(log_fn=None))
|
||||
|
||||
self.assertEqual((base / "Forge_MV.js").read_bytes(), plugin)
|
||||
self.assertEqual((upstream / "Forge_MZ.js").read_bytes(), plugin)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
"""RPG Maker Ace helper binaries (downloaded on demand, not committed to git)."""
|
||||
"""RPG Maker Ace helper binaries (seeded from offline bundle, not committed to git)."""
|
||||
|
||||
__all__ = ["ace_tool_path", "build_decrypter_command", "ensure_ace_tools"]
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
RV2JSON.exe — https://github.com/Sinflower/RV2JSON (bin/RV2JSON.exe)
|
||||
Decrypter CLI — https://github.com/uuksu/RPGMakerDecrypter (release asset)
|
||||
|
||||
End users receive curated copies via the offline bundle (``util/ace/offline/``)
|
||||
shipped with DazedMTLTool updates. Upstream fetches are maintainer-only
|
||||
(``--refresh-offline`` or ``--force``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -67,15 +71,13 @@ def _log(msg: str, log_fn) -> None:
|
|||
|
||||
|
||||
def _seed_from_offline(local: Path, offline_name: str, log_fn) -> bool:
|
||||
"""Copy a bundled offline exe into the active path if missing."""
|
||||
if local.is_file():
|
||||
return True
|
||||
"""Copy a bundled offline exe into the active path when the bundle exists."""
|
||||
src = OFFLINE_DIR / offline_name
|
||||
if not src.is_file():
|
||||
return False
|
||||
return local.is_file()
|
||||
local.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(src, local)
|
||||
_log(f"Using offline bundled {offline_name}", log_fn)
|
||||
_log(f"Synced {offline_name} from offline bundle", log_fn)
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -103,9 +105,8 @@ def _decrypter_upstream() -> tuple[str, str]:
|
|||
raise RuntimeError(f"No {DECRYPTER_ASSET} asset in latest {DECRYPTER_REPO} release")
|
||||
|
||||
|
||||
def ensure_rv2json(force: bool = False, log_fn=print) -> bool:
|
||||
"""Download RV2JSON.exe if missing or upstream changed. Returns True if ready."""
|
||||
_seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn)
|
||||
def _fetch_rv2json_upstream(force: bool, log_fn=print) -> bool:
|
||||
"""Download RV2JSON.exe from upstream (maintainer-only)."""
|
||||
versions = _load_versions()
|
||||
try:
|
||||
remote_sha, download_url = _rv2json_upstream()
|
||||
|
|
@ -113,8 +114,6 @@ def ensure_rv2json(force: bool = False, log_fn=print) -> bool:
|
|||
if RV2JSON_LOCAL.is_file():
|
||||
_log(f"Warning: Could not check RV2JSON update ({exc}); using local copy.", log_fn)
|
||||
return True
|
||||
if _seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn):
|
||||
return True
|
||||
_log(f"ERROR: RV2JSON unavailable ({exc}).", log_fn)
|
||||
return False
|
||||
|
||||
|
|
@ -129,8 +128,6 @@ def ensure_rv2json(force: bool = False, log_fn=print) -> bool:
|
|||
if RV2JSON_LOCAL.is_file():
|
||||
_log(f"Warning: RV2JSON update failed ({exc}); keeping existing copy.", log_fn)
|
||||
return True
|
||||
if _seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn):
|
||||
return True
|
||||
_log(f"ERROR: RV2JSON download failed: {exc}", log_fn)
|
||||
return False
|
||||
|
||||
|
|
@ -140,9 +137,8 @@ def ensure_rv2json(force: bool = False, log_fn=print) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def ensure_decrypter(force: bool = False, log_fn=print) -> bool:
|
||||
"""Download RPGMakerDecrypter CLI if missing or upstream release changed."""
|
||||
_seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn)
|
||||
def _fetch_decrypter_upstream(force: bool, log_fn=print) -> bool:
|
||||
"""Download RPGMakerDecrypter CLI from upstream (maintainer-only)."""
|
||||
if DECRYPTER_LOCAL.is_file():
|
||||
pass # prefer CLI (offline bundle or download)
|
||||
elif DECRYPTER_LEGACY.is_file():
|
||||
|
|
@ -156,8 +152,6 @@ def ensure_decrypter(force: bool = False, log_fn=print) -> bool:
|
|||
if DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file():
|
||||
_log(f"Warning: Could not check decrypter update ({exc}); using local copy.", log_fn)
|
||||
return True
|
||||
if _seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn):
|
||||
return True
|
||||
_log(f"ERROR: Decrypter unavailable ({exc}).", log_fn)
|
||||
return False
|
||||
|
||||
|
|
@ -172,8 +166,6 @@ def ensure_decrypter(force: bool = False, log_fn=print) -> bool:
|
|||
if DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file():
|
||||
_log(f"Warning: Decrypter update failed ({exc}); keeping existing copy.", log_fn)
|
||||
return True
|
||||
if _seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn):
|
||||
return True
|
||||
_log(f"ERROR: Decrypter download failed: {exc}", log_fn)
|
||||
return False
|
||||
|
||||
|
|
@ -183,40 +175,52 @@ def ensure_decrypter(force: bool = False, log_fn=print) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
def ensure_rv2json(force: bool = False, log_fn=print) -> bool:
|
||||
"""Ensure RV2JSON.exe is present from the offline bundle (no upstream fetch)."""
|
||||
_seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn)
|
||||
if force:
|
||||
return _fetch_rv2json_upstream(force=force, log_fn=log_fn)
|
||||
if RV2JSON_LOCAL.is_file():
|
||||
return True
|
||||
_log(
|
||||
f"ERROR: RV2JSON.exe not found. Update DazedMTLTool or ask the maintainer "
|
||||
f"to refresh util/ace/offline/RV2JSON.exe.",
|
||||
log_fn,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def ensure_decrypter(force: bool = False, log_fn=print) -> bool:
|
||||
"""Ensure the RPG Maker decrypter CLI is present from the offline bundle."""
|
||||
_seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn)
|
||||
if DECRYPTER_LOCAL.is_file():
|
||||
if not force:
|
||||
return True
|
||||
elif DECRYPTER_LEGACY.is_file() and not force:
|
||||
_log("Using legacy RPGMakerDecrypter.exe (local).", log_fn)
|
||||
return True
|
||||
|
||||
if force:
|
||||
return _fetch_decrypter_upstream(force=force, log_fn=log_fn)
|
||||
|
||||
if DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file():
|
||||
return True
|
||||
_log(
|
||||
f"ERROR: {DECRYPTER_ASSET} not found. Update DazedMTLTool or ask the maintainer "
|
||||
f"to refresh util/ace/offline/{DECRYPTER_ASSET}.",
|
||||
log_fn,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def seed_ace_tools(log_fn=None) -> None:
|
||||
"""Copy offline-bundled Ace tools if missing (no network)."""
|
||||
"""Sync runtime Ace tools from the offline bundle (no network)."""
|
||||
_seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn)
|
||||
_seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn)
|
||||
|
||||
|
||||
def check_ace_tools_update() -> bool:
|
||||
"""Return True if upstream Ace tools differ from the local copies."""
|
||||
seed_ace_tools()
|
||||
versions = _load_versions()
|
||||
|
||||
try:
|
||||
remote_sha, _ = _rv2json_upstream()
|
||||
if not RV2JSON_LOCAL.is_file() or versions.get("rv2json_sha", "") != remote_sha:
|
||||
return True
|
||||
except Exception:
|
||||
if not RV2JSON_LOCAL.is_file():
|
||||
return True
|
||||
|
||||
try:
|
||||
tag, _ = _decrypter_upstream()
|
||||
if not (DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file()):
|
||||
return True
|
||||
if DECRYPTER_LOCAL.is_file() and versions.get("decrypter_tag", "") != tag:
|
||||
return True
|
||||
except Exception:
|
||||
if not (DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file()):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def ensure_ace_tools(force: bool = False, log_fn=print) -> bool:
|
||||
"""Ensure both Ace tools are present (download / update if needed)."""
|
||||
"""Ensure both Ace tools are present (offline bundle; upstream only with force=True)."""
|
||||
ok_rv = ensure_rv2json(force=force, log_fn=log_fn)
|
||||
ok_dec = ensure_decrypter(force=force, log_fn=log_fn)
|
||||
return ok_rv and ok_dec
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ Upstream: https://gitgud.io/zero64801/forge-mvmz
|
|||
CI builds a unified forge.js plugin (master branch artifacts).
|
||||
Offline copies: util/forge/upstream/
|
||||
Active plugins: util/forge/Forge_MZ.js, util/forge/Forge_MV.js
|
||||
|
||||
End users receive curated copies shipped with DazedMTLTool updates.
|
||||
Upstream fetches are maintainer-only (``--refresh-offline`` or ``--force``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -164,49 +167,31 @@ def seed_forge_plugins(log_fn=None) -> None:
|
|||
_seed_from_offline(engine, log_fn)
|
||||
|
||||
|
||||
def check_forge_plugins_update() -> bool:
|
||||
"""Return True if upstream Forge plugins differ from the local copies."""
|
||||
seed_forge_plugins()
|
||||
missing = [e for e in PLUGIN_BY_ENGINE if not bundled_plugin_path(e).is_file()]
|
||||
if missing:
|
||||
return True
|
||||
versions = _load_versions()
|
||||
local_commit = versions.get("commit", "")
|
||||
try:
|
||||
return _upstream_commit() != local_commit
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def ensure_forge_plugins(force: bool = False, log_fn=print) -> bool:
|
||||
"""Ensure Forge plugins are present; fetch upstream when missing or stale."""
|
||||
"""Ensure Forge plugins are present from the offline bundle (no upstream fetch)."""
|
||||
for engine in PLUGIN_BY_ENGINE:
|
||||
_seed_from_offline(engine, log_fn)
|
||||
|
||||
missing = [e for e in PLUGIN_BY_ENGINE if not bundled_plugin_path(e).is_file()]
|
||||
versions = _load_versions()
|
||||
local_commit = versions.get("commit", "")
|
||||
if missing and not force:
|
||||
names = ", ".join(PLUGIN_BY_ENGINE[e] + ".js" for e in missing)
|
||||
_log(
|
||||
f"ERROR: Forge plugin(s) missing ({names}). "
|
||||
"Update DazedMTLTool or ask the maintainer to refresh util/forge/upstream/.",
|
||||
log_fn,
|
||||
)
|
||||
return False
|
||||
|
||||
need_fetch = bool(missing) or force
|
||||
if not need_fetch:
|
||||
try:
|
||||
need_fetch = _upstream_commit() != local_commit
|
||||
except Exception as exc:
|
||||
if missing:
|
||||
_log(f"ERROR: Forge upstream unavailable ({exc})", log_fn)
|
||||
return False
|
||||
_log(f"Warning: could not check Forge update ({exc}); using local copy.", log_fn)
|
||||
|
||||
if need_fetch:
|
||||
if force:
|
||||
if refresh_forge_plugins(log_fn=log_fn):
|
||||
return True
|
||||
have_local = all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
|
||||
if force or not have_local:
|
||||
if not have_local:
|
||||
_log("ERROR: Forge plugin update failed.", log_fn)
|
||||
return False
|
||||
_log("Warning: could not update Forge plugins; using local copy.", log_fn)
|
||||
|
||||
return True
|
||||
return all(bundled_plugin_path(e).is_file() for e in PLUGIN_BY_ENGINE)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
|
|
|||
|
|
@ -3,9 +3,8 @@
|
|||
WolfDawn is the Rust toolchain (https://gitgud.io/zero64801/wolfdawn) that
|
||||
unpacks, extracts, injects, and repacks WOLF RPG Editor game data. DazedMTLTool
|
||||
ships prebuilt ``wolf`` binaries offline under ``util/wolfdawn/bin/<platform>/``
|
||||
so end users never need a Rust toolchain. When no offline binary is bundled for
|
||||
the running platform, the tool pulls a prebuilt one from the WolfDawn release
|
||||
page and caches it into that same folder. Everything the Wolf workflow needs
|
||||
so end users never need a Rust toolchain or a live upstream fetch. Binaries are
|
||||
updated when DazedMTLTool itself is updated. Everything the Wolf workflow needs
|
||||
goes through the helpers here so command syntax and exit-code handling live in
|
||||
one place.
|
||||
|
||||
|
|
@ -97,7 +96,7 @@ def inject_had_applied(applied: int | None) -> bool:
|
|||
return applied is not None and applied > 0
|
||||
|
||||
# Committed, prebuilt binaries live here (per-platform) so end users don't need a
|
||||
# Rust toolchain. When one is missing we download it from the WolfDawn release.
|
||||
# Rust toolchain or a live upstream fetch.
|
||||
_PACKAGE_DIR = Path(__file__).resolve().parent
|
||||
_BUNDLED_DIR = _PACKAGE_DIR / "bin"
|
||||
|
||||
|
|
@ -266,23 +265,20 @@ def download_wolf_binary(platform: Optional[str] = None, log_fn=print) -> Path:
|
|||
return _extract_wolf_from_zip(zip_bytes, dest, log_fn)
|
||||
|
||||
|
||||
def ensure_wolf_binary(force: bool = False, log_fn=print) -> Path:
|
||||
"""Return the ``wolf`` binary path, downloading it only if needed.
|
||||
def ensure_wolf_binary(log_fn=print) -> Path:
|
||||
"""Return the bundled ``wolf`` binary path (no upstream fetch at runtime).
|
||||
|
||||
Resolution order:
|
||||
1. Committed/cached prebuilt binary under ``util/wolfdawn/bin/<platform>/``
|
||||
— used directly so the tool works fully offline.
|
||||
2. Otherwise (or when ``force`` is set), pull the prebuilt binary from the
|
||||
WolfDawn release page and cache it into that same folder.
|
||||
|
||||
Raises :class:`WolfDawnError` with actionable guidance when no offline binary
|
||||
is present and the download can't be completed.
|
||||
Raises :class:`WolfDawnError` when no offline binary is present for this
|
||||
platform. Maintainers refresh bundled binaries with
|
||||
``python -m util.wolfdawn.update_tools --refresh-all``.
|
||||
"""
|
||||
if not force:
|
||||
bundled = bundled_binary_path()
|
||||
if bundled.is_file():
|
||||
return bundled
|
||||
return download_wolf_binary(log_fn=log_fn)
|
||||
bundled = bundled_binary_path()
|
||||
if bundled.is_file():
|
||||
return bundled
|
||||
raise WolfDawnError(
|
||||
f"No bundled WolfDawn binary for '{_platform_dir()}' at {bundled}. "
|
||||
"Update DazedMTLTool to receive a prebuilt wolf binary."
|
||||
)
|
||||
|
||||
|
||||
def _log(msg: str, log_fn) -> None:
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
"""Check for and apply WolfDawn ``wolf`` binary updates.
|
||||
"""Check for and apply WolfDawn ``wolf`` binary updates (maintainer-only upstream fetch).
|
||||
|
||||
Resolution order for each platform:
|
||||
1. Temporary shallow clone + ``cargo`` source build (when possible on this machine)
|
||||
2. Prebuilt zip from the latest GitLab release
|
||||
3. Keep the existing offline bundled binary
|
||||
End users receive prebuilt binaries under ``util/wolfdawn/bin/<platform>/`` via
|
||||
DazedMTLTool updates. Maintainers refresh them with ``--refresh-all`` or ``--force``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -22,7 +20,6 @@ from util.wolfdawn import (
|
|||
)
|
||||
from util.wolfdawn.build_tools import (
|
||||
build_and_install_platforms,
|
||||
buildable_from_source,
|
||||
upstream_commit,
|
||||
)
|
||||
|
||||
|
|
@ -69,39 +66,6 @@ def _local_version(platform: str) -> str:
|
|||
return _platform_versions().get(platform, "")
|
||||
|
||||
|
||||
def _record_platform_version(platform: str, version: str) -> None:
|
||||
versions = _load_versions()
|
||||
platforms = versions.setdefault("platforms", {})
|
||||
if not isinstance(platforms, dict):
|
||||
platforms = {}
|
||||
versions["platforms"] = platforms
|
||||
platforms[platform] = version
|
||||
if version and not versions.get("commit"):
|
||||
versions["commit"] = version
|
||||
_save_versions(versions)
|
||||
|
||||
|
||||
def _desired_version(platform: str) -> str:
|
||||
"""Best upstream version we can install for ``platform`` on this machine."""
|
||||
if buildable_from_source(platform):
|
||||
return upstream_commit()
|
||||
asset = _latest_release_asset(platform)
|
||||
if asset:
|
||||
return asset[0]
|
||||
return upstream_commit()
|
||||
|
||||
|
||||
def check_wolfdawn_update(platform: str | None = None) -> bool:
|
||||
"""Return True when upstream WolfDawn differs from the bundled binary."""
|
||||
platform = platform or _platform_dir()
|
||||
if not bundled_binary_path(platform).is_file():
|
||||
return True
|
||||
try:
|
||||
return _local_version(platform) != _desired_version(platform)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _refresh_from_release(platform: str, log_fn=print) -> str | None:
|
||||
asset = _latest_release_asset(platform)
|
||||
if not asset:
|
||||
|
|
@ -169,30 +133,27 @@ def refresh_wolfdawn_binary(
|
|||
|
||||
|
||||
def ensure_wolfdawn_binary(force: bool = False, log_fn=print) -> bool:
|
||||
"""Ensure the ``wolf`` binary is present; refresh when missing or stale."""
|
||||
"""Ensure the bundled ``wolf`` binary is present (no upstream fetch by default)."""
|
||||
platform = _platform_dir()
|
||||
bundled = bundled_binary_path(platform)
|
||||
missing = not bundled.is_file()
|
||||
|
||||
need_fetch = missing or force
|
||||
if not need_fetch:
|
||||
try:
|
||||
need_fetch = check_wolfdawn_update(platform)
|
||||
except Exception as exc:
|
||||
if missing:
|
||||
_log(f"ERROR: WolfDawn upstream unavailable ({exc})", log_fn)
|
||||
return False
|
||||
_log(f"Warning: could not check WolfDawn update ({exc}); using local copy.", log_fn)
|
||||
|
||||
if need_fetch:
|
||||
if force:
|
||||
if refresh_wolfdawn_binary(platforms=BUNDLED_PLATFORMS, log_fn=log_fn):
|
||||
return bundled_binary_path(platform).is_file()
|
||||
if force or missing:
|
||||
_log("ERROR: WolfDawn update failed.", log_fn)
|
||||
return False
|
||||
_log("Warning: could not update WolfDawn; using local copy.", log_fn)
|
||||
if bundled.is_file():
|
||||
_log("Warning: WolfDawn update failed; using bundled copy.", log_fn)
|
||||
return True
|
||||
_log("ERROR: WolfDawn update failed.", log_fn)
|
||||
return False
|
||||
|
||||
return bundled.is_file()
|
||||
if bundled.is_file():
|
||||
return True
|
||||
_log(
|
||||
f"ERROR: no bundled WolfDawn binary for '{platform}' at {bundled}. "
|
||||
"Update DazedMTLTool to receive a prebuilt wolf binary.",
|
||||
log_fn,
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def main() -> int:
|
||||
|
|
|
|||
Loading…
Reference in a new issue