refactor(env+install/spot): collapse python/venv resolution into shell SPOT

Pulls VENV/PYTHON/PIP/PYTHONPATH out of the dotenv handler pipeline
into scripts/meta/python/detect.sh. load.sh now sources detect.sh and
prepends ${VENV}/bin to PATH above the INFINITO_ENV_LOADED guard, so
every BASH_ENV re-entry re-resolves PYTHON the moment a venv
materialises mid-chain — the same fix as b79ed2869 but at the SPOT,
which retires the reload-trick from ansible.sh, python.sh and
pre-commit.sh.

Also renames INVENTORY_VARS_FILE → INFINITO_INVENTORY_VARS_FILE under
the new env-prefix lint, drops the 5 dead INFINITO_ENV_*_LOADED unsets
left over from the multi-loader era, and adds a lint guarding the
INFINITO_ prefix invariant on env/*.env keys.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.7 committed 2026-05-21 00:39:52 +02:00
1 parent 20b78fa778
commit 80c396b839
26 files changed
+172 -257

No files matched your search

@@ -10,10 +10,10 @@ if TYPE_CHECKING:
from .compose import Compose
# Mirrors INVENTORY_VARS_FILE from scripts/meta/env/load.sh; a unit
# test locks the two literals together so they cannot drift.
DEV_INVENTORY_VARS_FILE: str = os.environ.get(
"INVENTORY_VARS_FILE", "inventories/development/default.yml"
# Mirrors INFINITO_INVENTORY_VARS_FILE from env/default.env; drift-guarded.
DEV_INVENTORY_VARS_FILE: str = (
os.environ.get("INFINITO_INVENTORY_VARS_FILE")
or "inventories/development/default.yml"
)
+1 -1
View File
@@ -85,7 +85,7 @@ For TLS-enabled local sites, run [`make trust-ca`](../../../Makefile) once after
|---|---|
| [Makefile](../../../Makefile) | All `deploy-*` and `make exec` / `make trust-ca` targets. |
| [dev CLI tree](../../../cli/administration/deploy/development/) | Python CLI for init, deploy, up, down, exec, etc. |
| [`make dotenv`](../../../Makefile) | Resolves `INFINITO_INVENTORY_DIR`, `INFINITO_INVENTORY_FILE`, and `INVENTORY_VARS_FILE` into `.env` for the dev CLI (see [variables.md](../environment/variables.md)). |
| [`make dotenv`](../../../Makefile) | Resolves `INFINITO_INVENTORY_DIR`, `INFINITO_INVENTORY_FILE`, and `INFINITO_INVENTORY_VARS_FILE` into `.env` for the dev CLI (see [variables.md](../environment/variables.md)). |
| [local deploy scripts](../../../scripts/tests/deploy/local/) | Bash glue behind the make targets (fresh / reuse / purge variants). |
| [variants.md](../design/variants.md) | Matrix-variant deep dive. |
| [role.md](../../agents/action/iteration/role.md) | Role-iteration loop for agents. Recommended reading even for human contributors. |
Vendored
-1
View File
@@ -17,4 +17,3 @@ INFINITO_DOMAIN_RE=infinito\.example
# IPv4 of the infinito application container inside INFINITO_SUBNET.
INFINITO_IP4=172.30.0.10
+3 -13
View File
@@ -112,6 +112,9 @@ INFINITO_GHCR_MIRROR_PREFIX=mirror
# Image tag used to address the published infinito image (CI publishes 'latest' on push).
INFINITO_IMAGE_TAG=latest
# Repo-relative path to the Ansible vars file `infinito administration inventory provision --vars-file <...>` consumes.
INFINITO_INVENTORY_VARS_FILE=inventories/development/default.yml
# True when /proc/version identifies the host as WSL2; generator overrides via runtime detection.
INFINITO_IS_WSL2=false
@@ -166,12 +169,6 @@ INFINITO_TEST_RUNNER=docker
# Pin matrix-variant deploys to a single round (empty -> iterate all variants).
INFINITO_VARIANT=
# Parent directory under which named virtualenvs are created.
INFINITO_VENV_BASE=/opt/venvs
# Name of the project venv (used to derive INFINITO_VENV_FALLBACK).
INFINITO_VENV_NAME=infinito
# Seconds 'make up' waits for the infinito container healthcheck before failing.
INFINITO_WAIT_HEALTH_TIMEOUT_S=600
@@ -180,10 +177,3 @@ INFINITO_WHITELIST=
# Master toggle for the worker SPOT chain (INFINITO_WORKER_CPU / _FETCH); falsy values: 0/false/no/off.
INFINITO_WORKER_ENABLED=true
# Repo-relative path to the Ansible vars file `infinito administration inventory provision --vars-file <...>` consumes.
INVENTORY_VARS_FILE=inventories/development/default.yml
# PYTHONPATH for ad-hoc `python -m ...` invocations (`.` = repo root).
PYTHONPATH=.
-9
View File
@@ -1,15 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# 🔁 Reload env after venv.sh: first env-gen runs before the venv exists, so PYTHON is the system fallback.
if [[ -n "${VENV:-}" && -x "${VENV}/bin/python" && "${PYTHON:-}" != "${VENV}/bin/python" ]]; then
unset INFINITO_ENV_LOADED
unset PYTHON
unset PIP
# shellcheck source=scripts/meta/env/load.sh
source "${ENV_SH:-scripts/meta/env/load.sh}"
fi
echo "📦 Installing Ansible collections"
: "${PYTHON:?PYTHON not set}"
-10
View File
@@ -1,16 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# 🔁 Reload env to pick up newly created venv
if [[ -n "${VENV:-}" && -x "${VENV}/bin/python" && "${PYTHON:-}" != "${VENV}/bin/python" ]]; then
unset INFINITO_ENV_LOADED
unset INFINITO_ENV_PYTHON_LOADED
unset PYTHON
unset PIP
# shellcheck source=scripts/meta/env/load.sh
source "${ENV_SH:-scripts/meta/env/load.sh}"
fi
: "${PYTHON:?PYTHON not set}"
echo "🔗 Installing pre-commit hooks"
-10
View File
@@ -1,16 +1,6 @@
#!/usr/bin/env bash
set -euo pipefail
# 🔁 Reload env to pick up newly created venv
if [[ -n "${VENV:-}" && -x "${VENV}/bin/python" && "${PYTHON:-}" != "${VENV}/bin/python" ]]; then
unset INFINITO_ENV_LOADED
unset INFINITO_ENV_PYTHON_LOADED
unset PYTHON
unset PIP
# shellcheck source=scripts/meta/env/load.sh
source "${ENV_SH:-scripts/meta/env/load.sh}"
fi
: "${PYTHON:?PYTHON not set}"
EXTRA="${1:-}"
+1 -3
View File
@@ -20,7 +20,6 @@ set -euo pipefail
# ------------------------------------------------------------
: "${VENV:?VENV not set (e.g. /opt/venvs/infinito)}"
: "${INFINITO_VENV_BASE:?INFINITO_VENV_BASE not set (e.g. /opt/venvs)}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
@@ -41,12 +40,11 @@ install_venv() {
local venv_python="${VENV}/bin/python"
echo "🐍 Virtualenv target : ${VENV}"
echo "📁 Virtualenv base : ${INFINITO_VENV_BASE}"
echo "🛠 Bootstrap python : ${bootstrap_python}"
echo "🎯 Venv python target : ${venv_python}"
echo
# Probe dirname(VENV), not INFINITO_VENV_BASE: VENV may live outside the legacy base, and a non-writable legacy path triggers a spurious sudo (fatal under no_new_privs sandboxes).
# Probe dirname(VENV) so a non-writable legacy path can't trigger a spurious sudo (fatal under no_new_privs sandboxes).
local venv_parent
venv_parent="$(dirname "${VENV}")"
if [[ -n "${VIRTUAL_ENV:-}" && "${VIRTUAL_ENV}" == "${VENV}" ]]; then
+26 -53
View File
@@ -1,57 +1,43 @@
#!/usr/bin/env bash
# shellcheck shell=bash
#
# Project env loader -- the single entry point that pulls the generated
# `.env` file into the current shell. `.env` is produced by
# `python -m cli.meta.env` (a.k.a. `make dotenv`) from the committed
# `env/default.env` plus runtime context (distro, GHA/Act flags,
# df/meminfo-derived sizes, sha256 secrets, etc.). `.env` is the single
# source of truth for both static defaults and runtime-resolved values.
#
# Behaviour:
# * Idempotent via INFINITO_ENV_LOADED guard.
# * Auto-generates `.env` on first source if it is missing, so a cold
# sourcing this loader works even before `make dotenv` has run. # nocheck: self-path-reference
# * Exports every variable declared in `.env`, BUT respects values
# that are already set in the caller's environment (setdefault
# semantics): `FOO=bar make ...` keeps `FOO=bar` even when `.env`
# declares its own default for `FOO`.
# * Prepends ${VENV}/bin to PATH when the venv exists (mirrors the
# legacy python.sh side-effect; not exported into `.env` because
# mutating PATH via a static snapshot is brittle).
#
# Re-entrancy guard:
# The Makefile exports BASH_ENV=$(ENV_SH), so every bash subprocess --
# including those spawned by `python -m cli.meta.env` itself (which
# shells out to e.g. scripts/inventory/resolve.sh) -- would otherwise
# re-enter this script. While generating, `.env` is still missing,
# and the generator would be called recursively until OOM. We set
# INFINITO_ENV_GENERATING=1 around the generator invocation and
# short-circuit here when we see it, so subprocesses skip the load
# entirely during generation.
if [[ "${INFINITO_ENV_LOADED:-}" == "1" ]]; then # nocheck: env-loader-internal-guard
return 0
fi
if [[ "${INFINITO_ENV_GENERATING:-}" == "1" ]]; then # nocheck: env-loader-internal-guard
return 0
fi
# Sources `.env` (auto-generates via `python -m cli.meta.env` if missing)
# with setdefault semantics: caller-set values win over `.env` defaults.
# Idempotent (INFINITO_ENV_LOADED). INFINITO_ENV_GENERATING short-circuits
# re-entry from generator subshells (BASH_ENV would otherwise recurse).
_infinito_env_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
_infinito_env_dotenv="${_infinito_env_repo_root}/.env"
# Re-resolve PYTHON + PATH on every source so a venv created mid-chain takes over.
# shellcheck source=scripts/meta/python/detect.sh
source "${_infinito_env_repo_root}/scripts/meta/python/detect.sh"
if [[ -n "${VENV:-}" && -d "${VENV%/}/bin" ]]; then
case ":${PATH:-}:" in
*:"${VENV%/}/bin":*) ;;
*) export PATH="${VENV%/}/bin${PATH:+:${PATH}}" ;;
esac
fi
if [[ "${INFINITO_ENV_LOADED:-}" == "1" ]]; then # nocheck: env-loader-internal-guard
unset _infinito_env_repo_root _infinito_env_dotenv
return 0
fi
if [[ "${INFINITO_ENV_GENERATING:-}" == "1" ]]; then # nocheck: env-loader-internal-guard
unset _infinito_env_repo_root _infinito_env_dotenv
return 0
fi
if [[ ! -f "${_infinito_env_dotenv}" ]]; then
(
cd "${_infinito_env_repo_root}" || exit 1
export INFINITO_ENV_GENERATING=1
python3 -m cli.meta.env
"${PYTHON}" -m cli.meta.env
) >&2
fi
# Snapshot any pre-existing non-empty values for keys declared in
# `.env` so we can restore them after the bulk `source`. This gives us
# setdefault semantics: caller-set values win over the static default
# in `.env`.
# Snapshot caller-set values, restore them after the bulk source (setdefault).
declare -A _infinito_env_preserved=()
while IFS= read -r _infinito_env_line; do
case "${_infinito_env_line}" in
@@ -77,18 +63,5 @@ for _infinito_env_key in "${!_infinito_env_preserved[@]}"; do
done
unset _infinito_env_key _infinito_env_preserved
if [[ -n "${VENV:-}" ]]; then
_infinito_venv_bin="${VENV%/}/bin"
if [[ -d "${_infinito_venv_bin}" ]]; then
case ":${PATH:-}:" in
*:"${_infinito_venv_bin}":*) ;;
*)
export PATH="${_infinito_venv_bin}${PATH:+:${PATH}}"
;;
esac
fi
unset _infinito_venv_bin
fi
export INFINITO_ENV_LOADED="1"
unset _infinito_env_repo_root _infinito_env_dotenv
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# shellcheck shell=bash
#
# SPOT for venv + Python interpreter resolution. Exports VENV, PYTHON,
# PIP, PYTHONPATH. Sourced; re-entrant.
VENV_NAME=infinito
if [[ -n "${VIRTUAL_ENV:-}" ]]; then
VENV_BASE="$(dirname "${VIRTUAL_ENV}")"
elif [[ -d /opt/venvs && -w /opt/venvs ]]; then
VENV_BASE=/opt/venvs
else
VENV_BASE="${HOME}/.venvs"
fi
VENV_FALLBACK="${VENV_BASE%/}/${VENV_NAME}"
VENV="${VENV:-${VIRTUAL_ENV:-${VENV_FALLBACK}}}"
if [[ -x "${VENV}/bin/python" ]]; then
PYTHON="${VENV}/bin/python"
elif command -v python3 >/dev/null 2>&1; then
PYTHON=python3
else
echo "detect.sh: no python3 on PATH and no venv at ${VENV}/bin/python" >&2
# shellcheck disable=SC2317
return 1 2>/dev/null || exit 1
fi
PIP="${PYTHON} -m pip"
PYTHONPATH="${PYTHONPATH:-.}"
export VENV PYTHON PIP PYTHONPATH
@@ -17,7 +17,7 @@ source "${SCRIPT_DIR}/../utils/cache-retry.sh"
: "${INFINITO_TEST_DEPLOY_TYPE:?INFINITO_TEST_DEPLOY_TYPE must be set (server|workstation|universal)}"
: "${INFINITO_INVENTORY_DIR:?INFINITO_INVENTORY_DIR must be set (e.g. /etc/inventories/local-full-server)}"
: "${INFINITO_INVENTORY_FILE:?INFINITO_INVENTORY_FILE is not set — source scripts/meta/env/load.sh first}"
: "${INVENTORY_VARS_FILE:?INVENTORY_VARS_FILE is not set — source scripts/meta/env/load.sh first}"
: "${INFINITO_INVENTORY_VARS_FILE:?INFINITO_INVENTORY_VARS_FILE is not set — source scripts/meta/env/load.sh first}"
# Optional overrides
INFINITO_WHITELIST="${INFINITO_WHITELIST:-}"
@@ -108,7 +108,7 @@ deploy_with_cache_retry "fresh-kept-all" -- \
"${PYTHON}" -m cli.administration.deploy.development exec \
--env "INFINITO_INVENTORY_DIR=${INFINITO_INVENTORY_DIR}" \
--env "INFINITO_INVENTORY_FILE=${INFINITO_INVENTORY_FILE}" \
--env "INVENTORY_VARS_FILE=${INVENTORY_VARS_FILE}" \
--env "INFINITO_INVENTORY_VARS_FILE=${INFINITO_INVENTORY_VARS_FILE}" \
--env "APPS_CSV=${apps_csv}" \
--env "APPS_COUNT=${apps_count}" \
--env "INFINITO_LIMIT_HOST=${INFINITO_LIMIT_HOST}" \
@@ -12,7 +12,7 @@ set -euo pipefail
: "${INFINITO_TEST_DEPLOY_TYPE:?INFINITO_TEST_DEPLOY_TYPE must be set (server|workstation|universal)}"
: "${INFINITO_INVENTORY_DIR:?INFINITO_INVENTORY_DIR must be set (e.g. /etc/inventories/local-full-server)}"
: "${INFINITO_INVENTORY_FILE:?INFINITO_INVENTORY_FILE is not set — source scripts/meta/env/load.sh first}"
: "${INVENTORY_VARS_FILE:?INVENTORY_VARS_FILE is not set — source scripts/meta/env/load.sh first}"
: "${INFINITO_INVENTORY_VARS_FILE:?INFINITO_INVENTORY_VARS_FILE is not set — source scripts/meta/env/load.sh first}"
# This script always generates inventories for the development compose stack.
RUNTIME_VARS_JSON='{"RUNTIME":"dev","SYS_SERVICE_RUNNER_RETRIES":1}'
@@ -99,7 +99,7 @@ echo ">>> Initializing inventory inside container"
--vars '${RUNTIME_VARS_JSON}' \
--host 'localhost' \
--ssl-disabled \
--vars-file '${INVENTORY_VARS_FILE}' \
--vars-file '${INFINITO_INVENTORY_VARS_FILE}' \
--include '${apps_csv}'
echo '✅ Inventory initialized.'
@@ -11,7 +11,7 @@
# Required env:
# INFINITO_INVENTORY_DIR absolute base inventory dir (no trailing slash)
# INFINITO_INVENTORY_FILE absolute path to <INFINITO_INVENTORY_DIR>/devices.yml
# INVENTORY_VARS_FILE repo-relative dev vars file (SPOT)
# INFINITO_INVENTORY_VARS_FILE repo-relative dev vars file (SPOT)
# APPS_CSV comma-separated app id list for `--include`
# APPS_COUNT length of APPS_CSV (echoed for log clarity)
# INFINITO_LIMIT_HOST Ansible host (typically "localhost")
@@ -21,7 +21,7 @@ cd /opt/src/infinito
: "${INFINITO_INVENTORY_DIR:?INFINITO_INVENTORY_DIR must be set}"
: "${INFINITO_INVENTORY_FILE:?INFINITO_INVENTORY_FILE must be set}"
: "${INVENTORY_VARS_FILE:?INVENTORY_VARS_FILE must be set}"
: "${INFINITO_INVENTORY_VARS_FILE:?INFINITO_INVENTORY_VARS_FILE must be set}"
: "${APPS_CSV:?APPS_CSV must be set}"
: "${APPS_COUNT:?APPS_COUNT must be set}"
: "${INFINITO_LIMIT_HOST:?INFINITO_LIMIT_HOST must be set}"
@@ -48,7 +48,7 @@ infinito administration inventory provision "${inv_dir}" \
--host "${INFINITO_LIMIT_HOST}" \
--ssl-disabled \
--vars "${RUNTIME_VARS_JSON}" \
--vars-file "${INVENTORY_VARS_FILE}" \
--vars-file "${INFINITO_INVENTORY_VARS_FILE}" \
--include "${APPS_CSV}"
echo ">>> Deploying against ${INFINITO_INVENTORY_FILE}"
@@ -43,11 +43,6 @@ load_repo_env() {
previous_pwd="$(pwd)"
cd "${REPO_ROOT}"
unset INFINITO_ENV_LOADED
unset INFINITO_ENV_PYTHON_LOADED
unset INFINITO_ENV_RUNTIME_LOADED
unset INFINITO_ENV_DEFAULTS_LOADED
unset INFINITO_ENV_INVENTORY_LOADED
unset INFINITO_ENV_GITHUB_LOADED
unset PYTHON
unset PIP
# shellcheck source=scripts/meta/env/load.sh
+1 -8
View File
@@ -75,14 +75,7 @@ EXPECTED_BASELINE_KEYS = frozenset(
"INFINITO_WORKER_ENABLED",
"INFINITO_WORKER_CPU",
"INFINITO_WORKER_FETCH",
"VENV",
"INFINITO_VENV_BASE",
"INFINITO_VENV_NAME",
"INFINITO_VENV_FALLBACK",
"PYTHON",
"PIP",
"PYTHONPATH",
"INVENTORY_VARS_FILE",
"INFINITO_INVENTORY_VARS_FILE",
"INFINITO_CACHE_REGISTRY_HOST_PATH",
"INFINITO_CACHE_REGISTRY_CA_HOST_PATH",
"INFINITO_CACHE_REGISTRY_MAX_SIZE",
+81
View File
@@ -0,0 +1,81 @@
"""Every key declared in `env/*.env` must carry the `INFINITO_` prefix.
The shared namespace exists so project env-vars never collide with
generic shell or Ansible-role variables of the same short name. Any
key without the prefix is either leaking into the wrong scope or
needs the prefix added.
"""
from __future__ import annotations
import re
import unittest
from dataclasses import dataclass
from typing import TYPE_CHECKING
from utils.cache.files import read_text
from . import PROJECT_ROOT
if TYPE_CHECKING:
from pathlib import Path
_KEY_RE = re.compile(r"^\s*(?P<key>[A-Za-z_][A-Za-z0-9_]*)\s*=")
_INFINITO_KEY_RE = re.compile(r"^INFINITO_[A-Z0-9_]+$")
@dataclass(frozen=True)
class _Violation:
file: str
line_no: int
key: str
def _scan_file(path: Path) -> list[_Violation]:
rel = path.relative_to(PROJECT_ROOT).as_posix()
out: list[_Violation] = []
for index, line in enumerate(read_text(str(path)).splitlines(), start=1):
match = _KEY_RE.match(line)
if match is None:
continue
key = match.group("key")
if _INFINITO_KEY_RE.match(key):
continue
out.append(_Violation(rel, index, key))
return out
def _scan_targets() -> list[Path]:
return sorted((PROJECT_ROOT / "env").glob("*.env"))
class TestEnvFilesInfinitoPrefix(unittest.TestCase):
def test_every_env_file_key_carries_infinito_prefix(self) -> None:
targets = _scan_targets()
self.assertTrue(targets, "no env/*.env files found to scan")
violations: list[_Violation] = []
for path in targets:
violations.extend(_scan_file(path))
if not violations:
return
lines = [
f"env/*.env keys without the INFINITO_ prefix "
f"({len(violations)} violations):",
"",
"Every key in env/*.env must start with INFINITO_ so it cannot "
"collide with generic shell or Ansible-role variables of the "
"same short name. Rename the key to INFINITO_<NAME> (and update "
"callers) or move it out of env/*.env if it is not part of the "
"project's INFINITO_* namespace.",
"",
"Offenders:",
]
lines.extend(f" {v.file}:{v.line_no}: {v.key}" for v in violations)
self.fail("\n".join(lines))
if __name__ == "__main__":
unittest.main()
+8
View File
@@ -46,6 +46,7 @@ _NOCHECK_RE = re.compile(r"#\s*nocheck\b")
_EXCLUDED_PREFIXES = (
"tests/lint/",
"utils/env/handlers/",
"scripts/meta/python/",
"env/",
)
@@ -66,6 +67,13 @@ def _registered_keys() -> set[str]:
if match is not None:
keys.add(match.group("key"))
python_detect = PROJECT_ROOT / "scripts" / "meta" / "python" / "detect.sh"
if python_detect.is_file():
for line in read_text(str(python_detect)).splitlines():
match = _DEFAULT_ENV_KEY_RE.match(line)
if match is not None:
keys.add(match.group("key"))
handlers_dir = PROJECT_ROOT / "utils" / "env" / "handlers"
for module in sorted(handlers_dir.glob("*.py")):
if module.name == "__init__.py":
@@ -3,7 +3,7 @@
The literal path "inventories/development/default.yml" is exposed in
TWO places that must stay in lockstep:
* `env/default.env` — `INVENTORY_VARS_FILE` (SPOT-of-record for callers
* `env/default.env` — `INFINITO_INVENTORY_VARS_FILE` (SPOT-of-record for callers
that go through `make dotenv` / the bash deploy chain).
* `cli/administration/deploy/development/common.DEV_INVENTORY_VARS_FILE`
— fallback for direct/test invocations that bypass the bash chain.
@@ -28,15 +28,15 @@ STATIC_ENV = PROJECT_ROOT / "env" / "default.env"
def _parse_env_default(file_text: str) -> str:
# Match: INVENTORY_VARS_FILE=<DEFAULT> (optional surrounding quotes)
# Match: INFINITO_INVENTORY_VARS_FILE=<DEFAULT> (optional surrounding quotes)
pattern = re.compile(
r'^\s*INVENTORY_VARS_FILE=(?P<value>"[^"]*"|\'[^\']*\'|[^\s#]+)\s*(#.*)?$',
r'^\s*INFINITO_INVENTORY_VARS_FILE=(?P<value>"[^"]*"|\'[^\']*\'|[^\s#]+)\s*(#.*)?$',
re.MULTILINE,
)
match = pattern.search(file_text)
if not match:
raise AssertionError(
"Could not find INVENTORY_VARS_FILE assignment in "
"Could not find INFINITO_INVENTORY_VARS_FILE assignment in "
f"{STATIC_ENV}; pattern was tightened intentionally so any "
"rewrite of that line forces a matching update here."
)
@@ -52,7 +52,7 @@ class TestInventoryVarsFileSpotDriftGuard(unittest.TestCase):
self.assertEqual(
env_default,
DEV_INVENTORY_VARS_FILE,
"INVENTORY_VARS_FILE default in env/default.env drifted from "
"INFINITO_INVENTORY_VARS_FILE default in env/default.env drifted from "
"cli.administration.deploy.development.common.DEV_INVENTORY_VARS_FILE. "
"Update both literals (or, better, rebase one on the other).",
)
+1 -2
View File
@@ -13,8 +13,7 @@ The orchestrator in [builder.py](../builder.py) iterates the registry in [__init
- A handler module name MUST match the lowercase of the variable it owns:
- `INFINITO_CONTAINER` -> `infinito_container.py`
- `INFINITO_VENV_BASE` -> `infinito_venv_base.py`
- `VENV` (no `INFINITO_` prefix in the key) -> `venv.py`
- `INFINITO_GHCR_MIRROR_PREFIX` -> `infinito_ghcr_mirror_prefix.py`
- The `infinito_` prefix in the filename MUST be present iff the owned key carries the `INFINITO_` prefix.
- Aggregate handlers (one helper call resolves several keys at once) MUST take the singular topic name without a key suffix.
Example: `infinito_inventory.py` resolves `INFINITO_INVENTORY_DIR`, `INFINITO_INVENTORY_FILE`, and `INFINITO_INVENTORY_HOST_VARS_FILE` from one `scripts/inventory/resolve.sh` invocation.
-10
View File
@@ -24,15 +24,10 @@ from . import (
infinito_registry_cache_max_size,
infinito_running_on_act,
infinito_running_on_github,
infinito_venv_base,
infinito_venv_fallback,
infinito_worker_cpu,
infinito_worker_fetch,
nix_config,
passthrough,
pip,
python,
venv,
)
ORDERED_HANDLERS = [
@@ -43,11 +38,6 @@ ORDERED_HANDLERS = [
infinito_running_on_act,
infinito_running_on_github,
infinito_is_wsl2,
infinito_venv_base,
infinito_venv_fallback,
venv,
python,
pip,
infinito_inventory,
gha_passthrough,
infinito_pull_policy,
-31
View File
@@ -1,31 +0,0 @@
"""INFINITO_VENV_BASE precedence:
1. An active VIRTUAL_ENV pins the base to its parent (caller is already
inside a managed venv, respect it).
2. The default.env default (typically ``/opt/venvs``) wins when it is
user-writable -- this is the container path.
3. Otherwise fall back to ``$HOME/.venvs`` so a non-root host user can
create the venv without sudo.
"""
from __future__ import annotations
import os
from pathlib import Path
from typing import TYPE_CHECKING
from utils.env.runtime import is_user_writable
if TYPE_CHECKING:
from utils.env.builder import BuildContext, EnvBuilder
KEY = "INFINITO_VENV_BASE"
def apply(eb: EnvBuilder, ctx: BuildContext) -> None:
comment = ctx.static_comments.get(KEY, "")
virtual_env = os.environ.get("VIRTUAL_ENV", "").strip()
if virtual_env:
eb.set(KEY, str(Path(virtual_env).parent) + "/", comment=comment)
elif not is_user_writable(eb.get(KEY) or "/opt/venvs"):
eb.set(KEY, str(Path.home() / ".venvs") + "/", comment=comment)
-21
View File
@@ -1,21 +0,0 @@
"""INFINITO_VENV_FALLBACK: derived venv path when VIRTUAL_ENV is unset:
``$INFINITO_VENV_BASE/$INFINITO_VENV_NAME``."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from utils.env.builder import BuildContext, EnvBuilder
KEY = "INFINITO_VENV_FALLBACK"
COMMENT = (
"Derived venv path when VIRTUAL_ENV is unset: "
"$INFINITO_VENV_BASE/$INFINITO_VENV_NAME."
)
def apply(eb: EnvBuilder, ctx: BuildContext) -> None:
venv_base = eb.get("INFINITO_VENV_BASE") or "/opt/venvs"
venv_name = eb.get("INFINITO_VENV_NAME") or "infinito"
eb.setdefault(KEY, f"{venv_base.rstrip('/')}/{venv_name}", comment=COMMENT)
+1 -4
View File
@@ -47,10 +47,7 @@ STATIC_KEYS = (
"INFINITO_VARIANT",
"INFINITO_RUN_FLAGS",
"INFINITO_WORKER_ENABLED",
"INFINITO_VENV_BASE",
"INFINITO_VENV_NAME",
"PYTHONPATH",
"INVENTORY_VARS_FILE",
"INFINITO_INVENTORY_VARS_FILE",
"INFINITO_CACHE_REGISTRY_HOST_PATH",
"INFINITO_CACHE_REGISTRY_CA_HOST_PATH",
"INFINITO_CACHE_PACKAGE_HOST_PATH",
-15
View File
@@ -1,15 +0,0 @@
"""PIP: resolved pip invocation; usually ``$PYTHON -m pip``."""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from utils.env.builder import BuildContext, EnvBuilder
KEY = "PIP"
COMMENT = "Resolved pip invocation; usually `$PYTHON -m pip`."
def apply(eb: EnvBuilder, ctx: BuildContext) -> None:
eb.setdefault(KEY, f"{eb.get('PYTHON')} -m pip", comment=COMMENT)
-21
View File
@@ -1,21 +0,0 @@
"""PYTHON: absolute path to the venv's python (or ``python3`` fallback)."""
from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from utils.env.builder import BuildContext, EnvBuilder
KEY = "PYTHON"
COMMENT = "Absolute path to the venv's python (or `python3` fallback)."
def apply(eb: EnvBuilder, ctx: BuildContext) -> None:
default_python = f"{eb.get('VENV').rstrip('/')}/bin/python"
eb.setdefault(
KEY,
default_python if Path(default_python).exists() else "python3",
comment=COMMENT,
)
-24
View File
@@ -1,24 +0,0 @@
"""VENV: effective venv directory (VIRTUAL_ENV if set, else INFINITO_VENV_FALLBACK).
VENV defaults to the infinito-owned venv path, not to whatever the
caller happens to have sourced (e.g. ``pkgmgr``). Callers that genuinely
want to point at a different venv can still override via the ``VENV``
env-var, which setdefault respects.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from utils.env.builder import BuildContext, EnvBuilder
KEY = "VENV"
COMMENT = "Effective venv directory (VIRTUAL_ENV if set, else INFINITO_VENV_FALLBACK)."
def apply(eb: EnvBuilder, ctx: BuildContext) -> None:
venv_base = eb.get("INFINITO_VENV_BASE") or "/opt/venvs"
venv_name = eb.get("INFINITO_VENV_NAME") or "infinito"
venv_fallback = f"{venv_base.rstrip('/')}/{venv_name}"
eb.setdefault(KEY, venv_fallback, comment=COMMENT)