d
This commit is contained in:
@@ -580,11 +580,44 @@ if (Test-Path (Join-Path $repo 'CLI.py')) { & python (Join-Path $repo 'CLI.py')
|
||||
# fallback
|
||||
python -m medeia_macina.cli_entry @args
|
||||
'@
|
||||
# Inject the actual repo path safely (escape embedded double-quotes if any)
|
||||
# Thin wrapper: prefer the canonical Python bootstrap installer
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repo = (Resolve-Path (Join-Path $scriptDir "..")).Path
|
||||
$venvPy = Join-Path $repo '.venv\Scripts\python.exe'
|
||||
|
||||
# Normalize incoming quiet flag (-Quiet) into -q for the Python script
|
||||
$forwardArgs = @()
|
||||
foreach ($a in $args) {
|
||||
if ($a -eq '-Quiet') { $forwardArgs += '-q' } else { $forwardArgs += $a }
|
||||
}
|
||||
|
||||
# Debug helper
|
||||
if ($env:MM_DEBUG) { Write-Host "MM_DEBUG: invoking python installer with args: $forwardArgs" -ForegroundColor Yellow }
|
||||
|
||||
if (Test-Path $venvPy) {
|
||||
& $venvPy (Join-Path $repo 'scripts\bootstrap.py') --no-delegate @forwardArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Fall back to any system Python (py -3 -> python3 -> python)
|
||||
if (Get-Command -Name py -ErrorAction SilentlyContinue) {
|
||||
& py -3 (Join-Path $repo 'scripts\bootstrap.py') --no-delegate @forwardArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
if (Get-Command -Name python3 -ErrorAction SilentlyContinue) {
|
||||
& python3 (Join-Path $repo 'scripts\bootstrap.py') --no-delegate @forwardArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
if (Get-Command -Name python -ErrorAction SilentlyContinue) {
|
||||
& python (Join-Path $repo 'scripts\bootstrap.py') --no-delegate @forwardArgs
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
Write-Host 'Error: no suitable Python 3 interpreter found. Please install Python 3 or use the venv.' -ForegroundColor Red
|
||||
exit 127 # Inject the actual repo path safely (escape embedded double-quotes if any)
|
||||
$ps1Text = $ps1Text.Replace('__REPO__', $repo.Replace('"', '""'))
|
||||
# Ensure the PowerShell shim falls back to the correct module when the venv isn't present
|
||||
$ps1Text = $ps1Text.Replace(' -m medeia_entry ', ' -m medeia_macina.cli_entry ')
|
||||
$ps1Text = $ps1Text.Replace('python -m medeia_entry', 'python -m medeia_macina.cli_entry')
|
||||
# (No legacy 'medeia_entry' shim - use the packaged entry 'medeia_macina.cli_entry')
|
||||
if (Test-Path $mmPs1) {
|
||||
$bak = "$mmPs1.bak$(Get-Date -UFormat %s)"
|
||||
Move-Item -Path $mmPs1 -Destination $bak -Force
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,34 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bootstrap script for POSIX (Linux/macOS) to create a Python venv and install the project.
|
||||
# Usage: scripts/bootstrap.sh [--editable] [--venv <path>] [--python <python>] [--desktop] [--no-install]
|
||||
set -e
|
||||
|
||||
# Ensure script is running under Bash. Some users invoke this script with `sh` (dash)
|
||||
# which does not support the Bash features used below (e.g., [[ ]], arrays, read -p).
|
||||
# If not running under Bash, re-exec using a discovered bash binary.
|
||||
if [ -z "${BASH_VERSION:-}" ]; then
|
||||
if command -v bash >/dev/null 2>&1; then
|
||||
echo "This script requires Bash; re-execing as 'bash $0'..."
|
||||
exec bash "$0" "$@"
|
||||
else
|
||||
echo "ERROR: This script requires Bash. Please run with 'bash $0' or install Bash." >&2
|
||||
exit 2
|
||||
fi
|
||||
# Thin POSIX wrapper that delegates to the canonical Python installer
|
||||
# (scripts/bootstrap.py). Platform bootstraps should prefer calling the
|
||||
# Python script using --no-delegate and -q/--quiet for quiet/non-interactive mode.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
# Prefer repo venv python, then system pythons
|
||||
if [ -x "$REPO/.venv/bin/python" ]; then
|
||||
PY="$REPO/.venv/bin/python"
|
||||
elif [ -x "$REPO/.venv/bin/python3" ]; then
|
||||
PY="$REPO/.venv/bin/python3"
|
||||
elif command -v python3 >/dev/null 2>&1; then
|
||||
PY="python3"
|
||||
elif command -v python >/dev/null 2>&1; then
|
||||
PY="python"
|
||||
else
|
||||
echo "Error: No Python interpreter found; please install Python 3 or create the project's .venv." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
set -euo pipefail
|
||||
# Translate -q into --quiet for the Python installer
|
||||
ARGS=()
|
||||
for a in "$@"; do
|
||||
if [ "$a" = "-q" ]; then
|
||||
ARGS+=("--quiet")
|
||||
else
|
||||
ARGS+=("$a")
|
||||
fi
|
||||
done
|
||||
|
||||
VENV_PATH=".venv"
|
||||
EDITABLE=false
|
||||
DESKTOP=false
|
||||
PYTHON_CMD=""
|
||||
NOINSTALL=false
|
||||
FORCE=false
|
||||
QUIET=false
|
||||
FIX_URLLIB3=false
|
||||
# Playwright options
|
||||
PLAYWRIGHT_BROWSERS="chromium" # comma-separated (chromium,firefox,webkit) or 'all'
|
||||
NO_PLAYWRIGHT=false
|
||||
REMOVE_PTH=false
|
||||
exec "$PY" "$REPO/scripts/bootstrap.py" --no-delegate "${ARGS[@]}"
|
||||
|
||||
# Prompt helper: read from the controlling terminal so prompts still work
|
||||
# when stdout/stderr are redirected or piped (e.g., piping output to sed).
|
||||
@@ -382,12 +386,7 @@ PY
|
||||
|
||||
else
|
||||
echo "WARNING: Could not import 'medeia_macina.cli_entry' from the venv." >&2
|
||||
# Check if legacy top-level module is present; if so, inform the user to prefer the packaged entrypoint
|
||||
if "$VENV_PY" -c 'import importlib; importlib.import_module("medeia_entry")' >/dev/null 2>&1; then
|
||||
echo "Note: 'medeia_entry' top-level module is present. It's recommended to install the project so 'medeia_macina.cli_entry' is available." >&2
|
||||
else
|
||||
echo "Action: Try running: $VENV_PY -m pip install -e . or inspect the venv site-packages to verify the installation." >&2
|
||||
fi
|
||||
echo "Action: Try running: $VENV_PY -m pip install -e . or inspect the venv site-packages to verify the installation." >&2
|
||||
fi
|
||||
|
||||
echo "Verifying environment for known issues (urllib3 compatibility)..."
|
||||
|
||||
724
scripts/setup.py
724
scripts/setup.py
@@ -1,724 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""DEPRECATED: scripts/setup.py
|
||||
|
||||
This file has been renamed to `scripts/bootstrap.py` to avoid having multiple
|
||||
`setup.py` files in the repository. Please use:
|
||||
|
||||
python ./scripts/bootstrap.py
|
||||
|
||||
This shim remains temporarily for backwards compatibility.
|
||||
|
||||
---
|
||||
|
||||
Original docstring:
|
||||
|
||||
scripts/setup.py
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import platform
|
||||
import shutil
|
||||
import os
|
||||
import time
|
||||
|
||||
|
||||
def run(cmd: list[str]) -> None:
|
||||
print(f"> {' '.join(cmd)}")
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
# Helpers to find shell executables and to run the platform-specific
|
||||
# bootstrap script (scripts/bootstrap.sh or scripts/bootstrap.ps1).
|
||||
def _find_powershell() -> str | None:
|
||||
for name in ("pwsh", "powershell"):
|
||||
p = shutil.which(name)
|
||||
if p:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def _find_shell() -> str | None:
|
||||
for name in ("bash", "sh"):
|
||||
p = shutil.which(name)
|
||||
if p:
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def run_platform_bootstrap(repo_root: Path) -> int:
|
||||
"""Run the platform bootstrap script in quiet/non-interactive mode if present.
|
||||
|
||||
Returns the script exit code (0 on success). If no script is present this is a
|
||||
no-op and returns 0.
|
||||
"""
|
||||
ps1 = repo_root / "scripts" / "bootstrap.ps1"
|
||||
sh_script = repo_root / "scripts" / "bootstrap.sh"
|
||||
|
||||
system = platform.system().lower()
|
||||
|
||||
if system == "windows" and ps1.exists():
|
||||
exe = _find_powershell()
|
||||
if not exe:
|
||||
print("PowerShell not found; cannot run bootstrap.ps1", file=sys.stderr)
|
||||
return 1
|
||||
cmd = [
|
||||
exe,
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-File",
|
||||
str(ps1),
|
||||
"-Quiet"
|
||||
]
|
||||
elif sh_script.exists():
|
||||
shell = _find_shell()
|
||||
if not shell:
|
||||
print("Shell not found; cannot run bootstrap.sh", file=sys.stderr)
|
||||
return 1
|
||||
# Use -q (quiet) to skip interactive prompts when supported.
|
||||
cmd = [shell, str(sh_script), "-q"]
|
||||
else:
|
||||
# Nothing to run
|
||||
return 0
|
||||
|
||||
print("Running platform bootstrap script:", " ".join(cmd))
|
||||
rc = subprocess.run(cmd, cwd=str(repo_root))
|
||||
if rc.returncode != 0:
|
||||
print(
|
||||
f"Bootstrap script failed with exit code {rc.returncode}",
|
||||
file=sys.stderr
|
||||
)
|
||||
return int(rc.returncode or 0)
|
||||
|
||||
|
||||
def playwright_package_installed() -> bool:
|
||||
try:
|
||||
import playwright # type: ignore
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _build_playwright_install_cmd(browsers: str | None) -> list[str]:
|
||||
"""Return the command to install Playwright browsers.
|
||||
|
||||
- If browsers is None or empty: default to install Chromium only.
|
||||
- If browsers contains 'all': install all engines by running 'playwright install' with no extra args.
|
||||
- Otherwise, validate entries and return a command that installs the named engines.
|
||||
"""
|
||||
base = [sys.executable, "-m", "playwright", "install"]
|
||||
if not browsers:
|
||||
return base + ["chromium"]
|
||||
|
||||
items = [b.strip().lower() for b in browsers.split(",") if b.strip()]
|
||||
if not items:
|
||||
return base + ["chromium"]
|
||||
if "all" in items:
|
||||
return base
|
||||
|
||||
allowed = {"chromium",
|
||||
"firefox",
|
||||
"webkit"}
|
||||
invalid = [b for b in items if b not in allowed]
|
||||
if invalid:
|
||||
raise ValueError(
|
||||
f"invalid browsers specified: {invalid}. Valid choices: chromium, firefox, webkit, or 'all'"
|
||||
)
|
||||
return base + items
|
||||
|
||||
|
||||
def _install_deno(version: str | None = None) -> int:
|
||||
"""Install Deno runtime for the current platform.
|
||||
|
||||
Uses the official Deno install scripts:
|
||||
- Unix/macOS: curl -fsSL https://deno.land/x/install/install.sh | sh [-s <version>]
|
||||
- Windows: powershell iwr https://deno.land/x/install/install.ps1 -useb | iex; Install-Deno [-Version <version>]
|
||||
|
||||
Returns exit code 0 on success, non-zero otherwise.
|
||||
"""
|
||||
system = platform.system().lower()
|
||||
|
||||
try:
|
||||
if system == "windows":
|
||||
# Use official PowerShell installer
|
||||
if version:
|
||||
ver = version if version.startswith("v") else f"v{version}"
|
||||
ps_cmd = f"iwr https://deno.land/x/install/install.ps1 -useb | iex; Install-Deno -Version {ver}"
|
||||
else:
|
||||
ps_cmd = "iwr https://deno.land/x/install/install.ps1 -useb | iex"
|
||||
run(
|
||||
[
|
||||
"powershell",
|
||||
"-NoProfile",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
ps_cmd
|
||||
]
|
||||
)
|
||||
else:
|
||||
# POSIX: use curl + sh installer
|
||||
if version:
|
||||
ver = version if version.startswith("v") else f"v{version}"
|
||||
cmd = f"curl -fsSL https://deno.land/x/install/install.sh | sh -s {ver}"
|
||||
else:
|
||||
cmd = "curl -fsSL https://deno.land/x/install/install.sh | sh"
|
||||
run(["sh", "-c", cmd])
|
||||
|
||||
# Check that 'deno' is now available in PATH
|
||||
if shutil.which("deno"):
|
||||
print(f"Deno installed at: {shutil.which('deno')}")
|
||||
return 0
|
||||
else:
|
||||
print(
|
||||
"Deno installation completed but 'deno' not found in PATH. You may need to add Deno's bin directory to your PATH manually.",
|
||||
file=sys.stderr
|
||||
)
|
||||
return 1
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Deno install failed: {exc}", file=sys.stderr)
|
||||
return int(exc.returncode or 1)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Setup Medios-Macina: install deps and Playwright browsers"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--skip-deps",
|
||||
action="store_true",
|
||||
help="Skip installing Python dependencies from requirements.txt"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-playwright",
|
||||
action="store_true",
|
||||
help="Skip running 'playwright install' (only install packages)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--playwright-only",
|
||||
action="store_true",
|
||||
help="Only run 'playwright install' (skips dependency installation)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--browsers",
|
||||
type=str,
|
||||
default="chromium",
|
||||
help=
|
||||
"Comma-separated list of browsers to install: chromium,firefox,webkit or 'all' (default: chromium)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--install-editable",
|
||||
action="store_true",
|
||||
help="Install the project in editable mode (pip install -e .) for running tests"
|
||||
)
|
||||
deno_group = parser.add_mutually_exclusive_group()
|
||||
deno_group.add_argument(
|
||||
"--install-deno",
|
||||
action="store_true",
|
||||
help="Install the Deno runtime (default behavior; kept for explicitness)"
|
||||
)
|
||||
deno_group.add_argument(
|
||||
"--no-deno",
|
||||
action="store_true",
|
||||
help="Skip installing Deno runtime (opt out)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--deno-version",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Specific Deno version to install (e.g., v1.34.3)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--upgrade-pip",
|
||||
action="store_true",
|
||||
help="Upgrade pip/setuptools/wheel before installing requirements"
|
||||
)
|
||||
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/setup.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.")
|
||||
return 0
|
||||
|
||||
if sys.version_info < (3, 8):
|
||||
print("Warning: Python 3.8+ is recommended.", file=sys.stderr)
|
||||
|
||||
# Opinionated: always create or use a local venv at the project root (.venv)
|
||||
venv_dir = repo_root / ".venv"
|
||||
|
||||
def _venv_python(p: Path) -> Path:
|
||||
if platform.system().lower() == "windows":
|
||||
return p / "Scripts" / "python.exe"
|
||||
return p / "bin" / "python"
|
||||
|
||||
def _ensure_local_venv() -> Path:
|
||||
"""Create (if missing) and return the path to the venv's python executable.
|
||||
|
||||
This is intentionally opinionated: we keep a venv at `./.venv` in the repo root
|
||||
and use that for all package operations to keep developer environments reproducible.
|
||||
"""
|
||||
try:
|
||||
if not venv_dir.exists():
|
||||
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}")
|
||||
|
||||
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")
|
||||
run([sys.executable, "-m", "venv", str(venv_dir)])
|
||||
py = _venv_python(venv_dir)
|
||||
if not py.exists():
|
||||
raise RuntimeError(f"Unable to locate venv python at {py}")
|
||||
return py
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(f"Failed to create or prepare local venv: {exc}", file=sys.stderr)
|
||||
raise
|
||||
|
||||
# Ensure a local venv is present and use it for subsequent installs.
|
||||
venv_python = _ensure_local_venv()
|
||||
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.
|
||||
args.skip_deps = False
|
||||
args.install_editable = True
|
||||
args.no_playwright = False
|
||||
|
||||
try:
|
||||
if args.playwright_only:
|
||||
if not playwright_package_installed():
|
||||
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)..."
|
||||
)
|
||||
try:
|
||||
cmd = _build_playwright_install_cmd(args.browsers)
|
||||
except ValueError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
run(cmd)
|
||||
print("Playwright browsers installed successfully.")
|
||||
return 0
|
||||
|
||||
if args.upgrade_pip:
|
||||
print("Upgrading pip, setuptools, and wheel in local venv...")
|
||||
run(
|
||||
[
|
||||
str(venv_python),
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--upgrade",
|
||||
"pip",
|
||||
"setuptools",
|
||||
"wheel"
|
||||
]
|
||||
)
|
||||
|
||||
if not args.skip_deps:
|
||||
req_file = repo_root / "requirements.txt"
|
||||
if not req_file.exists():
|
||||
print(
|
||||
f"requirements.txt not found at {req_file}; skipping dependency installation.",
|
||||
file=sys.stderr
|
||||
)
|
||||
else:
|
||||
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...")
|
||||
run([str(venv_python), "-m", "pip", "install", "playwright"])
|
||||
|
||||
print(
|
||||
"Installing Playwright browsers (this may download several hundred MB)..."
|
||||
)
|
||||
try:
|
||||
cmd = _build_playwright_install_cmd(args.browsers)
|
||||
except ValueError as exc:
|
||||
print(f"Error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# Run Playwright install using the venv's python so binaries are available in venv
|
||||
cmd[0] = str(venv_python)
|
||||
run(cmd)
|
||||
|
||||
# Optional: install the project in editable mode so tests can import the package
|
||||
# Install the project into the local venv (editable mode is the default, opinionated)
|
||||
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...")
|
||||
try:
|
||||
import subprocess as _sub
|
||||
rc = _sub.run(
|
||||
[
|
||||
str(venv_python),
|
||||
"-c",
|
||||
"import importlib; importlib.import_module('CLI')"
|
||||
],
|
||||
check=False
|
||||
)
|
||||
if rc.returncode == 0:
|
||||
print("OK: top-level 'CLI' is importable in the venv.")
|
||||
else:
|
||||
print(
|
||||
"Top-level 'CLI' not importable; attempting to add repo path to venv site-packages via a .pth file..."
|
||||
)
|
||||
cmd = [
|
||||
str(venv_python),
|
||||
"-c",
|
||||
(
|
||||
"import site, sysconfig\n"
|
||||
"out=[]\n"
|
||||
"try:\n out.extend(site.getsitepackages())\nexcept Exception:\n pass\n"
|
||||
"try:\n p = sysconfig.get_paths().get('purelib')\n if p:\n out.append(p)\nexcept Exception:\n pass\n"
|
||||
"seen=[]; res=[]\n"
|
||||
"for x in out:\n if x and x not in seen:\n seen.append(x); res.append(x)\n"
|
||||
"for s in res:\n print(s)\n"
|
||||
)
|
||||
]
|
||||
out = _sub.check_output(cmd, text=True).strip().splitlines()
|
||||
site_dir = None
|
||||
for sp in out:
|
||||
if sp and Path(sp).exists():
|
||||
site_dir = Path(sp)
|
||||
break
|
||||
if site_dir is None:
|
||||
print(
|
||||
"Could not determine venv site-packages directory; skipping .pth fallback"
|
||||
)
|
||||
else:
|
||||
pth_file = site_dir / "medeia_repo.pth"
|
||||
if pth_file.exists():
|
||||
txt = pth_file.read_text(encoding="utf-8")
|
||||
if str(repo_root) in txt:
|
||||
print(f".pth already contains repo root: {pth_file}")
|
||||
else:
|
||||
with pth_file.open("a", encoding="utf-8") as fh:
|
||||
fh.write(str(repo_root) + "\n")
|
||||
print(f"Appended repo root to existing .pth: {pth_file}")
|
||||
else:
|
||||
with pth_file.open("w", encoding="utf-8") as fh:
|
||||
fh.write(str(repo_root) + "\n")
|
||||
print(
|
||||
f"Wrote .pth adding repo root to venv site-packages: {pth_file}"
|
||||
)
|
||||
|
||||
# Re-check whether CLI can be imported now
|
||||
rc2 = _sub.run(
|
||||
[
|
||||
str(venv_python),
|
||||
"-c",
|
||||
"import importlib; importlib.import_module('CLI')"
|
||||
],
|
||||
check=False
|
||||
)
|
||||
if rc2.returncode == 0:
|
||||
print("Top-level 'CLI' import works after adding .pth")
|
||||
else:
|
||||
print(
|
||||
"Adding .pth did not make top-level 'CLI' importable; consider creating an egg-link or checking the venv."
|
||||
)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"Warning: failed to verify or modify site-packages for top-level CLI: {exc}"
|
||||
)
|
||||
|
||||
# Optional: install Deno runtime (default: install unless --no-deno is passed)
|
||||
install_deno_requested = True
|
||||
if getattr(args, "no_deno", False):
|
||||
install_deno_requested = False
|
||||
elif getattr(args, "install_deno", False):
|
||||
install_deno_requested = True
|
||||
|
||||
if install_deno_requested:
|
||||
print("Installing Deno runtime (local/system)...")
|
||||
rc = _install_deno(args.deno_version)
|
||||
if rc != 0:
|
||||
print("Deno installation failed.", file=sys.stderr)
|
||||
return rc
|
||||
|
||||
# Write project-local launcher scripts (project root) that prefer the local .venv
|
||||
def _write_launchers():
|
||||
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
|
||||
|
||||
ps1_text = r"""Param([Parameter(ValueFromRemainingArguments=$true)] $args)
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$repo = $scriptDir
|
||||
$venv = Join-Path $repo '.venv'
|
||||
# Ensure venv Scripts dir is on PATH for provider discovery
|
||||
$venvScripts = Join-Path $venv 'Scripts'
|
||||
if (Test-Path $venvScripts) { $env:PATH = $venvScripts + ';' + $env:PATH }
|
||||
$py = Join-Path $venv 'Scripts\python.exe'
|
||||
$cli = Join-Path $repo 'CLI.py'
|
||||
if (Test-Path $py) { & $py -m medeia_macina.cli_entry @args; exit $LASTEXITCODE }
|
||||
if (Test-Path $cli) { & python $cli @args; exit $LASTEXITCODE }
|
||||
# fallback
|
||||
python -m medeia_macina.cli_entry @args
|
||||
"""
|
||||
try:
|
||||
ps1.write_text(ps1_text, encoding="utf-8")
|
||||
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:
|
||||
# non-interactive mode, which we use so "python ./scripts/bootstrap.py" just
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
_write_launchers()
|
||||
|
||||
# Install user-global shims so `mm` can be executed from any shell session.
|
||||
def _install_user_shims(repo: Path) -> None:
|
||||
try:
|
||||
home = Path.home()
|
||||
system = platform.system().lower()
|
||||
|
||||
if system == "windows":
|
||||
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 = (
|
||||
"Param([Parameter(ValueFromRemainingArguments=$true)] $args)\n"
|
||||
f"$repo = \"{repo}\"\n"
|
||||
"$venv = Join-Path $repo '.venv'\n"
|
||||
"$exe = Join-Path $venv 'Scripts\\mm.exe'\n"
|
||||
"if (Test-Path $exe) { & $exe @args; exit $LASTEXITCODE }\n"
|
||||
"$py = Join-Path $venv 'Scripts\\python.exe'\n"
|
||||
"if (Test-Path $py) {\n"
|
||||
" if ($env:MM_DEBUG) {\n"
|
||||
" Write-Host \"MM_DEBUG: diagnostics\" -ForegroundColor Yellow\n"
|
||||
" & $py -c \"import sys,importlib,importlib.util,traceback; print('sys.executable:', sys.executable); print('sys.path (first 8):', sys.path[:8]);\"\n"
|
||||
" }\n"
|
||||
" & $py -m medeia_macina.cli_entry @args; exit $LASTEXITCODE\n"
|
||||
"}\n"
|
||||
"python -m medeia_macina.cli_entry @args\n"
|
||||
)
|
||||
if mm_ps1.exists():
|
||||
bak = mm_ps1.with_suffix(f".bak{int(time.time())}")
|
||||
mm_ps1.replace(bak)
|
||||
mm_ps1.write_text(ps1_text, encoding="utf-8")
|
||||
|
||||
# Attempt to add user_bin to the user's PATH if it's not present.
|
||||
try:
|
||||
cur = os.environ.get("PATH", "")
|
||||
str_bin = str(user_bin)
|
||||
if str_bin not in cur:
|
||||
ps_cmd = (
|
||||
"$bin = '{bin}';"
|
||||
"$cur = [Environment]::GetEnvironmentVariable('PATH','User');"
|
||||
"if ($cur -notlike \"*$bin*\") {[Environment]::SetEnvironmentVariable('PATH', ($bin + ';' + ($cur -ne $null ? $cur : '')), 'User')}"
|
||||
).format(bin=str_bin.replace('\\',
|
||||
'\\\\'))
|
||||
subprocess.run(
|
||||
["powershell",
|
||||
"-NoProfile",
|
||||
"-Command",
|
||||
ps_cmd],
|
||||
check=False
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
print(f"Installed global launchers to: {user_bin}")
|
||||
|
||||
else:
|
||||
# POSIX
|
||||
user_bin = Path(
|
||||
os.environ.get("XDG_BIN_HOME",
|
||||
str(home / ".local/bin"))
|
||||
)
|
||||
user_bin.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
mm_sh = user_bin / "mm"
|
||||
sh_text = (
|
||||
"#!/usr/bin/env bash\n"
|
||||
"set -e\n"
|
||||
f"REPO=\"{repo}\"\n"
|
||||
"# Prefer git top-level when available to avoid embedding a parent path.\n"
|
||||
"if command -v git >/dev/null 2>&1; then\n"
|
||||
" gitroot=$(git -C \"$REPO\" rev-parse --show-toplevel 2>/dev/null || true)\n"
|
||||
" if [ -n \"$gitroot\" ]; then\n"
|
||||
" REPO=\"$gitroot\"\n"
|
||||
" fi\n"
|
||||
"fi\n"
|
||||
"# If git not available or didn't resolve, walk up from CWD to find a project root.\n"
|
||||
"if [ ! -f \"$REPO/CLI.py\" ] && [ ! -f \"$REPO/pyproject.toml\" ]; then\n"
|
||||
" CUR=\"$(pwd -P)\"\n"
|
||||
" while [ \"$CUR\" != \"/\" ] && [ \"$CUR\" != \"\" ]; do\n"
|
||||
" if [ -f \"$CUR/CLI.py\" ] || [ -f \"$CUR/pyproject.toml\" ]; then\n"
|
||||
" REPO=\"$CUR\"\n"
|
||||
" break\n"
|
||||
" fi\n"
|
||||
" CUR=\"$(dirname \"$CUR\")\"\n"
|
||||
" done\n"
|
||||
"fi\n"
|
||||
"VENV=\"$REPO/.venv\"\n"
|
||||
"# Debug mode: set MM_DEBUG=1 to print repository, venv, and import diagnostics\n"
|
||||
"if [ -n \"${MM_DEBUG:-}\" ]; then\n"
|
||||
" echo \"MM_DEBUG: diagnostics\" >&2\n"
|
||||
" echo \"Resolved REPO: $REPO\" >&2\n"
|
||||
" echo \"Resolved VENV: $VENV\" >&2\n"
|
||||
" echo \"VENV exists: $( [ -d \"$VENV\" ] && echo yes || echo no )\" >&2\n"
|
||||
" echo \"Candidates:\" >&2\n"
|
||||
" echo \" VENV/bin/mm: $( [ -x \"$VENV/bin/mm\" ] && echo yes || echo no )\" >&2\n"
|
||||
" echo \" VENV/bin/python3: $( [ -x \"$VENV/bin/python3\" ] && echo yes || echo no )\" >&2\n"
|
||||
" echo \" VENV/bin/python: $( [ -x \"$VENV/bin/python\" ] && echo yes || echo no )\" >&2\n"
|
||||
" echo \" system python3: $(command -v python3 || echo none)\" >&2\n"
|
||||
" echo \" system python: $(command -v python || echo none)\" >&2\n"
|
||||
" for pycmd in \"$VENV/bin/python3\" \"$VENV/bin/python\" \"$(command -v python3 2>/dev/null)\" \"$(command -v python 2>/dev/null)\"; do\n"
|
||||
" if [ -n \"$pycmd\" ] && [ -x \"$pycmd\" ]; then\n"
|
||||
" echo \"---- Testing with: $pycmd ----\" >&2\n"
|
||||
" $pycmd - <<'PY'\nimport sys, importlib, traceback, importlib.util\nprint('sys.executable:', sys.executable)\nprint('sys.path (first 8):', sys.path[:8])\nfor mod in ('CLI','medeia_macina','medeia_macina.cli_entry'):\n try:\n spec = importlib.util.find_spec(mod)\n print(mod, 'spec:', spec)\n if spec:\n m = importlib.import_module(mod)\n print(mod, 'loaded at', getattr(m, '__file__', None))\n except Exception:\n print(mod, 'import failed')\n traceback.print_exc()\nPY\n"
|
||||
" fi\n"
|
||||
" done\n"
|
||||
" echo \"MM_DEBUG: end diagnostics\" >&2\n"
|
||||
"fi\n"
|
||||
"# Packaged console script in the venv if available\n"
|
||||
"if [ -x \"$VENV/bin/mm\" ]; then\n"
|
||||
" exec \"$VENV/bin/mm\" \"$@\"\n"
|
||||
"fi\n"
|
||||
"# Prefer venv's python3, then venv's python\n"
|
||||
"if [ -x \"$VENV/bin/python3\" ]; then\n"
|
||||
" exec \"$VENV/bin/python3\" -m medeia_macina.cli_entry \"$@\"\n"
|
||||
"fi\n"
|
||||
"if [ -x \"$VENV/bin/python\" ]; then\n"
|
||||
" exec \"$VENV/bin/python\" -m medeia_macina.cli_entry \"$@\"\n"
|
||||
"fi\n"
|
||||
"# Fallback to system python3, then system python (only if it's Python 3)\n"
|
||||
"if command -v python3 >/dev/null 2>&1; then\n"
|
||||
" exec python3 -m medeia_macina.cli_entry \"$@\"\n"
|
||||
"fi\n"
|
||||
"if command -v python >/dev/null 2>&1; then\n"
|
||||
" if python -c 'import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)'; then\n"
|
||||
" exec python -m medeia_macina.cli_entry \"$@\"\n"
|
||||
" fi\n"
|
||||
"fi\n"
|
||||
"echo 'Error: no suitable Python 3 interpreter found. Please install Python 3 or use the venv.' >&2\n"
|
||||
"exit 127\n"
|
||||
)
|
||||
if mm_sh.exists():
|
||||
bak = mm_sh.with_suffix(f".bak{int(time.time())}")
|
||||
mm_sh.replace(bak)
|
||||
mm_sh.write_text(sh_text, encoding="utf-8")
|
||||
mm_sh.chmod(mm_sh.stat().st_mode | 0o111)
|
||||
|
||||
# Ensure the user's bin is on PATH for future sessions by adding to ~/.profile
|
||||
cur_path = os.environ.get("PATH", "")
|
||||
if str(user_bin) not in cur_path:
|
||||
profile = home / ".profile"
|
||||
snippet = (
|
||||
"# Added by Medeia-Macina setup: ensure user local bin is on PATH\n"
|
||||
"if [ -d \"$HOME/.local/bin\" ] && [[ \":$PATH:\" != *\":$HOME/.local/bin:\"* ]]; then\n"
|
||||
" PATH=\"$HOME/.local/bin:$PATH\"\n"
|
||||
"fi\n"
|
||||
)
|
||||
try:
|
||||
txt = profile.read_text() if profile.exists() else ""
|
||||
if snippet.strip() not in txt:
|
||||
with profile.open("a", encoding="utf-8") as fh:
|
||||
fh.write("\n" + snippet)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
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)
|
||||
|
||||
_install_user_shims(repo_root)
|
||||
|
||||
print("Setup complete.")
|
||||
return 0
|
||||
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(
|
||||
f"Error: command failed with exit {exc.returncode}: {exc}",
|
||||
file=sys.stderr
|
||||
)
|
||||
return int(exc.returncode or 1)
|
||||
except Exception as exc: # pragma: no cover - defensive
|
||||
print(f"Unexpected error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user