diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index bf3083f..77e3ebf 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -45,6 +45,8 @@ jobs:
run: uv run ms2rescore --help
test-windows-installer:
+ # Only run on push to main (e.g., after PR merge)
+ if: ${{ github.ref == 'refs/heads/main' }}
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
diff --git a/ms2rescore.spec b/ms2rescore.spec
index 4ab7e37..8ecd4eb 100644
--- a/ms2rescore.spec
+++ b/ms2rescore.spec
@@ -45,7 +45,13 @@ while requirements:
checked.add(requirement)
module_version = importlib.metadata.version(re.match(r"^[\w\-]+", requirement)[0])
try:
- datas_, binaries_, hidden_imports_ = collect_all(requirement, include_py_files=True)
+ # Use filter to exclude problematic xgboost.testing module
+ filter_func = lambda name: not name.startswith("xgboost.testing") if requirement == "xgboost" else True
+ datas_, binaries_, hidden_imports_ = collect_all(
+ requirement,
+ include_py_files=True,
+ filter_submodules=filter_func
+ )
except (ImportError, RuntimeError) as e:
# Skip packages that fail to collect (e.g., xgboost.testing requires hypothesis)
print(f"Warning: Failed to collect {requirement}: {e}")
@@ -61,6 +67,18 @@ while requirements:
hidden_imports = sorted([h for h in hidden_imports if "tests" not in h.split(".")])
hidden_imports = [h for h in hidden_imports if "__pycache__" not in h]
+
+# Add hdf5plugin imports to fix runtime import issues
+hidden_imports.extend([
+ "hdf5plugin.plugins.bshuf",
+ "hdf5plugin.plugins.blosc",
+ "hdf5plugin.plugins.blosc2",
+ "hdf5plugin.plugins.lz4",
+ "hdf5plugin.plugins.fcidecomp",
+ "hdf5plugin.plugins.zfp",
+ "hdf5plugin.plugins.zstd",
+])
+
datas = [
d
for d in datas
diff --git a/ms2rescore/gui/__main__.py b/ms2rescore/gui/__main__.py
index 429e117..583a856 100644
--- a/ms2rescore/gui/__main__.py
+++ b/ms2rescore/gui/__main__.py
@@ -2,7 +2,7 @@
import multiprocessing
import os
-import contextlib
+import sys
from ms2rescore.gui.app import app
@@ -10,9 +10,15 @@
def main():
"""Entrypoint for MS²Rescore GUI."""
multiprocessing.freeze_support()
- # Redirect stdout when running GUI (packaged app might not have console attached)
- with contextlib.redirect_stdout(open(os.devnull, "w")):
- app()
+
+ # Fix for PyInstaller windowed mode: sys.stdout/stderr can be None
+ # This causes issues with libraries that try to write to stdout (e.g., Keras progress bars)
+ if sys.stdout is None:
+ sys.stdout = open(os.devnull, "w")
+ if sys.stderr is None:
+ sys.stderr = open(os.devnull, "w")
+
+ app()
if __name__ == "__main__":
diff --git a/ms2rescore/gui/app.py b/ms2rescore/gui/app.py
index c62e12b..bc781c8 100644
--- a/ms2rescore/gui/app.py
+++ b/ms2rescore/gui/app.py
@@ -859,6 +859,23 @@ def _check_updates_sync(root):
pass
+def _setup_logging(log_level: str, log_file: str):
+ """Setup file logging for GUI."""
+ log_level_map = {
+ "critical": logging.CRITICAL,
+ "error": logging.ERROR,
+ "warning": logging.WARNING,
+ "info": logging.INFO,
+ "debug": logging.DEBUG,
+ }
+ file_handler = logging.FileHandler(log_file, mode="w", encoding="utf-8")
+ file_handler.setFormatter(
+ logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
+ )
+ file_handler.setLevel(log_level_map.get(log_level, logging.INFO))
+ logging.getLogger().addHandler(file_handler)
+
+
def function(config):
"""Function to be executed in a separate process."""
config = config.copy()
@@ -867,6 +884,12 @@ def function(config):
else:
config_list = [config]
config = parse_configurations(config_list)
+
+ # Set up file logging for GUI
+ _setup_logging(
+ config["ms2rescore"]["log_level"], config["ms2rescore"]["output_path"] + ".log.txt"
+ )
+
rescore(configuration=config)
if config["ms2rescore"]["write_report"]:
webbrowser.open_new_tab(config["ms2rescore"]["output_path"] + ".report.html")
diff --git a/ms2rescore/report/generate.py b/ms2rescore/report/generate.py
index 0043bf4..4a5d411 100644
--- a/ms2rescore/report/generate.py
+++ b/ms2rescore/report/generate.py
@@ -12,6 +12,7 @@
import plotly.express as px
import psm_utils.io
from jinja2 import Environment, FileSystemLoader
+from plotly.offline import get_plotlyjs_version
from psm_utils.psm_list import PSMList
try:
@@ -93,6 +94,7 @@ def generate_report(
log_context = _get_log_context(files)
context = {
+ "plotlyjs_version": get_plotlyjs_version(),
"metadata": {
"generated_on": datetime.now().strftime("%d/%m/%Y %H:%M:%S"),
"ms2rescore_version": ms2rescore.__version__, # TODO: Write during run?
@@ -418,8 +420,10 @@ def _render_and_write(output_path_prefix: str, **context):
"""Render template with context and write to HTML file."""
report_path = Path(output_path_prefix + ".report.html").resolve()
logger.info("Writing report to %s", report_path.as_posix())
- template_dir = Path(__file__).parent / "templates"
- env = Environment(loader=FileSystemLoader(template_dir, encoding="utf-8"))
+
+ # Use importlib.resources for PyInstaller compatibility
+ template_dir = importlib.resources.files(templates)
+ env = Environment(loader=FileSystemLoader(str(template_dir), encoding="utf-8"))
template = env.get_template("base.html")
with open(report_path, "w", encoding="utf-8") as f:
f.write(template.render(**context))
diff --git a/ms2rescore/report/templates/base.html b/ms2rescore/report/templates/base.html
index d8e0877..f2f9afc 100644
--- a/ms2rescore/report/templates/base.html
+++ b/ms2rescore/report/templates/base.html
@@ -11,7 +11,7 @@
-
+
{% include 'style.html' %}