From cef2352a5e81b272d067c2c02eb9588e54edfcfd Mon Sep 17 00:00:00 2001 From: historia Date: Thu, 27 Aug 2026 17:03:57 -0400 Subject: feat: automatic updates added to configure backend menu --- app/backends/common.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 108 insertions(+), 6 deletions(-) (limited to 'app/backends/common.py') diff --git a/app/backends/common.py b/app/backends/common.py index 098dfb2..4edfd61 100644 --- a/app/backends/common.py +++ b/app/backends/common.py @@ -496,22 +496,124 @@ def git_clone(url: str, target: Path, *, emit=None, cancel=None) -> int: emit=emit, cancel=cancel) +def git_head(checkout: Path) -> Optional[str]: + """CHECKOUT's current HEAD commit sha, or None when it is not a repo.""" + proc = run_console_subprocess_quiet(["git", "-C", str(checkout), + "rev-parse", "HEAD"]) + if proc is None or proc.returncode != 0: + return None + return proc.stdout.decode("utf-8", errors="replace").strip() or None + + +def git_commit_time(checkout: Path) -> Optional[int]: + """CHECKOUT's HEAD commit time as a unix timestamp, or None. + + Uses the *committer* time (``%ct``): a rebase or cherry-pick rewrites + it to when the rewrite happened, so a force-pushed or rebased branch + always looks newer than binaries built from the pre-rewrite sources. + None (not a repo, probe failed) leaves the decision to the caller. + """ + proc = run_console_subprocess_quiet(["git", "-C", str(checkout), + "show", "-s", "--format=%ct", + "HEAD"]) + if proc is None or proc.returncode != 0: + return None + try: + return int(proc.stdout.decode("ascii", errors="replace").strip()) + except ValueError: + return None + + +def git_update(checkout: Path, *, emit=None, cancel=None) -> int: + """Update CHECKOUT to its remote's HEAD: fetch, then hard reset. + + The backend checkouts are read-only working copies of upstream repos — + all state that matters (models, build trees, server.json, voices.json) + is untracked and survives the reset, while local edits the installers + made (the vendored-ggml patch in the audio.cpp checkout) are meant to + be re-applied by the caller afterwards. ``git reset --hard`` is used + instead of ``git pull`` because a pull merges against the working tree + and would conflict on exactly those re-applied-by-design edits. + + The branch reset to is the remote's default (``refs/remotes/origin/ + HEAD``), falling back to ``main`` when the symbolic ref is missing (a + bare-ish mirror or a restrictive server). EMIT/CANCEL behave like + git_clone's (fetch runs with --progress so the task view sees updates). + Returns the exit code of the first failing step (0 when the checkout + now matches origin's HEAD). + """ + if emit is None: + print(f"[INFO] Updating git checkout {checkout}...") + else: + emit(f"[INFO] Updating git checkout {checkout}...") + fetch_argv = ["git", "-C", str(checkout), "fetch"] + reset_argv = ["git", "-C", str(checkout), "reset", "--hard"] + if emit is not None: + # --progress makes git report percentage updates even though stderr + # is piped (it normally only does so on a terminal), feeding the + # task view. + fetch_argv.append("--progress") + fetch_argv.append("origin") + fetch_rc = run_console_subprocess(fetch_argv, emit=emit, cancel=cancel) + if fetch_rc != 0: + return fetch_rc + branch = _origin_default_branch(checkout) + return run_console_subprocess(reset_argv + [f"origin/{branch}"], + emit=emit, cancel=cancel) + + +def _origin_default_branch(checkout: Path) -> str: + """The remote's default branch name for CHECKOUT ("main" as fallback).""" + proc = run_console_subprocess_quiet( + ["git", "-C", str(checkout), "symbolic-ref", + "refs/remotes/origin/HEAD"]) + if proc is not None and proc.returncode == 0: + ref = proc.stdout.decode("utf-8", errors="replace").strip() + # refs/remotes/origin/HEAD -> refs/remotes/origin/main + name = ref.rpartition("/")[2] + if name: + return name + return "main" + + +def run_console_subprocess_quiet(argv: List[str], + cwd: Optional[Path] = None): + """Run ARGV silently and return the completed result. + + Unlike run_console_subprocess (which streams or returns only an exit + code) this captures stdout and needs the process object itself, for the + small git probes (rev-parse, symbolic-ref) whose *output* matters and + whose failure is a normal, non-fatal outcome. Returns None when the + process could not be started. + """ + import subprocess + try: + return subprocess.run( + argv, capture_output=True, + cwd=str(cwd) if cwd is not None else None, check=False) + except OSError: + return None + + def pip_install(packages: List[str], *, emit=None, cancel=None, - env_dir: Optional[Path] = None) -> int: + env_dir: Optional[Path] = None, + upgrade: bool = False) -> int: """pip install PACKAGES into a managed venv. Returns exit code. Delegates to ``backends.envs.pip_install`` so backend TTS packages are installed into their dedicated tool-managed environments (``envs/tts`` default; ``envs/qwen`` / ``envs/faster`` via ENV_DIR) rather than into whatever interpreter happens to be running the wizard — and never two - conflicting stacks into the same env. With EMIT given (the in-TUI task - view) pip runs with its output streamed into EMIT; CANCEL aborts it. - The import is local to avoid a circular import (envs imports this - module). + conflicting stacks into the same env. With UPGRADE pip runs with + ``-U`` (the backend update action's freshness check: pip only installs + when a newer version resolves, else reports "already satisfied"). + With EMIT given (the in-TUI task view) pip runs with its output streamed + into EMIT; CANCEL aborts it. The import is local to avoid a circular + import (envs imports this module). """ from backends import envs return envs.pip_install(packages, emit=emit, cancel=cancel, - env_dir=env_dir) + env_dir=env_dir, upgrade=upgrade) def pip_uninstall(packages: List[str], *, emit=None, -- cgit v1.2.3