from __future__ import annotations import os import sys from dataclasses import dataclass from pathlib import Path @dataclass(frozen=True) class AvisynthRunnerSet: default: Path runner64: Path | None = None runner32: Path | None = None def for_script(self, script: Path) -> Path: if sys.platform != "win32": return self.default if script_requests_32bit(script): if self.runner32 is None: raise RuntimeError( f"{script} requests 32-bit AviSynth with a trailing #32bit " "marker, but no 32-bit runner was found. Set " "MBT_AVS_RUNNER32 or provide tools/avisynth_runner/" "mbt_avs_runner32.exe." ) return self.runner32 return self.default def bundled_runner_path(name: str | None = None) -> Path | None: if name is None: name = "mbt_avs_runner.exe" if sys.platform == "win32" else "mbt_avs_runner" runner = _runner_dir() / name return runner if runner.is_file() else None def runner_path_from_env_or_bundled() -> Path | None: runners = runner_set_from_env_or_bundled() return runners.default if runners is not None else None def runner_set_from_env_or_bundled() -> AvisynthRunnerSet | None: if sys.platform == "win32": return _windows_runner_set_from_env_or_bundled() env_path = os.environ.get("MBT_AVS_RUNNER", "").strip() if env_path: runner = validate_runner_path(env_path) return AvisynthRunnerSet(default=runner, runner64=runner) bundled = bundled_runner_path() if bundled is not None: return AvisynthRunnerSet(default=bundled, runner64=bundled) return None def _windows_runner_set_from_env_or_bundled() -> AvisynthRunnerSet | None: runner64 = _optional_runner_from_env("MBT_AVS_RUNNER64") if runner64 is None: runner64 = _optional_runner_from_env("MBT_AVS_RUNNER") if runner64 is None: runner64 = bundled_runner_path("mbt_avs_runner.exe") runner32 = _optional_runner_from_env("MBT_AVS_RUNNER32") if runner32 is None: runner32 = bundled_runner_path("mbt_avs_runner32.exe") default = runner64 or runner32 if default is None: return None return AvisynthRunnerSet(default=default, runner64=runner64, runner32=runner32) def _optional_runner_from_env(name: str) -> Path | None: raw = os.environ.get(name, "").strip() if not raw: return None return validate_runner_path(raw) def validate_runner_path(raw_path: str) -> Path: runner_path = Path(raw_path.strip().strip('"')).expanduser() for candidate in _runner_candidates(runner_path): if candidate.is_file(): return candidate.resolve() raise RuntimeError( f"Avisynth runner file not found: {runner_path}. " "Use the full path, set MBT_AVS_RUNNER, or leave blank to use the " "bundled runner when present." ) def _runner_candidates(path: Path) -> list[Path]: candidates = [path] if len(path.parts) == 1: candidates.append(_runner_dir() / path.name) return candidates def _runner_dir() -> Path: return Path(__file__).resolve().parent / "avisynth_runner" def script_requests_32bit(script: Path) -> bool: lines = script.read_text(encoding="utf-8").splitlines() while lines and not lines[-1].strip(): lines.pop() if not lines: return False normalized = "".join(lines[-1].split()).lower() return normalized in {"#32bit", "#32-bit"}