updated installer script
This commit is contained in:
@@ -804,6 +804,7 @@ local _store_status_hint_for_url
|
|||||||
local _refresh_current_store_url_status
|
local _refresh_current_store_url_status
|
||||||
local _skip_next_store_check_url = ''
|
local _skip_next_store_check_url = ''
|
||||||
local _pick_folder_windows
|
local _pick_folder_windows
|
||||||
|
M._ytdl_download_format_fallbacks = M._ytdl_download_format_fallbacks or {}
|
||||||
|
|
||||||
function M._load_store_choices_direct_async(cb)
|
function M._load_store_choices_direct_async(cb)
|
||||||
cb = cb or function() end
|
cb = cb or function() end
|
||||||
@@ -2070,6 +2071,10 @@ function M._prepare_ytdl_format_for_web_load(url, reason)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if explicit_reload_url ~= '' and first_value and first_value ~= '' then
|
||||||
|
M._ytdl_download_format_fallbacks[explicit_reload_url] = first_value
|
||||||
|
end
|
||||||
|
|
||||||
pcall(mp.set_property, 'options/ytdl-format', '')
|
pcall(mp.set_property, 'options/ytdl-format', '')
|
||||||
pcall(mp.set_property, 'file-local-options/ytdl-format', '')
|
pcall(mp.set_property, 'file-local-options/ytdl-format', '')
|
||||||
pcall(mp.set_property, 'ytdl-format', '')
|
pcall(mp.set_property, 'ytdl-format', '')
|
||||||
@@ -2078,6 +2083,7 @@ function M._prepare_ytdl_format_for_web_load(url, reason)
|
|||||||
.. ' reason=' .. tostring(reason or 'on-load')
|
.. ' reason=' .. tostring(reason or 'on-load')
|
||||||
.. ' url=' .. tostring(url)
|
.. ' url=' .. tostring(url)
|
||||||
.. ' values=' .. table.concat(active_props, '; ')
|
.. ' values=' .. table.concat(active_props, '; ')
|
||||||
|
.. ' fallback_cached=' .. tostring(first_value and first_value ~= '' and 'yes' or 'no')
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -2119,6 +2125,28 @@ local function _normalize_url_for_store_lookup(url)
|
|||||||
return url:lower()
|
return url:lower()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function M._remember_ytdl_download_format(url, fmt)
|
||||||
|
local normalized = _normalize_url_for_store_lookup(url)
|
||||||
|
local value = trim(tostring(fmt or ''))
|
||||||
|
if normalized == '' or value == '' then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
M._ytdl_download_format_fallbacks[normalized] = value
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function M._get_remembered_ytdl_download_format(url)
|
||||||
|
local normalized = _normalize_url_for_store_lookup(url)
|
||||||
|
if normalized == '' then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
local cached = trim(tostring((M._ytdl_download_format_fallbacks or {})[normalized] or ''))
|
||||||
|
if cached == '' then
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
return cached
|
||||||
|
end
|
||||||
|
|
||||||
local function _build_store_lookup_needles(url)
|
local function _build_store_lookup_needles(url)
|
||||||
local out = {}
|
local out = {}
|
||||||
local seen = {}
|
local seen = {}
|
||||||
@@ -4879,6 +4907,7 @@ local function _current_ytdl_format_string()
|
|||||||
local fmt = trim(tostring(mp.get_property_native('ytdl-format') or ''))
|
local fmt = trim(tostring(mp.get_property_native('ytdl-format') or ''))
|
||||||
local suspicious_reason = M._suspicious_ytdl_format_reason(fmt, url, raw)
|
local suspicious_reason = M._suspicious_ytdl_format_reason(fmt, url, raw)
|
||||||
if fmt ~= '' and not suspicious_reason then
|
if fmt ~= '' and not suspicious_reason then
|
||||||
|
M._remember_ytdl_download_format(url, fmt)
|
||||||
return fmt
|
return fmt
|
||||||
elseif fmt ~= '' then
|
elseif fmt ~= '' then
|
||||||
_lua_log('ytdl-format: ignoring suspicious current format source=ytdl-format value=' .. tostring(fmt) .. ' reason=' .. tostring(suspicious_reason))
|
_lua_log('ytdl-format: ignoring suspicious current format source=ytdl-format value=' .. tostring(fmt) .. ' reason=' .. tostring(suspicious_reason))
|
||||||
@@ -4888,6 +4917,7 @@ local function _current_ytdl_format_string()
|
|||||||
local opt = trim(tostring(mp.get_property('options/ytdl-format') or ''))
|
local opt = trim(tostring(mp.get_property('options/ytdl-format') or ''))
|
||||||
suspicious_reason = M._suspicious_ytdl_format_reason(opt, url, raw)
|
suspicious_reason = M._suspicious_ytdl_format_reason(opt, url, raw)
|
||||||
if opt ~= '' and not suspicious_reason then
|
if opt ~= '' and not suspicious_reason then
|
||||||
|
M._remember_ytdl_download_format(url, opt)
|
||||||
return opt
|
return opt
|
||||||
elseif opt ~= '' then
|
elseif opt ~= '' then
|
||||||
_lua_log('ytdl-format: ignoring suspicious current format source=options/ytdl-format value=' .. tostring(opt) .. ' reason=' .. tostring(suspicious_reason))
|
_lua_log('ytdl-format: ignoring suspicious current format source=options/ytdl-format value=' .. tostring(opt) .. ' reason=' .. tostring(suspicious_reason))
|
||||||
@@ -4898,6 +4928,7 @@ local function _current_ytdl_format_string()
|
|||||||
local raw_format_id = tostring(raw.format_id)
|
local raw_format_id = tostring(raw.format_id)
|
||||||
suspicious_reason = M._suspicious_ytdl_format_reason(raw_format_id, url, raw)
|
suspicious_reason = M._suspicious_ytdl_format_reason(raw_format_id, url, raw)
|
||||||
if not suspicious_reason then
|
if not suspicious_reason then
|
||||||
|
M._remember_ytdl_download_format(url, raw_format_id)
|
||||||
return raw_format_id
|
return raw_format_id
|
||||||
end
|
end
|
||||||
_lua_log('ytdl-format: ignoring suspicious current format source=ytdl-raw-info.format_id value=' .. tostring(raw_format_id) .. ' reason=' .. tostring(suspicious_reason))
|
_lua_log('ytdl-format: ignoring suspicious current format source=ytdl-raw-info.format_id value=' .. tostring(raw_format_id) .. ' reason=' .. tostring(suspicious_reason))
|
||||||
@@ -4914,6 +4945,7 @@ local function _current_ytdl_format_string()
|
|||||||
local joined = table.concat(parts, '+')
|
local joined = table.concat(parts, '+')
|
||||||
suspicious_reason = M._suspicious_ytdl_format_reason(joined, url, raw)
|
suspicious_reason = M._suspicious_ytdl_format_reason(joined, url, raw)
|
||||||
if not suspicious_reason then
|
if not suspicious_reason then
|
||||||
|
M._remember_ytdl_download_format(url, joined)
|
||||||
return joined
|
return joined
|
||||||
end
|
end
|
||||||
_lua_log('ytdl-format: ignoring suspicious current format source=ytdl-raw-info.requested_formats value=' .. tostring(joined) .. ' reason=' .. tostring(suspicious_reason))
|
_lua_log('ytdl-format: ignoring suspicious current format source=ytdl-raw-info.requested_formats value=' .. tostring(joined) .. ' reason=' .. tostring(suspicious_reason))
|
||||||
@@ -4921,6 +4953,17 @@ local function _current_ytdl_format_string()
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if url ~= '' and _is_ytdlp_url(url) then
|
||||||
|
local remembered = M._get_remembered_ytdl_download_format(url)
|
||||||
|
suspicious_reason = M._suspicious_ytdl_format_reason(remembered, url, raw)
|
||||||
|
if remembered and not suspicious_reason then
|
||||||
|
_lua_log('ytdl-format: using remembered download fallback value=' .. tostring(remembered) .. ' url=' .. tostring(url))
|
||||||
|
return remembered
|
||||||
|
elseif remembered then
|
||||||
|
_lua_log('ytdl-format: ignoring suspicious remembered fallback value=' .. tostring(remembered) .. ' reason=' .. tostring(suspicious_reason))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -5200,6 +5243,7 @@ local function _apply_ytdl_format_and_reload(url, fmt)
|
|||||||
_lua_log('change-format: setting ytdl format=' .. tostring(fmt))
|
_lua_log('change-format: setting ytdl format=' .. tostring(fmt))
|
||||||
_skip_next_store_check_url = _normalize_url_for_store_lookup(url)
|
_skip_next_store_check_url = _normalize_url_for_store_lookup(url)
|
||||||
_set_current_web_url(url)
|
_set_current_web_url(url)
|
||||||
|
M._remember_ytdl_download_format(url, fmt)
|
||||||
pcall(mp.set_property, 'options/ytdl-format', tostring(fmt))
|
pcall(mp.set_property, 'options/ytdl-format', tostring(fmt))
|
||||||
pcall(mp.set_property, 'file-local-options/ytdl-format', tostring(fmt))
|
pcall(mp.set_property, 'file-local-options/ytdl-format', tostring(fmt))
|
||||||
pcall(mp.set_property, 'ytdl-format', tostring(fmt))
|
pcall(mp.set_property, 'ytdl-format', tostring(fmt))
|
||||||
|
|||||||
@@ -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
|
Note: This Python script is the canonical installer for the project — prefer
|
||||||
running `python ./scripts/bootstrap.py` locally. The platform scripts
|
running `python ./scripts/bootstrap.py` locally. The platform scripts
|
||||||
(`scripts/bootstrap.ps1` and `scripts/bootstrap.sh`) are now thin wrappers
|
(`scripts/bootstrap.ps1` and `scripts/bootstrap.sh`) are thin wrappers
|
||||||
that delegate to this script (they call it with `--no-delegate -q`).
|
that delegate to this script.
|
||||||
|
|
||||||
When invoked without any arguments, `bootstrap.py` will automatically select and
|
The install flow is owned here so `bootstrap.py` remains the single global
|
||||||
run the platform-specific bootstrap helper (`scripts/bootstrap.ps1` on Windows
|
entry point, while platform wrappers only provide shell-specific convenience.
|
||||||
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.
|
|
||||||
|
|
||||||
This file replaces the old `scripts/setup.py` to ensure the repository only has
|
This file replaces the old `scripts/setup.py` to ensure the repository only has
|
||||||
one `setup.py` (at the repository root) for packaging.
|
one `setup.py` (at the repository root) for packaging.
|
||||||
|
|
||||||
Usage:
|
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 --skip-deps
|
||||||
python ./scripts/bootstrap.py --playwright-only
|
python ./scripts/bootstrap.py --playwright-only
|
||||||
|
|
||||||
@@ -249,10 +244,16 @@ def run_platform_bootstrap(repo_root: Path) -> int:
|
|||||||
return int(rc.returncode or 0)
|
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:
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
return True
|
[interpreter, "-c", "import playwright"],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -446,7 +447,7 @@ def main() -> int:
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--no-delegate",
|
"--no-delegate",
|
||||||
action="store_true",
|
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(
|
parser.add_argument(
|
||||||
"-q",
|
"-q",
|
||||||
@@ -812,7 +813,7 @@ def main() -> int:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def _interactive_menu() -> str | int:
|
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:
|
try:
|
||||||
installed = _is_installed()
|
installed = _is_installed()
|
||||||
while True:
|
while True:
|
||||||
@@ -878,8 +879,7 @@ def main() -> int:
|
|||||||
if choice in ("q", "quit", "exit"):
|
if choice in ("q", "quit", "exit"):
|
||||||
return 0
|
return 0
|
||||||
except EOFError:
|
except EOFError:
|
||||||
# Non-interactive, fall back to delegating to platform helper
|
return "install"
|
||||||
return "delegate"
|
|
||||||
|
|
||||||
def _prompt_hydrus_install_location() -> tuple[Path, str] | None:
|
def _prompt_hydrus_install_location() -> tuple[Path, str] | None:
|
||||||
"""Ask the user for the Hydrus installation root and folder name."""
|
"""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:
|
def _clone_repo(url: str, dest: Path, depth: int = 1) -> bool:
|
||||||
"""Helper to clone a repository."""
|
"""Helper to clone a repository."""
|
||||||
try:
|
try:
|
||||||
cmd = ["git", "clone", url, str(dest)]
|
cmd = ["git", "clone"]
|
||||||
if depth:
|
if depth:
|
||||||
cmd.extend(["--depth", str(depth)])
|
cmd.extend(["--depth", str(depth)])
|
||||||
|
cmd.extend([url, str(dest)])
|
||||||
subprocess.check_call(cmd)
|
subprocess.check_call(cmd)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -1318,25 +1319,14 @@ def main() -> int:
|
|||||||
continue
|
continue
|
||||||
elif sel == "uninstall":
|
elif sel == "uninstall":
|
||||||
return _do_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:
|
elif sel == 0:
|
||||||
return 0
|
return 0
|
||||||
elif sel == "menu":
|
elif sel == "menu":
|
||||||
continue
|
continue
|
||||||
elif not args.no_delegate and script_path is not None:
|
|
||||||
# Default non-interactive behavior: delegate to platform script
|
if not is_in_repo:
|
||||||
rc = run_platform_bootstrap(repo_root)
|
if not _ensure_repo_available():
|
||||||
if rc != 0:
|
return 1
|
||||||
return rc
|
|
||||||
if not args.quiet:
|
|
||||||
print("Platform bootstrap completed successfully.")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if sys.version_info < (3, 8):
|
if sys.version_info < (3, 8):
|
||||||
print("Warning: Python 3.8+ is recommended.", file=sys.stderr)
|
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)
|
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
|
return 1
|
||||||
|
|
||||||
# Determine total steps for progress bar
|
should_install_deps = not args.skip_deps and not args.playwright_only
|
||||||
total_steps = 7 # Base: venv, pip, deps, project, cli, finalize, env
|
should_install_playwright = not args.no_playwright
|
||||||
if args.upgrade_pip: total_steps += 1
|
should_install_project = not args.playwright_only
|
||||||
if not args.no_playwright: total_steps += 1 # Playwright is combined pkg+browsers
|
|
||||||
if not getattr(args, "no_mpv", False): total_steps += 1
|
total_steps = 2
|
||||||
if not getattr(args, "no_deno", False): total_steps += 1
|
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)
|
pb = ProgressBar(total_steps, quiet=args.quiet or args.debug)
|
||||||
|
|
||||||
@@ -1437,17 +1442,12 @@ def main() -> int:
|
|||||||
pb.update("Checking for pip...")
|
pb.update("Checking for pip...")
|
||||||
_ensure_pip_available(venv_python)
|
_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:
|
try:
|
||||||
if args.playwright_only:
|
if args.playwright_only:
|
||||||
# Playwright browser install (short-circuit)
|
# Playwright browser install (short-circuit)
|
||||||
if not playwright_package_installed():
|
pb.update("Setting up Playwright and browsers...")
|
||||||
_run_cmd([sys.executable, "-m", "pip", "install", "--no-cache-dir", "playwright"])
|
if not playwright_package_installed(venv_python):
|
||||||
|
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "playwright"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cmd = _build_playwright_install_cmd(args.browsers)
|
cmd = _build_playwright_install_cmd(args.browsers)
|
||||||
@@ -1476,15 +1476,16 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 4. Core Dependencies
|
# 4. Core Dependencies
|
||||||
pb.update("Installing core dependencies...")
|
|
||||||
req_file = repo_root / "scripts" / "requirements.txt"
|
req_file = repo_root / "scripts" / "requirements.txt"
|
||||||
if req_file.exists():
|
if should_install_deps:
|
||||||
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "-r", str(req_file)])
|
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
|
# 5. Playwright Setup
|
||||||
if not args.no_playwright:
|
if should_install_playwright:
|
||||||
pb.update("Setting up Playwright and browsers...")
|
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"])
|
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "playwright"])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -1495,16 +1496,22 @@ def main() -> int:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# 6. Internal Components
|
# 6. Internal Components
|
||||||
pb.update("Installing internal components...")
|
if should_install_project:
|
||||||
if platform.system() != "Windows":
|
pb.update("Installing internal components...")
|
||||||
old_mm = venv_dir / "bin" / "mm"
|
if platform.system() != "Windows":
|
||||||
if old_mm.exists():
|
old_mm = venv_dir / "bin" / "mm"
|
||||||
try:
|
if old_mm.exists():
|
||||||
old_mm.unlink()
|
try:
|
||||||
except Exception:
|
old_mm.unlink()
|
||||||
pass
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
_run_cmd([str(venv_python), "-m", "pip", "install", "--no-cache-dir", "-e", str(repo_root / "scripts")])
|
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
|
# 7. CLI Verification
|
||||||
pb.update("Verifying CLI configuration...")
|
pb.update("Verifying CLI configuration...")
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ Examples:
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -156,7 +157,7 @@ def run_git_clone(
|
|||||||
|
|
||||||
def run_git_pull(git: str, dest: Path) -> None:
|
def run_git_pull(git: str, dest: Path) -> None:
|
||||||
logging.info("Updating git repository in %s", dest)
|
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:
|
def _sanitize_store_name(name: str) -> str:
|
||||||
@@ -672,6 +673,186 @@ def parse_requirements_file(req_path: Path) -> list[str]:
|
|||||||
return names
|
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:
|
def open_in_editor(path: Path) -> bool:
|
||||||
"""Open the file using the OS default opener.
|
"""Open the file using the OS default opener.
|
||||||
|
|
||||||
@@ -1034,34 +1215,14 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
if choice == "1":
|
if choice == "1":
|
||||||
# Install dependencies into the repository venv (create venv if needed)
|
# Install dependencies into the repository venv (create venv if needed)
|
||||||
try:
|
try:
|
||||||
venv_dir = dest / str(
|
venv_py = ensure_repo_venv(
|
||||||
getattr(args,
|
dest,
|
||||||
"venv_name",
|
getattr(args, "venv_name", ".venv"),
|
||||||
".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:
|
except Exception as e:
|
||||||
logging.error("Failed to prepare venv: %s", e)
|
logging.error("Failed to prepare venv: %s", e)
|
||||||
return 8
|
return 8
|
||||||
|
|
||||||
if not venv_py:
|
|
||||||
logging.error(
|
|
||||||
"Could not locate python in venv %s",
|
|
||||||
venv_dir
|
|
||||||
)
|
|
||||||
return 9
|
|
||||||
|
|
||||||
req = find_requirements(dest)
|
req = find_requirements(dest)
|
||||||
if not req:
|
if not req:
|
||||||
logging.info(
|
logging.info(
|
||||||
@@ -1070,96 +1231,18 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
logging.info(
|
|
||||||
"Installing dependencies from %s into venv",
|
|
||||||
req
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
install_requirements_into_venv(
|
||||||
[
|
venv_py,
|
||||||
str(venv_py),
|
dest,
|
||||||
"-m",
|
req,
|
||||||
"pip",
|
reinstall=getattr(args, "reinstall_deps", False),
|
||||||
"install",
|
|
||||||
"--upgrade",
|
|
||||||
"pip"
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
)
|
)
|
||||||
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:
|
except subprocess.CalledProcessError as e:
|
||||||
logging.error("Failed to install dependencies: %s", e)
|
logging.error("Failed to install dependencies: %s", e)
|
||||||
return 10
|
return 10
|
||||||
|
|
||||||
# Post-install verification
|
verify_requirements_in_venv(venv_py, req)
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
@@ -1180,24 +1263,11 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
elif choice == "3":
|
elif choice == "3":
|
||||||
# Install a user-level service to start the hydrus client on boot
|
# Install a user-level service to start the hydrus client on boot
|
||||||
try:
|
try:
|
||||||
venv_dir = dest / str(
|
venv_py = ensure_repo_venv(
|
||||||
getattr(args,
|
dest,
|
||||||
"venv_name",
|
getattr(args, "venv_name", ".venv"),
|
||||||
".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:
|
except Exception as e:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Failed to prepare venv for service install: %s",
|
"Failed to prepare venv for service install: %s",
|
||||||
@@ -1205,13 +1275,6 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
)
|
)
|
||||||
return 8
|
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
|
# Prefer the helper script inside the repo if present, else use installed helper
|
||||||
script_dir = Path(__file__).resolve().parent
|
script_dir = Path(__file__).resolve().parent
|
||||||
helper_candidates = [
|
helper_candidates = [
|
||||||
@@ -1336,36 +1399,11 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
# Post-obtain setup: create repository-local venv (unless disabled)
|
# Post-obtain setup: create repository-local venv (unless disabled)
|
||||||
if not getattr(args, "no_venv", False):
|
if not getattr(args, "no_venv", False):
|
||||||
try:
|
try:
|
||||||
venv_py = None
|
venv_py = ensure_repo_venv(
|
||||||
venv_dir = dest / str(getattr(args, "venv_name", ".venv"))
|
dest,
|
||||||
if venv_dir.exists():
|
getattr(args, "venv_name", ".venv"),
|
||||||
if getattr(args, "recreate_venv", False):
|
recreate=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)
|
|
||||||
|
|
||||||
# Optionally install or reinstall requirements.txt
|
# Optionally install or reinstall requirements.txt
|
||||||
if getattr(args,
|
if getattr(args,
|
||||||
@@ -1375,148 +1413,24 @@ def main(argv: Optional[list[str]] = None) -> int:
|
|||||||
False):
|
False):
|
||||||
req = find_requirements(dest)
|
req = find_requirements(dest)
|
||||||
if req and req.exists():
|
if req and req.exists():
|
||||||
logging.info(
|
|
||||||
"Installing dependencies from %s into venv (reinstall=%s)",
|
|
||||||
req,
|
|
||||||
bool(getattr(args,
|
|
||||||
"reinstall_deps",
|
|
||||||
False)),
|
|
||||||
)
|
|
||||||
try:
|
try:
|
||||||
subprocess.run(
|
install_requirements_into_venv(
|
||||||
[
|
venv_py,
|
||||||
str(venv_py),
|
dest,
|
||||||
"-m",
|
req,
|
||||||
"pip",
|
reinstall=getattr(args, "reinstall_deps", False),
|
||||||
"install",
|
|
||||||
"--upgrade",
|
|
||||||
"pip"
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
)
|
)
|
||||||
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:
|
except subprocess.CalledProcessError as e:
|
||||||
logging.error("Failed to install dependencies: %s", e)
|
logging.error("Failed to install dependencies: %s", e)
|
||||||
return 10
|
return 10
|
||||||
|
|
||||||
# Post-install verification: ensure packages are visible inside the venv
|
if not verify_requirements_in_venv(venv_py, req):
|
||||||
pkgs = parse_requirements_file(req)
|
logging.warning(
|
||||||
if pkgs:
|
"To re-install and verify, run:\n %s -m pip install -r %s\nThen run the client with:\n %s %s",
|
||||||
logging.info(
|
venv_py,
|
||||||
"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",
|
|
||||||
req,
|
req,
|
||||||
|
venv_py,
|
||||||
|
dest / "hydrus_client.py",
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user