Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
CI, so the version lives in exactly one place. CI fails a ``v<X.Y.Z>`` release
tag whose value doesn't match this string.
"""
__version__ = "1.8.34"
__version__ = "1.9.0"
# Display name; single source shared by the app + mixins. "PrevizRender" is
# the product name (formerly "Render Mapper Pro"); installer/bundle artifact
# names in BlenderVideoMapper.spec and installer/windows.iss intentionally keep
Expand Down
9 changes: 9 additions & 0 deletions app_window/update_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,17 @@ def _on_update_checked(self, info, manual: bool, error: str = "") -> None:
self._append_log(f"[update] Check failed — {error}")
if manual:
detail = f"\n\n{error}" if error else ""
# A TLS/cert failure means no CA bundle is reachable — installed
# builds ship one, so this only bites when running from source.
cert_hint = ""
if "CERTIFICATE_VERIFY" in error or "SSL" in error:
cert_hint = (
"\n\nThis looks like a missing HTTPS certificate bundle. "
"Installed builds ship one; running from source, install "
"it with: pip install certifi")
warn(self, "Updates",
"Couldn't reach GitHub to check for updates." + detail
+ cert_hint
+ "\n\nYou can always download the latest version from the "
"Releases page on GitHub.")
return
Expand Down
11 changes: 9 additions & 2 deletions core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,9 @@ def ca_bundle_path() -> str | None:
CERTIFICATE_VERIFY_FAILED ("Couldn't reach GitHub …") unless we point it at a
bundled cacert.pem. Tries, in order: certifi (if importable), then the
cacert.pem the build drops into the bundle root / certifi/ dir — so HTTPS
works even if ``import certifi`` itself fails inside the frozen app. Returns
None from source (the system trust store is fine there)."""
works even if ``import certifi`` itself fails inside the frozen app, then an
explicit ``SSL_CERT_FILE`` / ``REQUESTS_CA_BUNDLE`` env var. Returns None
from a normal source checkout (the system trust store is fine there)."""
try:
import certifi
p = certifi.where()
Expand All @@ -122,6 +123,12 @@ def ca_bundle_path() -> str | None:
os.path.join(base, "certifi", "cacert.pem")):
if os.path.exists(cand):
return cand
# Last resort: honour an explicitly configured bundle so a user on a locked-
# down machine can point us at their own CA file without a rebuild.
for var in ("SSL_CERT_FILE", "REQUESTS_CA_BUNDLE"):
env_path = os.environ.get(var)
if env_path and os.path.exists(env_path):
return env_path
return None


Expand Down
12 changes: 11 additions & 1 deletion panels.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
_audio_probe_cache,
file_manager_name,
find_ffmpeg_tool,
reveal_in_file_manager,
video_has_audio,
)
from theme import LINK_COLORS, active_palette
Expand Down Expand Up @@ -1162,6 +1163,8 @@ def current_video(self) -> str:

def _show_video_context_menu(self, pos) -> None:
item = self.vid_list.itemAt(pos)
path = str(item.data(ROLE_VIDEO_PATH) or "") if item is not None else ""
is_video = bool(path) and not path.startswith("__add_video__")
menu = QMenu(self)
add_action = menu.addAction("Add Videos...")
render_action = menu.addAction("Start Render")
Expand All @@ -1171,7 +1174,12 @@ def _show_video_context_menu(self, pos) -> None:
menu.addSeparator()
muted = bool(item.data(ROLE_MUTED))
mute_action = menu.addAction("Unmute audio" if muted else "Mute audio")
if item is None or str(item.data(ROLE_VIDEO_PATH) or "").startswith("__add_video__"):
reveal_action = None
if is_video:
menu.addSeparator()
reveal_action = menu.addAction(f"Reveal in {file_manager_name()}")
reveal_action.setToolTip(str(Path(path).parent))
if not is_video:
remove_action.setEnabled(False)
chosen = menu.exec(self.vid_list.viewport().mapToGlobal(pos))
if chosen == add_action:
Expand All @@ -1182,6 +1190,8 @@ def _show_video_context_menu(self, pos) -> None:
self._remove_selected_video()
elif mute_action is not None and item is not None and chosen == mute_action:
self.toggle_mute(item.data(ROLE_VIDEO_PATH))
elif reveal_action is not None and chosen == reveal_action:
reveal_in_file_manager(path)


# Compact field widths so a single value never spans the panel: small numeric
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ requires-python = ">=3.12"
dynamic = ["version"]
dependencies = [
"PySide6==6.11.1",
# Ships a CA bundle so HTTPS (the GitHub update check) verifies even in a
# frozen build whose bundled Python carries no system certificates.
"certifi",
]

[project.optional-dependencies]
Expand Down
4 changes: 4 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# Pinned for reproducibility; see requirements-dev.txt for dev/CI tooling and
# requirements-build.txt for the frozen-app build pins.
PySide6==6.11.1
# CA bundle for HTTPS verification. A frozen build ships its own Python with no
# system certificates, so the GitHub update check fails with
# CERTIFICATE_VERIFY_FAILED without this. ca_bundle_path() prefers it.
certifi
# Optional — enables the headless three.js (.glb/.gltf) render backend. The
# Chromium browser itself is downloaded on demand, not via pip.
playwright==1.60.0
24 changes: 24 additions & 0 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
import sys
from typing import cast

import pytest

from core.utils import (
atomic_write_text,
ca_bundle_path,
expand_output_tokens,
ext_for_format,
ffmpeg_movie_av_args,
Expand Down Expand Up @@ -176,3 +179,24 @@ def test_terminate_process_already_exited_is_noop():
p.wait()
terminate_process(p) # must not raise on an already-dead process
assert p.poll() is not None


def test_ca_bundle_prefers_certifi():
# When certifi is importable its bundle wins — this is what a source checkout
# and every installed build rely on for HTTPS verification (the update check).
# CI's lint-test job installs only pinned tools, so skip when it's absent.
certifi = pytest.importorskip("certifi")
p = ca_bundle_path()
assert p is not None and p == certifi.where()


def test_ca_bundle_env_fallback(tmp_path, monkeypatch):
# No certifi, not frozen, but SSL_CERT_FILE points at a real bundle → honour
# it, so a locked-down machine can supply its own CA file without a rebuild.
ca = tmp_path / "corp-ca.pem"
ca.write_text("dummy")
monkeypatch.setitem(sys.modules, "certifi", None) # force `import certifi` to fail
monkeypatch.setattr(sys, "frozen", False, raising=False)
monkeypatch.delenv("REQUESTS_CA_BUNDLE", raising=False)
monkeypatch.setenv("SSL_CERT_FILE", str(ca))
assert ca_bundle_path() == str(ca)
Loading