From 7c8b0edb5c45f22bb1b0ddb576a2917f68226ef2 Mon Sep 17 00:00:00 2001 From: nose Date: Wed, 24 Dec 2025 04:05:35 -0800 Subject: [PATCH] bootstrap: add automatic .pth write for editable installs so top-level CLI is importable --- docs/BOOTSTRAP.md | 2 + scripts/bootstrap.ps1 | 62 ++++++++++++++++++++++++ scripts/bootstrap.sh | 106 ++++++++++++++++++++++++++++++++++++++---- scripts/setup.py | 50 ++++++++++++++++++++ 4 files changed, 211 insertions(+), 9 deletions(-) diff --git a/docs/BOOTSTRAP.md b/docs/BOOTSTRAP.md index abececd..db9e88b 100644 --- a/docs/BOOTSTRAP.md +++ b/docs/BOOTSTRAP.md @@ -49,6 +49,8 @@ Running `python ./scripts/setup.py` is intentionally opinionated: it will create These launchers prefer the local `./.venv` Python and console scripts so you can run the project with `./mm` or `mm.ps1` directly from the repo root. +- When installing in editable mode from a development checkout, the bootstrap will also add a small `.pth` file to the venv's `site-packages` pointing at the repository root. This ensures top-level scripts such as `CLI.py` are importable even when using PEP 660 editable wheels (avoids having to create an egg-link by hand). + Additionally, the setup helpers install a global `mm` launcher into your user bin so you can run `mm` from any shell session: - POSIX: `~/.local/bin/mm` (created if missing; the script attempts to add `~/.local/bin` to `PATH` by updating `~/.profile` / shell RCs if required) diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 index 617f09b..4675793 100644 --- a/scripts/bootstrap.ps1 +++ b/scripts/bootstrap.ps1 @@ -170,6 +170,68 @@ if (-not $NoInstall) { Write-Log "pip install failed: $_" "ERROR"; exit 6 } + # Verify top-level 'CLI' import and (if missing) attempt to make it available + Write-Log "Verifying installed CLI import..." + try { + & $venvPython -c "import importlib; importlib.import_module('medeia_macina.cli_entry')" 2>$null + if ($LASTEXITCODE -eq 0) { Write-Log "OK: 'medeia_macina.cli_entry' is importable in the venv." } + } catch {} + + try { + & $venvPython -c "import importlib; importlib.import_module('CLI')" 2>$null + if ($LASTEXITCODE -eq 0) { Write-Log "Top-level 'CLI' is importable in the venv." } + else { + Write-Log "Top-level 'CLI' not importable; attempting to add repo root to venv site-packages via .pth" "INFO" + $sites = Get-SitePackages -python $venvPython + $siteDir = $sites | Where-Object { Test-Path $_ } | Select-Object -First 1 + if ($siteDir) { + $pth = Join-Path $siteDir 'medeia_repo.pth' + if (Test-Path $pth) { + if (-not (Select-String -Path $pth -Pattern ([regex]::Escape($repoRoot)) -Quiet)) { + Add-Content -Path $pth -Value $repoRoot + Write-Log "Appended repo root to existing .pth: $pth" "INFO" + } else { + Write-Log ".pth already contains repo root: $pth" "INFO" + } + } else { + Set-Content -LiteralPath $pth -Value $repoRoot -Encoding UTF8 + Write-Log "Wrote .pth adding repo root to venv site-packages: $pth" "INFO" + } + + # Re-check import + & $venvPython -c "import importlib; importlib.import_module('CLI')" 2>$null + if ($LASTEXITCODE -eq 0) { + Write-Log "Top-level 'CLI' import works after adding .pth" "INFO" + } else { + Write-Log "Adding .pth did not make top-level 'CLI' importable." "ERROR" + if ($Editable) { + Write-Log "Editable install already requested; attempting editable reinstall for good measure..." "INFO" + try { & $venvPython -m pip install -e . } catch { Write-Log "Editable reinstall failed: $_" "ERROR"; exit 6 } + & $venvPython -c "import importlib; importlib.import_module('CLI')" 2>$null + if ($LASTEXITCODE -eq 0) { Write-Log "Top-level 'CLI' is now importable after reinstall." "INFO" } + else { Write-Log "Editable reinstall did not make 'CLI' importable; inspect the venv or create an egg-link manually." "ERROR"; exit 6 } + } else { + if (-not $Quiet) { + $ans = Read-Host "Top-level 'CLI' not importable; install project in editable mode now? (Y/n)" + if ($ans -eq 'y' -or $ans -eq 'Y') { try { & $venvPython -m pip install -e . } catch { Write-Log "Editable install failed: $_" "ERROR"; exit 6 } } + else { Write-Log "Warning: continuing without top-level 'CLI' importable; some entrypoints may fail." "ERROR" } + } else { Write-Log "Top-level 'CLI' not importable and cannot prompt (quiet mode); aborting." "ERROR"; exit 6 } + } + } + } else { + Write-Log "Unable to determine site-packages to write .pth; falling back to editable install prompt" "WARNING" + if ($Editable) { try { & $venvPython -m pip install -e . } catch { Write-Log "Editable install failed: $_" "ERROR"; exit 6 } } + elseif (-not $Quiet) { + $ans = Read-Host "Top-level 'CLI' not importable; install project in editable mode now? (Y/n)" + if ($ans -eq 'y' -or $ans -eq 'Y') { try { & $venvPython -m pip install -e . } catch { Write-Log "Editable install failed: $_" "ERROR"; exit 6 } } + } else { Write-Log "Top-level 'CLI' not importable and cannot prompt (quiet mode); aborting." "ERROR"; exit 6 } + } + } + } catch { + Write-Log "Failed to verify top-level 'CLI': $_" "ERROR" + exit 6 + } + # Install Playwright browsers (default: chromium) unless explicitly disabled if (-not $NoPlaywright) { Write-Log "Ensuring Playwright browsers are installed (browsers=$PlaywrightBrowsers)..." diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index f6578eb..65d94c8 100644 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -285,19 +285,107 @@ if [[ "$NOINSTALL" != "true" ]]; then # If not explicitly requested, auto-selec # Additional compatibility check: top-level 'CLI' module may be required by # older entrypoints or direct imports when running from a development checkout. if ! "$VENV_PY" -c 'import importlib,sys; importlib.import_module("CLI")' >/dev/null 2>&1; then - echo "Note: top-level 'CLI' module not importable; some entrypoints expect it." >&2 + echo "Note: top-level 'CLI' module not importable; attempting to add the repo root to venv site-packages via a .pth file so top-level imports work." >&2 - # If this appears to be a development checkout, offer to install editable mode - if [[ -d "$REPO/.git" ]] || git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then - if [[ "$EDITABLE" == "true" ]]; then - echo "Installing project in editable mode to provide top-level 'CLI'..." - "$VENV_PY" -m pip install -e "$REPO" || { echo "Editable install failed" >&2; exit 6; } + # Try to discover venv site-packages and write a .pth pointing at the repo root + site_pkgs=$("$VENV_PY" - <<'PY' +import site, sysconfig +out=[] +try: + out.extend(site.getsitepackages()) +except Exception: + pass +try: + p = sysconfig.get_paths().get('purelib') + if p: + out.append(p) +except Exception: + pass +seen=set(); res=[] +for x in out: + if x and x not in seen: + seen.add(x); res.append(x) +for s in res: + print(s) +PY +) + site_pkg_dir="" + while IFS= read -r sp; do + if [[ -d "$sp" ]]; then site_pkg_dir="$sp"; break; fi + done <<< "$site_pkgs" + + if [[ -n "$site_pkg_dir" ]]; then + pth_file="$site_pkg_dir/medeia_repo.pth" + if [[ -f "$pth_file" ]]; then + if grep -qxF "$REPO" "$pth_file" >/dev/null 2>&1; then + echo ".pth already present and contains repo root: $pth_file" >&2 + else + echo "$REPO" >> "$pth_file" + echo "Appended repo root to existing .pth: $pth_file" >&2 + fi else - if [[ "$QUIET" != "true" && -t 0 ]]; then - read -p "Top-level 'CLI' not importable; install project in editable mode now? (Y/n) " devans2 - if [[ -z "$devans2" || "$devans2" == "y" || "$devans2" == "Y" ]]; then + echo "$REPO" > "$pth_file" + echo "Wrote .pth adding repo root to venv site-packages: $pth_file" >&2 + fi + + # Re-check whether 'CLI' is now importable + if "$VENV_PY" -c 'import importlib,sys; importlib.import_module("CLI")' >/dev/null 2>&1; then + echo "Top-level 'CLI' import works after adding .pth" >&2 + else + echo "Adding .pth did not make top-level 'CLI' importable." >&2 + + # Fallback: if this is a git checkout, try editable reinstall or prompt user + if [[ -d "$REPO/.git" ]] || git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if [[ "$EDITABLE" == "true" ]]; then + echo "Editable install already requested; attempting editable reinstall for good measure..." >&2 "$VENV_PY" -m pip install -e "$REPO" || { echo "Editable install failed" >&2; exit 6; } + if "$VENV_PY" -c 'import importlib,sys; importlib.import_module("CLI")' >/dev/null 2>&1; then + echo "Top-level 'CLI' is now importable after reinstall." >&2 + else + echo "Editable reinstall did not make 'CLI' importable. Please inspect the venv or create an egg-link." >&2 + exit 6 + fi else + if [[ "$QUIET" != "true" && -t 0 ]]; then + read -p "Top-level 'CLI' not importable; install project in editable mode now? (Y/n) " devans2 + if [[ -z "$devans2" || "$devans2" == "y" || "$devans2" == "Y" ]]; then + "$VENV_PY" -m pip install -e "$REPO" || { echo "Editable install failed" >&2; exit 6; } + else + echo "Warning: continuing without top-level 'CLI' importable; some entrypoints may fail." >&2 + fi + else + echo "Top-level 'CLI' not importable and cannot prompt (quiet mode); aborting." >&2 + exit 6 + fi + fi + else + echo "Top-level 'CLI' not importable and not a git checkout; continuing at your own risk." >&2 + fi + fi + else + echo "Unable to determine site-packages directory to write .pth; skipping .pth fallback." >&2 + if [[ -d "$REPO/.git" ]] || git -C "$REPO" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if [[ "$EDITABLE" == "true" ]]; then + echo "Attempting editable install to provide top-level 'CLI'..." + "$VENV_PY" -m pip install -e "$REPO" || { echo "Editable install failed" >&2; exit 6; } + else + if [[ "$QUIET" != "true" && -t 0 ]]; then + read -p "Top-level 'CLI' not importable; install project in editable mode now? (Y/n) " devans2 + if [[ -z "$devans2" || "$devans2" == "y" || "$devans2" == "Y" ]]; then + "$VENV_PY" -m pip install -e "$REPO" || { echo "Editable install failed" >&2; exit 6; } + else + echo "Warning: continuing without top-level 'CLI' importable; some entrypoints may fail." >&2 + fi + else + echo "Top-level 'CLI' not importable and cannot prompt (quiet mode); aborting." >&2 + exit 6 + fi + fi + else + echo "Top-level 'CLI' not importable and not a git checkout; continuing." >&2 + fi + fi + fi echo "Continuing without editable install; 'mm' may not work as expected." >&2 fi else diff --git a/scripts/setup.py b/scripts/setup.py index 1d9437a..9c836e6 100644 --- a/scripts/setup.py +++ b/scripts/setup.py @@ -228,6 +228,56 @@ def main() -> int: 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):