mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-07 03:15:56 +00:00
Replace 8 bash env modules with a single Python generator
(cli.meta.env) that materialises env/static.env + runtime context
into a gitignored .env. Each variable lives in its own handler
under utils/env/handlers/; defaults carry inline one-line `# ...`
comments.
Rename to the INFINITO_* schema:
- workflow inputs: APPS, SERVICES_DISABLED, CMD, DEBUG, WHITELIST,
BUNDLES, FULL_CYCLE, VARIANT, RUN_FLAGS, LIMIT_HOST, PARALLEL
- inventory: INVENTORY_DIR / INVENTORY_FILE / HOST_VARS_FILE,
PW_FILE -> INVENTORY_PASSWORD_FILE
- venv: VENV_BASE / VENV_FALLBACK / VENV_NAME
- new SPOT defaults: COMPILE_SILENCE, CONTAINER_NO_CA,
WAIT_HEALTH_TIMEOUT_S, PACKAGE_CACHE_FRONTEND_INIT_IMAGE,
promoted IMAGE_TAG from GHA-only to general passthrough
compose.yml hardening:
- inline \${VAR:-default} removed; strict \${VAR:?...} everywhere
- new integration lint blocks regressions
Tooling:
- scripts/install/all.sh owns the stamp-file install logic
(Makefile target delegates)
- scripts/meta/env/load.sh is the single shell entry point;
8 bash modules deleted
- new shell lint: no INFINITO_* defaults outside env/static.env
Tests:
- 93 new unit tests for utils/env/{builder,parser,runtime,writer}
- restructure tests/lint into repository/ (file-content rules) vs
filesystem/{shell,python,env}/ (filesystem-shape rules)
- drift + smoke integration tests against the generator
Docs:
- variables.md is now a pointer; per-variable docs live next to
the key in env/static.env / env/ci.env
- compose.yml.md drops the Default column
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
122 lines
3.6 KiB
Python
122 lines
3.6 KiB
Python
"""Runtime-context lookups for the .env generator.
|
|
|
|
Each helper resolves a single piece of information from the host the
|
|
generator runs on (disk free space, RAM, hostname, /proc/version,
|
|
GitHub Actions env vars, ...). They are intentionally tiny and
|
|
side-effect-free apart from the OS read they need to perform, so they
|
|
can be exercised independently in tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import re
|
|
import socket
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
|
|
def is_user_writable(path: str) -> bool:
|
|
"""Return True when `path` (or its closest existing ancestor) can
|
|
be written to by the current user without privilege escalation.
|
|
|
|
Used to decide between a system path (e.g. ``/opt/venvs``) and a
|
|
user-local fallback (``$HOME/.venvs``) when the system path is not
|
|
yet prepared on a fresh dev machine."""
|
|
p = Path(path)
|
|
while not p.exists() and str(p) not in ("/", "."):
|
|
p = p.parent
|
|
return os.access(p, os.W_OK)
|
|
|
|
|
|
def df_avail_gb(path: str) -> int:
|
|
"""Return free space in GB at `path` (or the closest existing
|
|
ancestor). 0 when `df` fails or the output is unparseable."""
|
|
p = Path(path)
|
|
while not p.exists() and str(p) not in ("/", "."):
|
|
p = p.parent
|
|
try:
|
|
out = subprocess.check_output(
|
|
["df", "--output=avail", "-B1G", str(p)],
|
|
text=True,
|
|
stderr=subprocess.DEVNULL,
|
|
)
|
|
except (subprocess.CalledProcessError, OSError):
|
|
return 0
|
|
lines = out.strip().splitlines()
|
|
if len(lines) < 2:
|
|
return 0
|
|
try:
|
|
return int(lines[1].strip())
|
|
except ValueError:
|
|
return 0
|
|
|
|
|
|
def mem_available_mb() -> int:
|
|
"""`MemAvailable` from /proc/meminfo in MB. 0 on failure."""
|
|
try:
|
|
text = Path(
|
|
"/proc/meminfo"
|
|
).read_text() # nocheck: cache-read -- live kernel pseudo-file
|
|
except OSError:
|
|
return 0
|
|
for line in text.splitlines():
|
|
if line.startswith("MemAvailable:"):
|
|
try:
|
|
kb = int(line.split()[1])
|
|
return kb // 1024
|
|
except (IndexError, ValueError):
|
|
return 0
|
|
return 0
|
|
|
|
|
|
def is_wsl2() -> bool:
|
|
"""True when /proc/version mentions microsoft or wsl. False otherwise."""
|
|
try:
|
|
text = Path(
|
|
"/proc/version"
|
|
).read_text() # nocheck: cache-read -- live kernel pseudo-file
|
|
except OSError:
|
|
return False
|
|
return bool(re.search(r"microsoft|wsl", text, re.IGNORECASE))
|
|
|
|
|
|
def detect_gha_act() -> tuple[bool, bool]:
|
|
"""Return (running_on_github, running_on_act).
|
|
|
|
Mirrors the legacy scripts/meta/env/runtime.sh logic:
|
|
- GITHUB_ACTIONS=true AND ACT=true -> act (local nektos act)
|
|
- GITHUB_ACTIONS=true AND not ACT -> real github actions
|
|
"""
|
|
gha = os.environ.get("GITHUB_ACTIONS") == "true"
|
|
act = gha and os.environ.get("ACT") == "true"
|
|
return (gha and not act, act)
|
|
|
|
|
|
def hostname() -> str:
|
|
"""`socket.gethostname()` with a `local` fallback for sandboxed hosts
|
|
that fail the syscall."""
|
|
try:
|
|
name = socket.gethostname()
|
|
except OSError:
|
|
return "local"
|
|
return name or "local"
|
|
|
|
|
|
def run_helper(
|
|
cmd: list[str],
|
|
cwd: Path,
|
|
extra_env: dict[str, str] | None = None,
|
|
) -> str:
|
|
"""Run an existing shell helper, return stripped stdout. Empty
|
|
string on failure. Helpers run with `cwd` as their working
|
|
directory."""
|
|
env = {**os.environ, **(extra_env or {})}
|
|
try:
|
|
out = subprocess.check_output(
|
|
cmd, cwd=str(cwd), env=env, text=True, stderr=subprocess.DEVNULL
|
|
)
|
|
return out.strip()
|
|
except (subprocess.CalledProcessError, OSError, FileNotFoundError):
|
|
return ""
|