mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
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:
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"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
@@ -17,4 +17,3 @@ INFINITO_DOMAIN_RE=infinito\.example
|
||||
|
||||
# IPv4 of the infinito application container inside INFINITO_SUBNET.
|
||||
INFINITO_IP4=172.30.0.10
|
||||
|
||||
Vendored
+3
-13
@@ -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=.
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:-}"
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+26
-53
@@ -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
|
||||
Executable
+33
@@ -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
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
@@ -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).",
|
||||
)
|
||||
|
||||
Vendored
+1
-2
@@ -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.
|
||||
|
||||
Vendored
-10
@@ -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
@@ -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
@@ -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)
|
||||
Vendored
+1
-4
@@ -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",
|
||||
|
||||
Vendored
-15
@@ -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)
|
||||
Vendored
-21
@@ -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,
|
||||
)
|
||||
Vendored
-24
@@ -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)
|
||||
Reference in new issue
Block a user