nh
This commit is contained in:
@@ -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 = (
|
||||
|
||||
Reference in New Issue
Block a user