From 01f5e35b6102b0c029116d60f9d94e6a4cd1a233 Mon Sep 17 00:00:00 2001 From: Nose Date: Wed, 1 Apr 2026 14:36:36 -0700 Subject: [PATCH] fix installer --- scripts/hydrusnetwork.py | 41 ++++++------ scripts/run_client.py | 130 ++++++++++++++++++++++++++++++++------- 2 files changed, 128 insertions(+), 43 deletions(-) diff --git a/scripts/hydrusnetwork.py b/scripts/hydrusnetwork.py index 4d7e9b1..7eb4d7c 100644 --- a/scripts/hydrusnetwork.py +++ b/scripts/hydrusnetwork.py @@ -1619,14 +1619,14 @@ def main(argv: Optional[list[str]] = None) -> int: break script_dir = Path(__file__).resolve().parent - ensure_run_client_helper(dest, script_dir) + installed_helper = ensure_run_client_helper(dest, script_dir) run_client_script = None if client_found: - # Prefer run_client helper located in the cloned repo; if missing, fall back to top-level scripts folder helper. - helper_candidates = [dest / "run_client.py", script_dir / "run_client.py"] + # Prefer the helper installed directly into the Hydrus repository. + helper_candidates = [installed_helper, dest / "run_client.py", script_dir / "run_client.py"] for cand in helper_candidates: - if cand.exists(): + if cand and cand.exists(): run_client_script = cand break if getattr(args, @@ -1830,20 +1830,22 @@ def main(argv: Optional[list[str]] = None) -> int: # Helpful hint: show the new run_client helper and direct run example try: - helper_to_show = ( - run_client_script if - (run_client_script and run_client_script.exists()) else - (script_dir / "run_client.py") - ) if venv_py: - logging.info( - "To run the Hydrus client using the repo venv (no activation needed):\n %s %s [args]\nOr use the helper: %s --help\nHelper examples:\n %s --install-deps --verify\n %s --headless --detached", - venv_py, - dest / "hydrus_client.py", - helper_to_show, - helper_to_show, - helper_to_show, - ) + if run_client_script and run_client_script.exists() and run_client_script.resolve().parent == dest.resolve(): + logging.info( + "To run the Hydrus client using the repo venv (no activation needed):\n %s %s [args]\nOr use the helper: %s --help\nHelper examples:\n %s --install-deps --verify\n %s --no-update --headless --detached", + venv_py, + dest / "hydrus_client.py", + run_client_script, + run_client_script, + run_client_script, + ) + else: + logging.info( + "To run the Hydrus client using the repo venv (no activation needed):\n %s %s [args]", + venv_py, + dest / "hydrus_client.py", + ) else: logging.info( "To run the Hydrus client: python %s [args]", @@ -1875,14 +1877,11 @@ def ensure_run_client_helper(dest: Path, script_dir: Path) -> Optional[Path]: return None helper_dest = dest / "run_client.py" - if helper_dest.exists(): - return helper_dest - try: shutil.copy2(helper_src, helper_dest) if os.name != "nt": helper_dest.chmod(helper_dest.stat().st_mode | 0o111) - logging.debug("Copied run_client helper to %s", helper_dest) + logging.debug("Installed run_client helper to %s", helper_dest) return helper_dest except Exception as exc: logging.debug("Failed to copy run_client helper: %s", exc) diff --git a/scripts/run_client.py b/scripts/run_client.py index 3c457c5..d1dfa55 100644 --- a/scripts/run_client.py +++ b/scripts/run_client.py @@ -22,7 +22,7 @@ from __future__ import annotations import argparse import os -import pwd +import re import shlex import shutil import subprocess @@ -30,6 +30,11 @@ import sys from pathlib import Path from typing import List, Optional +try: + import pwd +except ImportError: + pwd = None + def get_python_in_venv(venv_dir: Path) -> Optional[Path]: try: @@ -72,6 +77,7 @@ def find_requirements(root: Path) -> Optional[Path]: def install_requirements( venv_py: Path, + repo_root: Path, req_path: Path, reinstall: bool = False, upgrade: bool = False @@ -93,20 +99,13 @@ def install_requirements( check=True, **kwargs ) - install_cmd = [str(venv_py), "-m", "pip", "install", "-r", str(req_path)] + python_version = get_python_version_info(venv_py) + install_cmd = [str(venv_py), "-m", "pip", "install", "--prefer-binary"] if upgrade: - install_cmd = [str(venv_py), "-m", "pip", "install", "--upgrade", "-r", str(req_path)] + install_cmd.append("--upgrade") if reinstall: - install_cmd = [ - str(venv_py), - "-m", - "pip", - "install", - "--upgrade", - "--force-reinstall", - "-r", - str(req_path), - ] + install_cmd.extend(["--upgrade", "--force-reinstall"]) + install_cmd.extend(build_hydrus_install_targets(req_path, repo_root, python_version)) subprocess.run(install_cmd, check=True, **kwargs) return True except subprocess.CalledProcessError as e: @@ -140,6 +139,69 @@ def parse_requirements_file(req_path: Path) -> List[str]: return names +def get_python_version_info(python_exe: Path) -> Optional[tuple[int, int, int]]: + try: + result = subprocess.run( + [ + str(python_exe), + "-c", + "import sys; print('.'.join(str(part) for part in sys.version_info[:3]))", + ], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + check=True, + timeout=10, + ) + version_text = (result.stdout or "").strip() + major, minor, micro = version_text.split(".", 2) + return int(major), int(minor), int(micro) + except Exception: + return None + + +def is_hydrus_repo(repo_root: Path) -> bool: + return (repo_root / "setup_venv.py").exists() and (repo_root / "hydrus_client.py").exists() + + +def build_hydrus_install_targets( + req_path: Path, + repo_root: Path, + python_version: Optional[tuple[int, int, int]], +) -> List[str]: + if python_version is None or python_version < (3, 13) or not is_hydrus_repo(repo_root): + return ["-r", str(req_path)] + + overrides = { + "pyside6": "PySide6==6.10.1", + "qtpy": "QtPy==2.4.3", + "opencv-python-headless": "opencv-python-headless==4.13.0.90", + "numpy": "numpy==2.4.1", + } + targets: List[str] = [] + seen_packages = set() + + for raw_line in req_path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + + package_name = re.split(r"[<>=!~\[]", line, maxsplit=1)[0].strip().lower() + if package_name in overrides: + line = overrides[package_name] + + targets.append(line) + seen_packages.add(package_name) + + if os.name == "nt" and "pywin32" not in seen_packages: + targets.append("pywin32") + + print( + f"Using Hydrus compatibility dependency set for Python {python_version[0]}.{python_version[1]}.{python_version[2]}" + ) + return targets + + def verify_imports(venv_py: Path, packages: List[str]) -> bool: # Skip mpv check as it is problematic to install and causes slow startups packages = [p for p in packages if p.lower() != "mpv"] @@ -228,6 +290,8 @@ def is_first_run(repo_root: Path) -> bool: def _user_exists(username: str) -> bool: + if pwd is None: + return False try: pwd.getpwnam(username) return True @@ -914,7 +978,12 @@ def main(argv: Optional[List[str]] = None) -> int: p.add_argument( "--pull", action="store_true", - help="Run 'git pull' before starting the client", + help="Force a repository update before starting the client (legacy alias; startup update is enabled by default)", + ) + p.add_argument( + "--no-update", + action="store_true", + help="Skip the default repository git pull before startup", ) p.add_argument( "--update-deps", @@ -984,19 +1053,36 @@ def main(argv: Optional[List[str]] = None) -> int: else: repo_root = workspace_root - # Handle git pull update if requested - # Skip execution during service install/uninstall; it will run when the service starts - if args.pull and not (args.install_service or args.uninstall_service): + repo_updated = False + should_pull_repo = not args.no_update and not (args.install_service or args.uninstall_service) + if should_pull_repo: if shutil.which("git"): if (repo_root / ".git").exists(): if not args.quiet: - print(f"Updating repository via 'git pull' in {repo_root}...") + print(f"Updating repository via 'git pull --ff-only' in {repo_root}...") try: - # Use creationflags to hide the window on Windows k = {} if os.name == "nt": k["creationflags"] = 0x08000000 - subprocess.run(["git", "pull"], cwd=str(repo_root), check=False, **k) + result = subprocess.run( + ["git", "pull", "--ff-only"], + cwd=str(repo_root), + check=False, + capture_output=True, + text=True, + **k, + ) + pull_output = "\n".join( + part.strip() + for part in [result.stdout or "", result.stderr or ""] + if part.strip() + ) + if result.returncode != 0: + print(f"Warning: git pull failed: {pull_output or result.returncode}") + else: + repo_updated = "already up to date" not in pull_output.lower() + if pull_output and not args.quiet: + print(pull_output) except Exception as e: print(f"Warning: git pull failed: {e}") else: @@ -1127,7 +1213,7 @@ def main(argv: Optional[List[str]] = None) -> int: except Exception: pass - if not should_update and args.pull and not (args.install_service or args.uninstall_service): + if not should_update and (args.pull or repo_updated) and not (args.install_service or args.uninstall_service): should_update = True if args.install_deps or args.reinstall or should_update: @@ -1135,7 +1221,7 @@ def main(argv: Optional[List[str]] = None) -> int: if not req: print("No requirements.txt found; skipping install") else: - ok = install_requirements(venv_py, req, reinstall=args.reinstall, upgrade=should_update) + ok = install_requirements(venv_py, repo_root, req, reinstall=args.reinstall, upgrade=should_update) if not ok: print("Dependency installation failed; aborting") return 4