updated installer script
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user