nh
This commit is contained in:
@@ -526,41 +526,10 @@ try {
|
||||
$globalBin = Join-Path $env:USERPROFILE 'bin'
|
||||
New-Item -ItemType Directory -Path $globalBin -Force | Out-Null
|
||||
|
||||
$mmCmd = Join-Path $globalBin 'mm.cmd'
|
||||
$mmPs1 = Join-Path $globalBin 'mm.ps1'
|
||||
|
||||
$repo = $repoRoot
|
||||
|
||||
$cmdText = @"
|
||||
@echo off
|
||||
set "REPO=__REPO__"
|
||||
if exist "%REPO%\.venv\Scripts\mm.exe" "%REPO%\.venv\Scripts\mm.exe" %*
|
||||
if defined MM_DEBUG (
|
||||
echo MM_DEBUG: REPO=%REPO%
|
||||
if exist "%REPO%\.venv\Scripts\python.exe" (
|
||||
"%REPO%\.venv\Scripts\python.exe" -c "import sys,importlib,importlib.util; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);"
|
||||
) else (
|
||||
python -c "import sys,importlib,importlib.util; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);"
|
||||
)
|
||||
)
|
||||
if exist "%REPO%\.venv\Scripts\python.exe" (
|
||||
"%REPO%\.venv\Scripts\python.exe" -m medeia_macina.cli_entry %*
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
if exist "%REPO%\CLI.py" (
|
||||
python "%REPO%\CLI.py" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
)
|
||||
python -m medeia_macina.cli_entry %*
|
||||
"@
|
||||
# Inject actual repo path safely (escape double-quotes if any)
|
||||
$cmdText = $cmdText.Replace('__REPO__', $repo.Replace('"', '""'))
|
||||
if (Test-Path $mmCmd) {
|
||||
$bak = "$mmCmd.bak$(Get-Date -UFormat %s)"
|
||||
Move-Item -Path $mmCmd -Destination $bak -Force
|
||||
}
|
||||
Set-Content -LiteralPath $mmCmd -Value $cmdText -Encoding UTF8
|
||||
|
||||
# PowerShell shim: use single-quoted here-string so literal PowerShell variables
|
||||
# (like $args) are not expanded by this script when writing the file.
|
||||
$ps1Text = @'
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
Unified project bootstrap helper (Python-only).
|
||||
|
||||
This script installs Python dependencies from `requirements.txt` and then
|
||||
This script installs Python dependencies from `scripts/requirements.txt` and then
|
||||
downloads Playwright browser binaries by running `python -m playwright install`.
|
||||
By default this script installs **Chromium** only to conserve space; pass
|
||||
`--browsers all` to install all supported engines (chromium, firefox, webkit).
|
||||
@@ -30,7 +30,7 @@ Usage:
|
||||
python ./scripts/bootstrap.py --playwright-only
|
||||
|
||||
Optional flags:
|
||||
--skip-deps Skip `pip install -r requirements.txt` step
|
||||
--skip-deps Skip `pip install -r scripts/requirements.txt` step
|
||||
--no-playwright Skip running `python -m playwright install` (still installs deps)
|
||||
--playwright-only Install only Playwright browsers (installs playwright package if missing)
|
||||
--browsers Comma-separated list of Playwright browsers to install (default: chromium)
|
||||
@@ -223,7 +223,7 @@ def main() -> int:
|
||||
parser.add_argument(
|
||||
"--skip-deps",
|
||||
action="store_true",
|
||||
help="Skip installing Python dependencies from requirements.txt",
|
||||
help="Skip installing Python dependencies from scripts/requirements.txt",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-playwright",
|
||||
@@ -317,12 +317,102 @@ def main() -> int:
|
||||
return False
|
||||
|
||||
def _do_uninstall() -> int:
|
||||
"""Attempt to remove the local venv and any shims written to the user's bin."""
|
||||
"""Attempt to remove the local venv and any shims written to the user's bin.
|
||||
|
||||
If this script is running using the Python inside the local `.venv`, we
|
||||
attempt to re-run the uninstall using a Python interpreter outside the
|
||||
venv (so files can be removed on Windows). If no suitable external
|
||||
interpreter can be found, the user is asked to deactivate the venv and
|
||||
re-run the uninstall.
|
||||
"""
|
||||
vdir = repo_root / ".venv"
|
||||
if not vdir.exists():
|
||||
if not args.quiet:
|
||||
print("No local .venv found; nothing to uninstall.")
|
||||
return 0
|
||||
|
||||
# If the current interpreter is the one inside the local venv, try to
|
||||
# run the uninstall via a Python outside the venv so files (including
|
||||
# the interpreter binary) can be removed on Windows.
|
||||
try:
|
||||
current_exe = Path(sys.executable).resolve()
|
||||
in_venv = str(current_exe).lower().startswith(str(vdir.resolve()).lower())
|
||||
except Exception:
|
||||
in_venv = False
|
||||
|
||||
if in_venv:
|
||||
if not args.quiet:
|
||||
print(f"Detected local venv Python in use: {current_exe}")
|
||||
|
||||
if not args.yes:
|
||||
try:
|
||||
resp = input("Uninstall will be attempted using a system Python outside the .venv. Continue? [Y/n]: ")
|
||||
except EOFError:
|
||||
print("Non-interactive environment; pass --uninstall --yes to uninstall without prompts.", file=sys.stderr)
|
||||
return 2
|
||||
if resp.strip().lower() in ("n", "no"):
|
||||
print("Uninstall aborted.")
|
||||
return 1
|
||||
|
||||
def _find_external_python() -> list[str] | None:
|
||||
"""Return a command (list) for a Python interpreter outside the venv, or None."""
|
||||
try:
|
||||
base = Path(sys.base_prefix)
|
||||
candidates: list[Path | str] = []
|
||||
if platform.system().lower() == "windows":
|
||||
candidates.append(base / "python.exe")
|
||||
else:
|
||||
candidates.extend([base / "bin" / "python3", base / "bin" / "python"])
|
||||
|
||||
for name in ("python3", "python"):
|
||||
p = shutil.which(name)
|
||||
if p:
|
||||
candidates.append(Path(p))
|
||||
|
||||
# Special-case the Windows py launcher: ensure it resolves
|
||||
# to a Python outside the venv before returning ['py','-3']
|
||||
if platform.system().lower() == "windows":
|
||||
py_launcher = shutil.which("py")
|
||||
if py_launcher:
|
||||
try:
|
||||
out = subprocess.check_output(["py", "-3", "-c", "import sys; print(sys.executable)"], text=True).strip()
|
||||
if out and not str(Path(out).resolve()).lower().startswith(str(vdir.resolve()).lower()):
|
||||
return ["py", "-3"]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for c in candidates:
|
||||
try:
|
||||
if isinstance(c, Path) and c.exists():
|
||||
c_resolved = Path(c).resolve()
|
||||
if not str(c_resolved).lower().startswith(str(vdir.resolve()).lower()) and c_resolved != current_exe:
|
||||
return [str(c_resolved)]
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
ext = _find_external_python()
|
||||
if ext:
|
||||
cmd = ext + [str(repo_root / "scripts" / "bootstrap.py"), "--uninstall", "--yes"]
|
||||
if not args.quiet:
|
||||
print("Attempting uninstall using external Python:", " ".join(cmd))
|
||||
rc = subprocess.run(cmd)
|
||||
if rc.returncode != 0:
|
||||
print(
|
||||
f"External uninstall exited with {rc.returncode}; ensure no processes are using files in {vdir} and try again.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return int(rc.returncode or 0)
|
||||
|
||||
print(
|
||||
"Could not find a Python interpreter outside the local .venv. Please deactivate your venv (run 'deactivate') or run the uninstall from a system Python:\n python ./scripts/bootstrap.py --uninstall --yes",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
|
||||
# Normal (non-venv) uninstall flow: confirm and remove launchers, shims, and venv
|
||||
if not args.yes:
|
||||
try:
|
||||
prompt = input(f"Remove local virtualenv at {vdir} and installed user shims? [y/N]: ")
|
||||
@@ -334,15 +424,19 @@ def main() -> int:
|
||||
return 1
|
||||
|
||||
# Remove repo-local launchers
|
||||
for name in ("mm", "mm.ps1", "mm.bat"):
|
||||
p = repo_root / name
|
||||
if p.exists():
|
||||
def _remove_launcher(path: Path) -> None:
|
||||
if path.exists():
|
||||
try:
|
||||
p.unlink()
|
||||
path.unlink()
|
||||
if not args.quiet:
|
||||
print(f"Removed local launcher: {p}")
|
||||
print(f"Removed local launcher: {path}")
|
||||
except Exception as exc:
|
||||
print(f"Warning: failed to remove {p}: {exc}", file=sys.stderr)
|
||||
print(f"Warning: failed to remove {path}: {exc}", file=sys.stderr)
|
||||
|
||||
scripts_launcher = repo_root / "scripts" / "mm.ps1"
|
||||
_remove_launcher(scripts_launcher)
|
||||
for legacy in ("mm", "mm.ps1", "mm.bat"):
|
||||
_remove_launcher(repo_root / legacy)
|
||||
|
||||
# Remove user shims that the installer may have written
|
||||
try:
|
||||
@@ -350,12 +444,15 @@ def main() -> int:
|
||||
if system == "windows":
|
||||
user_bin = Path(os.environ.get("USERPROFILE", str(Path.home()))) / "bin"
|
||||
if user_bin.exists():
|
||||
for name in ("mm.cmd", "mm.ps1"):
|
||||
for name in ("mm.ps1",):
|
||||
p = user_bin / name
|
||||
if p.exists():
|
||||
p.unlink()
|
||||
if not args.quiet:
|
||||
print(f"Removed user shim: {p}")
|
||||
try:
|
||||
p.unlink()
|
||||
if not args.quiet:
|
||||
print(f"Removed user shim: {p}")
|
||||
except Exception as exc:
|
||||
print(f"Warning: failed to remove {p}: {exc}", file=sys.stderr)
|
||||
else:
|
||||
user_bin = Path(os.environ.get("XDG_BIN_HOME", str(Path.home() / ".local/bin")))
|
||||
if user_bin.exists():
|
||||
@@ -480,11 +577,37 @@ def main() -> int:
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Failed to create or prepare local venv: {exc}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
def _ensure_pip_available(python_path: Path) -> None:
|
||||
"""Ensure pip is available inside the venv; fall back to ensurepip if needed."""
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
[str(python_path), "-m", "pip", "--version"],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
check=True,
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not args.quiet:
|
||||
print("Bootstrapping pip inside the local virtualenv...")
|
||||
try:
|
||||
run([str(python_path), "-m", "ensurepip", "--upgrade"])
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(
|
||||
"Failed to install pip inside the local virtualenv via ensurepip; ensure your Python build includes ensurepip and retry.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise
|
||||
|
||||
# Ensure a local venv is present and use it for subsequent installs.
|
||||
venv_python = _ensure_local_venv()
|
||||
if not args.quiet:
|
||||
print(f"Using venv python: {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.
|
||||
@@ -531,7 +654,7 @@ def main() -> int:
|
||||
)
|
||||
|
||||
if not args.skip_deps:
|
||||
req_file = repo_root / "requirements.txt"
|
||||
req_file = repo_root / "scripts" / "requirements.txt"
|
||||
if not req_file.exists():
|
||||
print(
|
||||
f"requirements.txt not found at {req_file}; skipping dependency installation.",
|
||||
@@ -662,35 +785,15 @@ def main() -> int:
|
||||
print("Deno installation failed.", file=sys.stderr)
|
||||
return rc
|
||||
|
||||
# Write project-local launcher scripts (project root) that prefer the local .venv
|
||||
# Write project-local launcher script under scripts/ to keep the repo root uncluttered.
|
||||
def _write_launchers() -> None:
|
||||
sh = repo_root / "mm"
|
||||
ps1 = repo_root / "mm.ps1"
|
||||
bat = repo_root / "mm.bat"
|
||||
|
||||
sh_text = """#!/usr/bin/env bash
|
||||
set -e
|
||||
SCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"
|
||||
REPO=\"$SCRIPT_DIR\"
|
||||
VENV=\"$REPO/.venv\"
|
||||
# Make tools installed into the local venv available in PATH for provider discovery
|
||||
export PATH=\"$VENV/bin:$PATH\"
|
||||
PY=\"$VENV/bin/python\"
|
||||
if [ -x \"$PY\" ]; then
|
||||
exec \"$PY\" -m medeia_macina.cli_entry \"$@\"
|
||||
else
|
||||
exec python -m medeia_macina.cli_entry \"$@\"
|
||||
fi
|
||||
"""
|
||||
try:
|
||||
sh.write_text(sh_text, encoding="utf-8")
|
||||
sh.chmod(sh.stat().st_mode | 0o111)
|
||||
except Exception:
|
||||
pass
|
||||
launcher_dir = repo_root / "scripts"
|
||||
launcher_dir.mkdir(parents=True, exist_ok=True)
|
||||
ps1 = launcher_dir / "mm.ps1"
|
||||
|
||||
ps1_text = r"""Param([Parameter(ValueFromRemainingArguments=$true)] $args)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repo = $scriptDir
|
||||
$repo = (Resolve-Path (Join-Path $scriptDir "..")).Path
|
||||
$venv = Join-Path $repo '.venv'
|
||||
# Ensure venv Scripts dir is on PATH for provider discovery
|
||||
$venvScripts = Join-Path $venv 'Scripts'
|
||||
@@ -707,19 +810,6 @@ python -m medeia_macina.cli_entry @args
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
bat_text = (
|
||||
"@echo off\r\n"
|
||||
"set SCRIPT_DIR=%~dp0\r\n"
|
||||
"set PATH=%SCRIPT_DIR%\\.venv\\Scripts;%PATH%\r\n"
|
||||
'if exist "%SCRIPT_DIR%\\.venv\\Scripts\\python.exe" "%SCRIPT_DIR%\\.venv\\Scripts\\python.exe" -m medeia_macina.cli_entry %*\r\n'
|
||||
'if exist "%SCRIPT_DIR%\\CLI.py" python "%SCRIPT_DIR%\\CLI.py" %*\r\n'
|
||||
"python -m medeia_macina.cli_entry %*\r\n"
|
||||
)
|
||||
try:
|
||||
bat.write_text(bat_text, encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_write_launchers()
|
||||
|
||||
# Install user-global shims so `mm` can be executed from any shell session.
|
||||
@@ -732,24 +822,6 @@ python -m medeia_macina.cli_entry @args
|
||||
user_bin = Path(os.environ.get("USERPROFILE", str(home))) / "bin"
|
||||
user_bin.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write mm.cmd (CMD shim)
|
||||
mm_cmd = user_bin / "mm.cmd"
|
||||
cmd_text = (
|
||||
f"@echo off\r\n"
|
||||
f"set REPO={repo}\r\n"
|
||||
f'if exist "%REPO%\\.venv\\Scripts\\mm.exe" "%REPO%\\.venv\\Scripts\\mm.exe" %*\r\n'
|
||||
f"if defined MM_DEBUG (\r\n"
|
||||
f" echo MM_DEBUG: REPO=%REPO%\r\n"
|
||||
f' if exist "%REPO%\\.venv\\Scripts\\python.exe" "%REPO%\\.venv\\Scripts\\python.exe" -c "import sys,importlib,importlib.util; print(\'sys.executable:\', sys.executable); print(\'sys.path (first 8):\', sys.path[:8]);" \r\n'
|
||||
f")\r\n"
|
||||
f'if exist "%REPO%\\.venv\\Scripts\\python.exe" "%REPO%\\.venv\\Scripts\\python.exe" -m medeia_macina.cli_entry %*\r\n'
|
||||
f"python -m medeia_macina.cli_entry %*\r\n"
|
||||
)
|
||||
if mm_cmd.exists():
|
||||
bak = mm_cmd.with_suffix(f".bak{int(time.time())}")
|
||||
mm_cmd.replace(bak)
|
||||
mm_cmd.write_text(cmd_text, encoding="utf-8")
|
||||
|
||||
# Write mm.ps1 (PowerShell shim)
|
||||
mm_ps1 = user_bin / "mm.ps1"
|
||||
ps1_text = (
|
||||
|
||||
@@ -10,7 +10,7 @@ Works on Linux and Windows. Behavior:
|
||||
2) Update hydrus (git pull)
|
||||
3) Re-clone (remove and re-clone)
|
||||
- If `git` is not available, the script will fall back to downloading the repository ZIP and extracting it.
|
||||
- By default the script will create a repository-local virtual environment `./<dest>/.venv` after cloning/extraction; use `--no-venv` to skip this. By default the script will install dependencies from `requirements.txt` into that venv (use `--no-install-deps` to skip). After setup the script will print instructions for running the client; use `--run-client` to *launch* `hydrus_client.py` using the created repo venv's Python (use `--run-client-detached` to run it in the background).
|
||||
- By default the script will create a repository-local virtual environment `./<dest>/.venv` after cloning/extraction; use `--no-venv` to skip this. By default the script will install dependencies from `scripts/requirements.txt` into that venv (use `--no-install-deps` to skip). After setup the script will print instructions for running the client; use `--run-client` to *launch* `hydrus_client.py` using the created repo venv's Python (use `--run-client-detached` to run it in the background).
|
||||
|
||||
Examples:
|
||||
python scripts/hydrusnetwork.py
|
||||
@@ -510,17 +510,18 @@ def fix_permissions(
|
||||
|
||||
|
||||
def find_requirements(root: Path) -> Optional[Path]:
|
||||
"""Return a requirements.txt Path if found in common locations (root, client, requirements) or via a shallow search.
|
||||
"""Return a requirements.txt Path if found in common locations (scripts, root, client, requirements) or via a shallow search.
|
||||
|
||||
This tries a few sensible locations used by various projects and performs a shallow
|
||||
two-level walk to find a requirements.txt so installation works even if the file is
|
||||
not at the repository root.
|
||||
"""
|
||||
candidates = [
|
||||
root / "requirements.txt",
|
||||
root / "client" / "requirements.txt",
|
||||
root / "requirements" / "requirements.txt",
|
||||
]
|
||||
candidates = [
|
||||
root / "scripts" / "requirements.txt",
|
||||
root / "requirements.txt",
|
||||
root / "client" / "requirements.txt",
|
||||
root / "requirements" / "requirements.txt",
|
||||
]
|
||||
for c in candidates:
|
||||
if c.exists():
|
||||
return c
|
||||
|
||||
29
scripts/requirements-dev.txt
Normal file
29
scripts/requirements-dev.txt
Normal file
@@ -0,0 +1,29 @@
|
||||
# Development dependencies for Medeia-Macina
|
||||
# Install with: pip install -r requirements-dev.txt
|
||||
|
||||
# Main requirements
|
||||
-r requirements.txt
|
||||
|
||||
# Testing
|
||||
pytest>=7.4.0
|
||||
pytest-cov>=4.1.0
|
||||
pytest-asyncio>=0.21.0
|
||||
|
||||
# Code quality
|
||||
black>=23.11.0
|
||||
flake8>=6.1.0
|
||||
isort>=5.12.0
|
||||
mypy>=1.7.0
|
||||
pylint>=3.0.0
|
||||
|
||||
# Documentation
|
||||
sphinx>=7.2.0
|
||||
sphinx-rtd-theme>=1.3.0
|
||||
|
||||
# Debugging and profiling
|
||||
ipython>=8.17.0
|
||||
ipdb>=0.13.0
|
||||
memory-profiler>=0.61.0
|
||||
|
||||
# Version control and CI/CD helpers
|
||||
pre-commit>=3.5.0
|
||||
47
scripts/requirements.txt
Normal file
47
scripts/requirements.txt
Normal file
@@ -0,0 +1,47 @@
|
||||
# Core CLI and TUI frameworks
|
||||
typer>=0.9.0
|
||||
rich>=13.7.0
|
||||
prompt-toolkit>=3.0.0
|
||||
textual>=0.30.0
|
||||
|
||||
# Media processing and downloading
|
||||
yt-dlp[default]>=2023.11.0
|
||||
requests>=2.31.0
|
||||
httpx>=0.25.0
|
||||
# Ensure requests can detect encodings and ship certificates
|
||||
charset-normalizer>=3.2.0
|
||||
certifi>=2024.12.0
|
||||
# Optional Telegram support installs telethon>=1.36.0 when [provider=telegram] is configured.
|
||||
internetarchive>=4.1.0
|
||||
|
||||
# Document and data handling
|
||||
pypdf>=3.0.0
|
||||
mutagen>=1.46.0
|
||||
cbor2>=4.0
|
||||
zstandard>=0.23.0
|
||||
|
||||
# Image and media support
|
||||
Pillow>=10.0.0
|
||||
python-bidi>=0.4.2
|
||||
ffmpeg-python>=0.2.0
|
||||
|
||||
# Metadata extraction and processing
|
||||
musicbrainzngs>=0.7.0
|
||||
lxml>=4.9.0
|
||||
|
||||
# Advanced searching and libraries
|
||||
# Optional Soulseek support installs aioslsk>=1.6.0 when [provider=soulseek] is configured.
|
||||
imdbinfo>=0.1.10
|
||||
|
||||
# Encryption and security (if needed by Crypto usage)
|
||||
pycryptodome>=3.18.0
|
||||
|
||||
# Data processing
|
||||
bencode3
|
||||
tqdm>=4.66.0
|
||||
|
||||
# Browser automation (for web scraping if needed)
|
||||
playwright>=1.40.0
|
||||
|
||||
# Development and utilities
|
||||
python-dateutil>=2.8.0
|
||||
@@ -7,7 +7,7 @@ present or its copy of this helper gets removed.
|
||||
|
||||
Features (subset of the repo helper):
|
||||
- Locate repository venv (default: <workspace>/hydrusnetwork/.venv)
|
||||
- Install or reinstall requirements.txt into the venv
|
||||
- Install or reinstall scripts/requirements.txt into the venv
|
||||
- Verify key imports
|
||||
- Launch hydrus_client.py (foreground or detached)
|
||||
- Install/uninstall simple user-level start-on-boot services (schtasks/systemd/crontab)
|
||||
@@ -50,7 +50,7 @@ def get_python_in_venv(venv_dir: Path) -> Optional[Path]:
|
||||
|
||||
|
||||
def find_requirements(root: Path) -> Optional[Path]:
|
||||
candidates = [root / "requirements.txt", root / "client" / "requirements.txt"]
|
||||
candidates = [root / "scripts" / "requirements.txt", root / "requirements.txt", root / "client" / "requirements.txt"]
|
||||
for c in candidates:
|
||||
if c.exists():
|
||||
return c
|
||||
|
||||
Reference in New Issue
Block a user