updated installer script
This commit is contained in:
@@ -14,22 +14,17 @@ ffmpeg-python is installed as a dependency, but requires ffmpeg itself to be on
|
||||
|
||||
Note: This Python script is the canonical installer for the project — prefer
|
||||
running `python ./scripts/bootstrap.py` locally. The platform scripts
|
||||
(`scripts/bootstrap.ps1` and `scripts/bootstrap.sh`) are now thin wrappers
|
||||
that delegate to this script (they call it with `--no-delegate -q`).
|
||||
(`scripts/bootstrap.ps1` and `scripts/bootstrap.sh`) are thin wrappers
|
||||
that delegate to this script.
|
||||
|
||||
When invoked without any arguments, `bootstrap.py` will automatically select and
|
||||
run the platform-specific bootstrap helper (`scripts/bootstrap.ps1` on Windows
|
||||
or `scripts/bootstrap.sh` on POSIX) in **non-interactive (quiet)** mode so a
|
||||
single `python ./scripts/bootstrap.py` call does the usual bootstrap on your OS.
|
||||
|
||||
The platform bootstrap scripts also attempt (best-effort) to install `mpv` if
|
||||
it is not found on your PATH, since some workflows use it.
|
||||
The install flow is owned here so `bootstrap.py` remains the single global
|
||||
entry point, while platform wrappers only provide shell-specific convenience.
|
||||
|
||||
This file replaces the old `scripts/setup.py` to ensure the repository only has
|
||||
one `setup.py` (at the repository root) for packaging.
|
||||
|
||||
Usage:
|
||||
python ./scripts/bootstrap.py # install deps and playwright browsers (or run platform bootstrap if no args)
|
||||
python ./scripts/bootstrap.py # install deps and playwright browsers
|
||||
python ./scripts/bootstrap.py --skip-deps
|
||||
python ./scripts/bootstrap.py --playwright-only
|
||||
|
||||
@@ -249,10 +244,16 @@ def run_platform_bootstrap(repo_root: Path) -> int:
|
||||
return int(rc.returncode or 0)
|
||||
|
||||
|
||||
def playwright_package_installed() -> bool:
|
||||
def playwright_package_installed(python_path: Optional[Path] = None) -> bool:
|
||||
interpreter = str(python_path or sys.executable)
|
||||
try:
|
||||
|
||||
return True
|
||||
result = subprocess.run(
|
||||
[interpreter, "-c", "import playwright"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=False,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -446,7 +447,7 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--no-delegate",
|
||||
action="store_true",
|
||||
help="Do not delegate to platform bootstrap scripts; run the Python bootstrap directly.",
|
||||
help="Legacy no-op retained for wrapper compatibility; Python bootstrap is always the canonical entry point.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
@@ -812,7 +813,7 @@ def main() -> int:
|
||||
return False
|
||||
|
||||
def _interactive_menu() -> str | int:
|
||||
"""Show a simple interactive menu to choose install/uninstall or delegate."""
|
||||
"""Show a simple interactive menu to choose install/uninstall tasks."""
|
||||
try:
|
||||
installed = _is_installed()
|
||||
while True:
|
||||
@@ -878,8 +879,7 @@ def main() -> int:
|
||||
if choice in ("q", "quit", "exit"):
|
||||
return 0
|
||||
except EOFError:
|
||||
# Non-interactive, fall back to delegating to platform helper
|
||||
return "delegate"
|
||||
return "install"
|
||||
|
||||
def _prompt_hydrus_install_location() -> tuple[Path, str] | None:
|
||||
"""Ask the user for the Hydrus installation root and folder name."""
|
||||
@@ -907,9 +907,10 @@ def main() -> int:
|
||||
def _clone_repo(url: str, dest: Path, depth: int = 1) -> bool:
|
||||
"""Helper to clone a repository."""
|
||||
try:
|
||||
cmd = ["git", "clone", url, str(dest)]
|
||||
cmd = ["git", "clone"]
|
||||
if depth:
|
||||
cmd.extend(["--depth", str(depth)])
|
||||
cmd.extend([url, str(dest)])
|
||||
subprocess.check_call(cmd)
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -1318,25 +1319,14 @@ def main() -> int:
|
||||
continue
|
||||
elif sel == "uninstall":
|
||||
return _do_uninstall()
|
||||
elif sel == "delegate":
|
||||
rc = run_platform_bootstrap(repo_root)
|
||||
if rc != 0:
|
||||
return rc
|
||||
if not args.quiet:
|
||||
print("Platform bootstrap completed successfully.")
|
||||
return 0
|
||||
elif sel == 0:
|
||||
return 0
|
||||
elif sel == "menu":
|
||||
continue
|
||||
elif not args.no_delegate and script_path is not None:
|
||||
# Default non-interactive behavior: delegate to platform script
|
||||
rc = run_platform_bootstrap(repo_root)
|
||||
if rc != 0:
|
||||
return rc
|
||||
if not args.quiet:
|
||||
print("Platform bootstrap completed successfully.")
|
||||
return 0
|
||||
|
||||
if not is_in_repo:
|
||||
if not _ensure_repo_available():
|
||||
return 1
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
print("Warning: Python 3.8+ is recommended.", file=sys.stderr)
|
||||
@@ -1360,12 +1350,27 @@ def main() -> int:
|
||||
print("Error: No project repository found. Please ensure you are running this script inside the project folder or follow the interactive install prompts.", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Determine total steps for progress bar
|
||||
total_steps = 7 # Base: venv, pip, deps, project, cli, finalize, env
|
||||
if args.upgrade_pip: total_steps += 1
|
||||
if not args.no_playwright: total_steps += 1 # Playwright is combined pkg+browsers
|
||||
if not getattr(args, "no_mpv", False): total_steps += 1
|
||||
if not getattr(args, "no_deno", False): total_steps += 1
|
||||
should_install_deps = not args.skip_deps and not args.playwright_only
|
||||
should_install_playwright = not args.no_playwright
|
||||
should_install_project = not args.playwright_only
|
||||
|
||||
total_steps = 2
|
||||
if args.playwright_only:
|
||||
total_steps += 1
|
||||
else:
|
||||
if args.upgrade_pip:
|
||||
total_steps += 1
|
||||
if should_install_deps:
|
||||
total_steps += 1
|
||||
if should_install_playwright:
|
||||
total_steps += 1
|
||||
if should_install_project:
|
||||
total_steps += 1
|
||||
total_steps += 3
|
||||
if not getattr(args, "no_mpv", False):
|
||||
total_steps += 1
|
||||
if not getattr(args, "no_deno", False):
|
||||
total_steps += 1
|
||||
|
||||
pb = ProgressBar(total_steps, quiet=args.quiet or args.debug)
|
||||
|
||||
@@ -1437,17 +1442,12 @@ def main() -> int:
|
||||
pb.update("Checking for pip...")
|
||||
_ensure_pip_available(venv_python)
|
||||
|
||||
# Enforce opinionated behavior: install deps, playwright, deno, and install project in editable mode.
|
||||
# Ignore `--skip-deps` and `--install-editable` flags to keep the setup deterministic.
|
||||
args.skip_deps = False
|
||||
args.install_editable = True
|
||||
args.no_playwright = False
|
||||
|
||||
try:
|
||||
if args.playwright_only:
|
||||
# Playwright browser install (short-circuit)
|
||||
if not playwright_package_installed():
|
||||
_run_cmd([sys.executable, "-m", "pip", "install", "--no-cache-dir", "playwright"])
|
||||
pb.update("Setting up Playwright and browsers...")
|
||||
if not playwright_package_installed(venv_python):
|
||||
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "playwright"])
|
||||
|
||||
try:
|
||||
cmd = _build_playwright_install_cmd(args.browsers)
|
||||
@@ -1476,15 +1476,16 @@ def main() -> int:
|
||||
)
|
||||
|
||||
# 4. Core Dependencies
|
||||
pb.update("Installing core dependencies...")
|
||||
req_file = repo_root / "scripts" / "requirements.txt"
|
||||
if req_file.exists():
|
||||
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "-r", str(req_file)])
|
||||
if should_install_deps:
|
||||
pb.update("Installing core dependencies...")
|
||||
if req_file.exists():
|
||||
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "-r", str(req_file)])
|
||||
|
||||
# 5. Playwright Setup
|
||||
if not args.no_playwright:
|
||||
if should_install_playwright:
|
||||
pb.update("Setting up Playwright and browsers...")
|
||||
if not playwright_package_installed():
|
||||
if not playwright_package_installed(venv_python):
|
||||
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "playwright"])
|
||||
|
||||
try:
|
||||
@@ -1495,16 +1496,22 @@ def main() -> int:
|
||||
pass
|
||||
|
||||
# 6. Internal Components
|
||||
pb.update("Installing internal components...")
|
||||
if platform.system() != "Windows":
|
||||
old_mm = venv_dir / "bin" / "mm"
|
||||
if old_mm.exists():
|
||||
try:
|
||||
old_mm.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "-e", str(repo_root / "scripts")])
|
||||
if should_install_project:
|
||||
pb.update("Installing internal components...")
|
||||
if platform.system() != "Windows":
|
||||
old_mm = venv_dir / "bin" / "mm"
|
||||
if old_mm.exists():
|
||||
try:
|
||||
old_mm.unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
project_cmd = [str(venv_python), "-m", "pip", "install", "--no-cache-dir"]
|
||||
if args.install_editable:
|
||||
project_cmd.extend(["-e", str(repo_root / "scripts")])
|
||||
else:
|
||||
project_cmd.append(str(repo_root / "scripts"))
|
||||
_run_cmd(project_cmd)
|
||||
|
||||
# 7. CLI Verification
|
||||
pb.update("Verifying CLI configuration...")
|
||||
|
||||
@@ -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