updated installer script

This commit is contained in:
2026-04-01 13:58:06 -07:00
parent 57b595c1a4
commit 849dcbda85
3 changed files with 324 additions and 359 deletions

View File

@@ -21,6 +21,7 @@ Examples:
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
@@ -156,7 +157,7 @@ def run_git_clone(
def run_git_pull(git: str, dest: Path) -> None:
logging.info("Updating git repository in %s", dest)
subprocess.run([git, "-C", str(dest), "pull"], check=True)
subprocess.run([git, "-C", str(dest), "pull", "--ff-only"], check=True)
def _sanitize_store_name(name: str) -> str:
@@ -672,6 +673,186 @@ def parse_requirements_file(req_path: Path) -> list[str]:
return names
IMPORT_NAME_OVERRIDES = {
"pyyaml": "yaml",
"pillow": "PIL",
"python-dateutil": "dateutil",
"beautifulsoup4": "bs4",
"pillow-heif": "pillow_heif",
"pillow-jxl-plugin": "pillow_jxl",
"pyopenssl": "OpenSSL",
"pysocks": "socks",
"service-identity": "service_identity",
"show-in-file-manager": "showinfm",
"opencv-python-headless": "cv2",
"mpv": "mpv",
"pyside6": "PySide6",
"pyside6-essentials": "PySide6",
"pyside6-addons": "PySide6",
}
def ensure_repo_venv(
repo_root: Path,
venv_name: str = ".venv",
recreate: bool = False,
purpose: Optional[str] = None,
) -> Path:
venv_dir = repo_root / str(venv_name)
if venv_dir.exists():
if recreate:
logging.info("Removing existing venv: %s", venv_dir)
shutil.rmtree(venv_dir)
else:
logging.info("Using existing venv at %s", venv_dir)
if not venv_dir.exists():
if purpose:
logging.info("Creating venv at %s for %s", venv_dir, purpose)
else:
logging.info("Creating venv at %s", venv_dir)
subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
venv_py = get_python_in_venv(venv_dir)
if not venv_py:
raise RuntimeError(f"Could not locate python in venv {venv_dir}")
logging.info("Venv ready: %s", venv_py)
return venv_py
def install_requirements_into_venv(
venv_py: Path,
repo_root: Path,
req_path: Path,
reinstall: bool = False,
) -> None:
logging.info(
"Installing dependencies from %s into venv (reinstall=%s)",
req_path,
bool(reinstall),
)
subprocess.run(
[
str(venv_py),
"-m",
"pip",
"install",
"--disable-pip-version-check",
"--upgrade",
"pip",
],
check=True,
)
cmd = [
str(venv_py),
"-m",
"pip",
"install",
"--disable-pip-version-check",
]
if reinstall:
cmd.extend(["--upgrade", "--force-reinstall"])
cmd.extend(["-r", str(req_path)])
subprocess.run(cmd, cwd=str(repo_root), check=True)
logging.info("Dependencies installed successfully")
def verify_requirements_in_venv(venv_py: Path, req_path: Path) -> bool:
packages = parse_requirements_file(req_path)
if not packages:
logging.debug(
"No parseable packages found in %s for verification; skipping further checks",
req_path,
)
return True
logging.info("Running pip consistency check inside the venv...")
pip_check = subprocess.run(
[str(venv_py), "-m", "pip", "check"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
if pip_check.returncode != 0:
output = (pip_check.stdout or "").strip() or "Unknown dependency issue"
logging.warning("pip check reported issues:\n%s", output)
seen_modules: set[str] = set()
targets: list[tuple[str, str]] = []
for package in packages:
module_name = IMPORT_NAME_OVERRIDES.get(package, package)
if module_name in seen_modules:
continue
seen_modules.add(module_name)
targets.append((package, module_name))
verify_script = (
"import importlib, json\n"
f"targets = {targets!r}\n"
"failures = []\n"
"for package, module_name in targets:\n"
" try:\n"
" importlib.import_module(module_name)\n"
" except Exception as exc:\n"
" failures.append((package, module_name, f'{type(exc).__name__}: {exc}'))\n"
"print(json.dumps(failures))\n"
"raise SystemExit(1 if failures else 0)\n"
)
result = subprocess.run(
[str(venv_py), "-c", verify_script],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
failures: list[tuple[str, str, str]] = []
raw_output = (result.stdout or "").strip()
if raw_output:
try:
decoded = json.loads(raw_output)
failures = [tuple(item) for item in decoded]
except json.JSONDecodeError:
logging.warning(
"Dependency import verification returned unexpected output: %s",
raw_output,
)
any_missing = False
for package, module_name, error in failures:
if module_name == "mpv":
logging.info(
"Package '%s' is installed, but import '%s' failed (likely missing system libmpv). This is usually non-critical.",
package,
module_name,
)
continue
logging.warning(
"Package '%s' appears installed but import '%s' failed inside venv: %s",
package,
module_name,
error,
)
any_missing = True
if result.returncode != 0 and not failures:
stderr_text = (result.stderr or "").strip()
if stderr_text:
logging.warning("Dependency import verification failed: %s", stderr_text)
any_missing = True
if pip_check.returncode == 0 and not any_missing:
logging.info("Dependency verification completed successfully")
return True
logging.warning(
"Some dependencies may not be importable in the venv; consider running with --reinstall-deps"
)
return False
def open_in_editor(path: Path) -> bool:
"""Open the file using the OS default opener.
@@ -1034,34 +1215,14 @@ def main(argv: Optional[list[str]] = None) -> int:
if choice == "1":
# Install dependencies into the repository venv (create venv if needed)
try:
venv_dir = dest / str(
getattr(args,
"venv_name",
".venv")
venv_py = ensure_repo_venv(
dest,
getattr(args, "venv_name", ".venv"),
)
if venv_dir.exists():
logging.info("Using existing venv at %s", venv_dir)
else:
logging.info("Creating venv at %s", venv_dir)
subprocess.run(
[sys.executable,
"-m",
"venv",
str(venv_dir)],
check=True
)
venv_py = get_python_in_venv(venv_dir)
except Exception as e:
logging.error("Failed to prepare venv: %s", e)
return 8
if not venv_py:
logging.error(
"Could not locate python in venv %s",
venv_dir
)
return 9
req = find_requirements(dest)
if not req:
logging.info(
@@ -1070,96 +1231,18 @@ def main(argv: Optional[list[str]] = None) -> int:
)
return 0
logging.info(
"Installing dependencies from %s into venv",
req
)
try:
subprocess.run(
[
str(venv_py),
"-m",
"pip",
"install",
"--upgrade",
"pip"
],
check=True,
install_requirements_into_venv(
venv_py,
dest,
req,
reinstall=getattr(args, "reinstall_deps", False),
)
subprocess.run(
[
str(venv_py),
"-m",
"pip",
"install",
"--upgrade",
"-r",
str(req)
],
cwd=str(dest),
check=True,
)
logging.info("Dependencies installed successfully")
except subprocess.CalledProcessError as e:
logging.error("Failed to install dependencies: %s", e)
return 10
# Post-install verification
pkgs = parse_requirements_file(req)
if pkgs:
logging.info(
"Verifying installed packages inside the venv..."
)
any_missing = False
import_map = {
"pyyaml": "yaml",
"pillow": "PIL",
"python-dateutil": "dateutil",
"beautifulsoup4": "bs4",
"pillow-heif": "pillow_heif",
"pillow-jxl-plugin": "pillow_jxl",
"pyopenssl": "OpenSSL",
"pysocks": "socks",
"service-identity": "service_identity",
"show-in-file-manager": "showinfm",
"opencv-python-headless": "cv2",
"mpv": "mpv",
"pyside6": "PySide6",
"pyside6-essentials": "PySide6",
"pyside6-addons": "PySide6",
}
for pkg in pkgs:
mod = import_map.get(pkg, pkg)
try:
subprocess.run(
[str(venv_py),
"-c",
f"import {mod}"],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except Exception:
if mod == "mpv":
# python-mpv requires system libmpv; failure is common on server/headless envs
logging.info("Package '%s' is installed, but 'import %s' failed (likely missing system libmpv). This is usually non-critical.", pkg, mod)
continue
logging.warning(
"Package '%s' not importable inside venv (module %s)",
pkg,
mod,
)
any_missing = True
if any_missing:
logging.warning(
"Some packages failed to import inside the venv; consider running with --reinstall-deps"
)
else:
logging.info(
"All packages imported successfully inside the venv"
)
verify_requirements_in_venv(venv_py, req)
return 0
@@ -1180,24 +1263,11 @@ def main(argv: Optional[list[str]] = None) -> int:
elif choice == "3":
# Install a user-level service to start the hydrus client on boot
try:
venv_dir = dest / str(
getattr(args,
"venv_name",
".venv")
venv_py = ensure_repo_venv(
dest,
getattr(args, "venv_name", ".venv"),
purpose="service install",
)
if not venv_dir.exists():
logging.info(
"Creating venv at %s to perform service install",
venv_dir
)
subprocess.run(
[sys.executable,
"-m",
"venv",
str(venv_dir)],
check=True
)
venv_py = get_python_in_venv(venv_dir)
except Exception as e:
logging.error(
"Failed to prepare venv for service install: %s",
@@ -1205,13 +1275,6 @@ def main(argv: Optional[list[str]] = None) -> int:
)
return 8
if not venv_py:
logging.error(
"Could not locate python in venv %s; cannot manage service.",
venv_dir,
)
return 9
# Prefer the helper script inside the repo if present, else use installed helper
script_dir = Path(__file__).resolve().parent
helper_candidates = [
@@ -1336,36 +1399,11 @@ def main(argv: Optional[list[str]] = None) -> int:
# Post-obtain setup: create repository-local venv (unless disabled)
if not getattr(args, "no_venv", False):
try:
venv_py = None
venv_dir = dest / str(getattr(args, "venv_name", ".venv"))
if venv_dir.exists():
if getattr(args, "recreate_venv", False):
logging.info("Removing existing venv: %s", venv_dir)
shutil.rmtree(venv_dir)
else:
logging.info("Using existing venv at %s", venv_dir)
if not venv_dir.exists():
logging.info("Creating venv at %s", venv_dir)
try:
subprocess.run(
[sys.executable,
"-m",
"venv",
str(venv_dir)],
check=True
)
except subprocess.CalledProcessError as e:
logging.error("Failed to create venv: %s", e)
return 8
try:
venv_py = get_python_in_venv(venv_dir)
except Exception:
venv_py = None
if not venv_py:
logging.error("Could not locate python in venv %s", venv_dir)
return 9
logging.info("Venv ready: %s", venv_py)
venv_py = ensure_repo_venv(
dest,
getattr(args, "venv_name", ".venv"),
recreate=getattr(args, "recreate_venv", False),
)
# Optionally install or reinstall requirements.txt
if getattr(args,
@@ -1375,148 +1413,24 @@ def main(argv: Optional[list[str]] = None) -> int:
False):
req = find_requirements(dest)
if req and req.exists():
logging.info(
"Installing dependencies from %s into venv (reinstall=%s)",
req,
bool(getattr(args,
"reinstall_deps",
False)),
)
try:
subprocess.run(
[
str(venv_py),
"-m",
"pip",
"install",
"--upgrade",
"pip"
],
check=True,
install_requirements_into_venv(
venv_py,
dest,
req,
reinstall=getattr(args, "reinstall_deps", False),
)
if getattr(args, "reinstall_deps", False):
subprocess.run(
[
str(venv_py),
"-m",
"pip",
"install",
"--upgrade",
"--force-reinstall",
"-r",
str(req),
],
cwd=str(dest),
check=True,
)
else:
subprocess.run(
[
str(venv_py),
"-m",
"pip",
"install",
"-r",
str(req)
],
cwd=str(dest),
check=True,
)
logging.info("Dependencies installed successfully")
except subprocess.CalledProcessError as e:
logging.error("Failed to install dependencies: %s", e)
return 10
# Post-install verification: ensure packages are visible inside the venv
pkgs = parse_requirements_file(req)
if pkgs:
logging.info(
"Verifying installed packages inside the venv..."
)
any_missing = False
# Small mapping for known differences between package name and import name
import_map = {
"pyyaml": "yaml",
"pillow": "PIL",
"python-dateutil": "dateutil",
"beautifulsoup4": "bs4",
"pillow-heif": "pillow_heif",
"pillow-jxl-plugin": "pillow_jxl",
"pyopenssl": "OpenSSL",
"pysocks": "socks",
"service-identity": "service_identity",
"show-in-file-manager": "showinfm",
"opencv-python-headless": "cv2",
"mpv": "mpv",
"pyside6": "PySide6",
"pyside6-essentials": "PySide6",
"pyside6-addons": "PySide6",
}
for pkg in pkgs:
try:
out = subprocess.run(
[str(venv_py),
"-m",
"pip",
"show",
pkg],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
)
if out.returncode != 0 or not out.stdout.strip():
logging.warning(
"Package '%s' not found in venv (pip show failed).",
pkg
)
any_missing = True
continue
# Try import test for common mappings
import_name = import_map.get(pkg, pkg)
try:
subprocess.run(
[
str(venv_py),
"-c",
f"import {import_name}"
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
except subprocess.CalledProcessError:
if import_name == "mpv":
# python-mpv requires system libmpv; failure is common on server/headless envs
logging.info("Package '%s' is installed, but 'import %s' failed (likely missing system libmpv). This is usually non-critical.", pkg, import_name)
continue
logging.warning(
"Package '%s' appears installed but 'import %s' failed inside venv.",
pkg,
import_name,
)
any_missing = True
except Exception as exc:
logging.debug(
"Verification error for package %s: %s",
pkg,
exc
)
any_missing = True
if any_missing:
logging.warning(
"Some packages may not be importable in the venv. To re-install and verify, run:\n %s -m pip install -r %s\nThen run the client with:\n %s %s",
venv_py,
req,
venv_py,
dest / "hydrus_client.py",
)
else:
logging.debug(
"No parseable packages found in %s for verification; skipping further checks",
if not verify_requirements_in_venv(venv_py, req):
logging.warning(
"To re-install and verify, run:\n %s -m pip install -r %s\nThen run the client with:\n %s %s",
venv_py,
req,
venv_py,
dest / "hydrus_client.py",
)
else: