ethanolivertroy_obsidian-ma.../python/install_package.py

109 lines
3 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Install a Python package into a plugin-managed virtual environment.
Usage:
python install_package.py --package "markitdown[all]" --venv-dir /path/to/.venv
Installing into a dedicated venv avoids PEP 668 externally-managed-environment
errors from Homebrew / system Python, and keeps the plugin's dependency isolated.
Output:
Streams pip output line by line to stdout.
Final line is JSON: {"success": true, "python_path": "..."} or
{"success": false, "error": "message"}
"""
import argparse
import json
import os
import shutil
import subprocess
import sys
def venv_python(venv_dir: str) -> str:
if sys.platform == "win32":
return os.path.join(venv_dir, "Scripts", "python.exe")
return os.path.join(venv_dir, "bin", "python")
def run_pip(python: str, package: str) -> int:
process = subprocess.Popen(
[python, "-m", "pip", "install", package],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
if process.stdout is not None:
for line in process.stdout:
print(line, end="", flush=True)
process.wait()
return process.returncode or 0
def ensure_venv(venv_dir: str, bootstrap_python: str) -> str:
py = venv_python(venv_dir)
if os.path.isfile(py):
return py
print(f"Creating virtual environment at {venv_dir}...", flush=True)
result = subprocess.run(
[bootstrap_python, "-m", "venv", venv_dir],
capture_output=True,
text=True,
)
if result.returncode != 0 or not os.path.isfile(py):
# Partial/failed create can leave a non-empty dir that blocks retries
shutil.rmtree(venv_dir, ignore_errors=True)
detail = (result.stderr or result.stdout or "").strip()
raise RuntimeError(
f"Failed to create virtual environment at {venv_dir}: {detail or f'exit {result.returncode}'}"
)
return py
def main():
parser = argparse.ArgumentParser(
description="Install a Python package into a plugin-managed venv"
)
parser.add_argument(
"--package",
required=True,
help="Package specification (e.g., 'markitdown[all]')",
)
parser.add_argument(
"--venv-dir",
required=True,
help="Directory for the plugin-managed virtual environment",
)
args = parser.parse_args()
try:
py = ensure_venv(args.venv_dir, sys.executable)
print(f"Installing {args.package} into {py}...", flush=True)
code = run_pip(py, args.package)
if code == 0:
print(json.dumps({"success": True, "python_path": py}))
else:
print(
json.dumps(
{
"success": False,
"error": f"pip exited with code {code}",
}
)
)
sys.exit(1)
except Exception as e:
print(json.dumps({"success": False, "error": str(e)}))
sys.exit(1)
if __name__ == "__main__":
main()