This commit is contained in:
2025-12-31 16:10:35 -08:00
parent 9464bd0d21
commit 807ea7f53a
10 changed files with 313 additions and 1179 deletions

View File

@@ -8,6 +8,11 @@ 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).
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`).
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
@@ -230,6 +235,17 @@ def main() -> int:
action="store_true",
help="Only run 'playwright install' (skips dependency installation)",
)
parser.add_argument(
"--no-delegate",
action="store_true",
help="Do not delegate to platform bootstrap scripts; run the Python bootstrap directly.",
)
parser.add_argument(
"-q",
"--quiet",
action="store_true",
help="Quiet mode: minimize informational output (useful when called from platform wrappers)",
)
parser.add_argument(
"--browsers",
type=str,
@@ -264,21 +280,170 @@ def main() -> int:
action="store_true",
help="Upgrade pip/setuptools/wheel before installing requirements",
)
parser.add_argument(
"--uninstall",
action="store_true",
help="Uninstall local .venv and user shims (non-interactive)",
)
parser.add_argument(
"-y",
"--yes",
action="store_true",
help="Assume yes for confirmation prompts during uninstall",
)
args = parser.parse_args()
repo_root = Path(__file__).resolve().parent.parent
# If invoked without any arguments, prefer to delegate to the platform
# bootstrap script (if present). The bootstrap scripts support a quiet/
# non-interactive mode, which we use so "python ./scripts/bootstrap.py" just
# does the right thing on Windows and *nix without extra flags.
if len(sys.argv) == 1:
rc = run_platform_bootstrap(repo_root)
if rc != 0:
return rc
print("Platform bootstrap completed successfully.")
# Helpers for interactive menu and uninstall detection
def _venv_python_path(p: Path) -> Path | None:
"""Return the path to a python executable inside a venv directory if present."""
if (p / "Scripts" / "python.exe").exists():
return p / "Scripts" / "python.exe"
if (p / "bin" / "python").exists():
return p / "bin" / "python"
return None
def _is_installed() -> bool:
"""Return True if the project appears installed into the local .venv."""
vdir = repo_root / ".venv"
py = _venv_python_path(vdir)
if py is None:
return False
try:
rc = subprocess.run([str(py), "-m", "pip", "show", "medeia-macina"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False)
return rc.returncode == 0
except Exception:
return False
def _do_uninstall() -> int:
"""Attempt to remove the local venv and any shims written to the user's bin."""
vdir = repo_root / ".venv"
if not vdir.exists():
if not args.quiet:
print("No local .venv found; nothing to uninstall.")
return 0
if not args.yes:
try:
prompt = input(f"Remove local virtualenv at {vdir} and installed user shims? [y/N]: ")
except EOFError:
print("Non-interactive environment; pass --uninstall --yes to uninstall without prompts.", file=sys.stderr)
return 2
if prompt.strip().lower() not in ("y", "yes"):
print("Uninstall aborted.")
return 1
# Remove repo-local launchers
for name in ("mm", "mm.ps1", "mm.bat"):
p = repo_root / name
if p.exists():
try:
p.unlink()
if not args.quiet:
print(f"Removed local launcher: {p}")
except Exception as exc:
print(f"Warning: failed to remove {p}: {exc}", file=sys.stderr)
# Remove user shims that the installer may have written
try:
system = platform.system().lower()
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"):
p = user_bin / name
if p.exists():
p.unlink()
if not args.quiet:
print(f"Removed user shim: {p}")
else:
user_bin = Path(os.environ.get("XDG_BIN_HOME", str(Path.home() / ".local/bin")))
if user_bin.exists():
p = user_bin / "mm"
if p.exists():
p.unlink()
if not args.quiet:
print(f"Removed user shim: {p}")
except Exception as exc:
print(f"Warning: failed to remove user shims: {exc}", file=sys.stderr)
# Remove .venv directory
try:
shutil.rmtree(vdir)
if not args.quiet:
print(f"Removed local virtualenv: {vdir}")
except Exception as exc:
print(f"Failed to remove venv: {exc}", file=sys.stderr)
return 1
return 0
def _interactive_menu() -> str | int:
"""Show a simple interactive menu to choose install/uninstall or delegate."""
try:
installed = _is_installed()
while True:
print("\nMedeia-Macina bootstrap - interactive menu")
if installed:
print("1) Install / Reinstall")
print("2) Uninstall")
print("3) Status")
print("q) Quit")
choice = input("Choose an option: ").strip().lower()
if not choice or choice in ("1", "install", "reinstall"):
return "install"
if choice in ("2", "uninstall"):
return "uninstall"
if choice in ("3", "status"):
print("Installation detected." if installed else "Not installed.")
continue
if choice in ("q", "quit", "exit"):
return 0
else:
print("1) Install")
print("q) Quit")
choice = input("Choose an option: ").strip().lower()
if not choice or choice in ("1", "install"):
return "install"
if choice in ("q", "quit", "exit"):
return 0
except EOFError:
# Non-interactive, fall back to delegating to platform helper
return "delegate"
# If the user passed --uninstall explicitly, perform non-interactive uninstall and exit
if args.uninstall:
return _do_uninstall()
# If invoked without any arguments and not asked to skip delegation, prefer
# the interactive menu when running in a TTY; otherwise delegate to the
# platform-specific bootstrap helper (non-interactive).
if len(sys.argv) == 1 and not args.no_delegate:
if sys.stdin.isatty() and not args.quiet:
sel = _interactive_menu()
if sel == "install":
# user chose to install/reinstall; set defaults and continue
args.skip_deps = False
args.install_editable = True
args.no_playwright = False
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
else:
return int(sel or 0)
else:
rc = run_platform_bootstrap(repo_root)
if rc != 0:
return rc
if not args.quiet:
print("Platform bootstrap completed successfully.")
return 0
if sys.version_info < (3, 8):
print("Warning: Python 3.8+ is recommended.", file=sys.stderr)
@@ -295,15 +460,18 @@ def main() -> int:
try:
if not venv_dir.exists():
print(f"Creating local virtualenv at: {venv_dir}")
if not args.quiet:
print(f"Creating local virtualenv at: {venv_dir}")
run([sys.executable, "-m", "venv", str(venv_dir)])
else:
print(f"Using existing virtualenv at: {venv_dir}")
if not args.quiet:
print(f"Using existing virtualenv at: {venv_dir}")
py = _venv_python(venv_dir)
if not py.exists():
# Try recreating venv if python is missing
print(f"Local venv python not found at {py}; recreating venv")
if not args.quiet:
print(f"Local venv python not found at {py}; recreating venv")
run([sys.executable, "-m", "venv", str(venv_dir)])
py = _venv_python(venv_dir)
if not py.exists():
@@ -315,7 +483,8 @@ def main() -> int:
# Ensure a local venv is present and use it for subsequent installs.
venv_python = _ensure_local_venv()
print(f"Using venv python: {venv_python}")
if not args.quiet:
print(f"Using venv python: {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.
@@ -326,12 +495,14 @@ def main() -> int:
try:
if args.playwright_only:
if not playwright_package_installed():
print("'playwright' package not found; installing it via pip...")
if not args.quiet:
print("'playwright' package not found; installing it via pip...")
run([sys.executable, "-m", "pip", "install", "playwright"])
print(
"Installing Playwright browsers (this may download several hundred MB)..."
)
if not args.quiet:
print(
"Installing Playwright browsers (this may download several hundred MB)..."
)
try:
cmd = _build_playwright_install_cmd(args.browsers)
except ValueError as exc:
@@ -339,11 +510,13 @@ def main() -> int:
return 2
run(cmd)
print("Playwright browsers installed successfully.")
if not args.quiet:
print("Playwright browsers installed successfully.")
return 0
if args.upgrade_pip:
print("Upgrading pip, setuptools, and wheel in local venv...")
if not args.quiet:
print("Upgrading pip, setuptools, and wheel in local venv...")
run(
[
str(venv_python),
@@ -365,19 +538,22 @@ def main() -> int:
file=sys.stderr,
)
else:
print(
f"Installing Python dependencies into local venv from {req_file}..."
)
if not args.quiet:
print(
f"Installing Python dependencies into local venv from {req_file}..."
)
run([str(venv_python), "-m", "pip", "install", "-r", str(req_file)])
if not args.no_playwright:
if not playwright_package_installed():
print("'playwright' package not installed in venv; installing it...")
if not args.quiet:
print("'playwright' package not installed in venv; installing it...")
run([str(venv_python), "-m", "pip", "install", "playwright"])
print(
"Installing Playwright browsers (this may download several hundred MB)..."
)
if not args.quiet:
print(
"Installing Playwright browsers (this may download several hundred MB)..."
)
try:
cmd = _build_playwright_install_cmd(args.browsers)
except ValueError as exc:
@@ -389,11 +565,13 @@ def main() -> int:
run(cmd)
# Install the project into the local venv (editable mode is the default, opinionated)
print("Installing project into local venv (editable mode)")
if not args.quiet:
print("Installing project into local venv (editable mode)")
run([str(venv_python), "-m", "pip", "install", "-e", "."])
# Verify top-level 'CLI' import and, if missing, attempt to make it available
print("Verifying top-level 'CLI' import in venv...")
if not args.quiet:
print("Verifying top-level 'CLI' import in venv...")
try:
rc = subprocess.run(
[
@@ -477,7 +655,8 @@ def main() -> int:
install_deno_requested = True
if install_deno_requested:
print("Installing Deno runtime (local/system)...")
if not args.quiet:
print("Installing Deno runtime (local/system)...")
rc = _install_deno(args.deno_version)
if rc != 0:
print("Deno installation failed.", file=sys.stderr)
@@ -615,7 +794,8 @@ python -m medeia_macina.cli_entry @args
except Exception:
pass
print(f"Installed global launchers to: {user_bin}")
if not args.quiet:
print(f"Installed global launchers to: {user_bin}")
else:
# POSIX
@@ -716,7 +896,8 @@ python -m medeia_macina.cli_entry @args
except Exception:
pass
print(f"Installed global launcher to: {mm_sh}")
if not args.quiet:
print(f"Installed global launcher to: {mm_sh}")
except Exception as exc: # pragma: no cover - best effort
print(f"Failed to install global shims: {exc}", file=sys.stderr)