mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 02:54:26 +00:00
Refactor scripts/install/lint.sh (~870 LOC bash) into a Python package
under utils/install/. scripts/install/lint.sh becomes a 7-line wrapper
that delegates to `python3 -m utils.install.lint`.
Layout:
- utils/install/{primitives,pip,npm,github_release,system_pkg}.py:
channel-agnostic installers
- utils/install/lint/<tool>.py: one ensure() per tool (actionlint,
shfmt, mbake, ruff, ansible_lint, galaxy_importer, markdownlint_cli2,
eslint, shellcheck, ansible_commands, ansible_collections)
- utils/install/lint/__main__.py: CLI + group dispatch + stamp logic
INFINITO_LINT_RUNNER dispatch:
- New env var (host | docker, default docker), tracked in env/static.env
+ passthrough handler + drift test.
- scripts/install/wrapper.sh routes `make install-lint` to host or
container based on the runner.
- scripts/lint/wrapper.sh already dispatched; container branch now uses
bash (not --login) so /etc/profile does not reset PATH, and forwards
BASH_ENV so the .env keys land in the lint script's env.
Stamp logic:
- Per-interpreter stamp at build/install-lint-<sha256(sys.executable)>.stamp
so host venv and container venv track installs independently despite
bind-mounted build/.
- `--force` drops the stamp; group args bypass the stamp.
Tests + docs:
- 43 unit tests under tests/unit/utils/install/{,lint/} cover primitives,
pip/npm/github_release/system_pkg fallback chains, and the main CLI
(stamp logic, group dispatch, error path).
- utils/install/README.md + utils/install/lint/README.md document the
structure and the per-tool ensure() contract.
- docs/contributing/tools/make.md install-lint row updated to mention
the runner dispatch + per-env stamp + install-lint-force.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
135 lines
4.3 KiB
Python
135 lines
4.3 KiB
Python
"""Pip-based installer with venv / sudo / --user fallback logic."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
from utils.install.primitives import ensure_dir_on_path, log
|
|
|
|
|
|
def detect_python_bin() -> str:
|
|
candidate = os.environ.get("PYTHON")
|
|
if candidate and shutil.which(candidate) is not None:
|
|
return candidate
|
|
if shutil.which("python3") is not None:
|
|
return "python3"
|
|
if shutil.which("python") is not None:
|
|
return "python"
|
|
raise RuntimeError("Need python, python3, curl, or wget.")
|
|
|
|
|
|
def python_runs_in_venv(python_bin: str) -> bool:
|
|
code = "import sys; raise SystemExit(0 if sys.prefix != getattr(sys, 'base_prefix', sys.prefix) else 1)"
|
|
result = subprocess.run([python_bin, "-c", code], capture_output=True, check=False)
|
|
return result.returncode == 0
|
|
|
|
|
|
def detect_python_scripts_dir(python_bin: str) -> str:
|
|
code = "import sysconfig; print(sysconfig.get_path('scripts') or '')"
|
|
result = subprocess.run(
|
|
[python_bin, "-c", code], capture_output=True, text=True, check=True
|
|
)
|
|
return result.stdout.strip()
|
|
|
|
|
|
def detect_python_user_scripts_dir(python_bin: str) -> str:
|
|
code = (
|
|
"import site, sys\n"
|
|
"ub = site.getuserbase()\n"
|
|
"sys.exit(1) if not ub else print(f'{ub}/bin')\n"
|
|
)
|
|
result = subprocess.run(
|
|
[python_bin, "-c", code], capture_output=True, text=True, check=False
|
|
)
|
|
if result.returncode != 0:
|
|
return ""
|
|
return result.stdout.strip()
|
|
|
|
|
|
def pip_supports_break_system_packages(python_bin: str) -> bool:
|
|
result = subprocess.run(
|
|
[python_bin, "-m", "pip", "install", "--help"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode != 0:
|
|
return False
|
|
return "--break-system-packages" in result.stdout
|
|
|
|
|
|
def install_pip_pkg(pip_spec: str) -> None:
|
|
python_bin = detect_python_bin()
|
|
scripts_dir = ""
|
|
user_scripts_dir = ""
|
|
|
|
try:
|
|
scripts_dir = detect_python_scripts_dir(python_bin)
|
|
except subprocess.CalledProcessError:
|
|
scripts_dir = ""
|
|
|
|
log(f"Installing Python package '{pip_spec}' via {python_bin} -m pip")
|
|
|
|
pip_args = [python_bin, "-m", "pip", "install", "--upgrade", pip_spec]
|
|
|
|
if python_runs_in_venv(python_bin):
|
|
try:
|
|
subprocess.run(pip_args, check=True)
|
|
except subprocess.CalledProcessError as exc:
|
|
raise RuntimeError(f"pip install failed for '{pip_spec}' in venv") from exc
|
|
elif os.geteuid() == 0:
|
|
try:
|
|
subprocess.run(pip_args, check=True)
|
|
except subprocess.CalledProcessError:
|
|
if not pip_supports_break_system_packages(python_bin):
|
|
raise RuntimeError(
|
|
f"pip install failed for '{pip_spec}' (root) and "
|
|
"--break-system-packages is unsupported"
|
|
) from None
|
|
log("Retrying Python package install with --break-system-packages")
|
|
try:
|
|
subprocess.run(
|
|
[
|
|
python_bin,
|
|
"-m",
|
|
"pip",
|
|
"install",
|
|
"--break-system-packages",
|
|
"--upgrade",
|
|
pip_spec,
|
|
],
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as exc:
|
|
raise RuntimeError(
|
|
f"pip install --break-system-packages failed for '{pip_spec}'"
|
|
) from exc
|
|
else:
|
|
try:
|
|
subprocess.run(
|
|
[python_bin, "-m", "pip", "install", "--user", "--upgrade", pip_spec],
|
|
check=True,
|
|
)
|
|
except subprocess.CalledProcessError as exc:
|
|
raise RuntimeError(f"pip install --user failed for '{pip_spec}'") from exc
|
|
user_scripts_dir = detect_python_user_scripts_dir(python_bin)
|
|
|
|
if scripts_dir and Path(scripts_dir).is_dir():
|
|
ensure_dir_on_path(scripts_dir)
|
|
|
|
if user_scripts_dir and Path(user_scripts_dir).is_dir():
|
|
ensure_dir_on_path(user_scripts_dir)
|
|
|
|
|
|
__all__ = [
|
|
"detect_python_bin",
|
|
"detect_python_scripts_dir",
|
|
"detect_python_user_scripts_dir",
|
|
"install_pip_pkg",
|
|
"pip_supports_break_system_packages",
|
|
"python_runs_in_venv",
|
|
]
|