"""Serialise an EnvBuilder to .env on disk. The output format is the docker-compose `.env`-subset that is also sourceable from bash (via `set -a; source .env; set +a`). Values that contain only "safe" characters are emitted bare; everything else is double-quoted with backslash-escaping of `\\` and `"`. Each key is preceded by its documentation comment when the builder carries one, mirroring the convention used in `default.env`. """ from __future__ import annotations import os import re from typing import TYPE_CHECKING if TYPE_CHECKING: from pathlib import Path from utils.env.builder import EnvBuilder _SAFE_VALUE_RE = re.compile(r"^[A-Za-z0-9_/.@:%+=-]+$") def quote_for_dotenv(value: str) -> str: if value != "" and _SAFE_VALUE_RE.match(value): return value if "\n" in value or "\r" in value: raise ValueError( "Multi-line env values cannot be written to .env " "(bash `source` and docker-compose `--env-file` both fail " "to round-trip them). Collapse to a single line at the " f"producer. Offending value: {value!r}" ) escaped = value.replace("\\", "\\\\").replace('"', '\\"') return f'"{escaped}"' def write_dotenv(eb: EnvBuilder, dest: Path) -> None: lines: list[str] = [ "# Generated by `make dotenv` (python -m cli.meta.env) -- do NOT edit.", "# Re-run after changes to default.env or after the runtime context", "# shifts (distro, CI vs local, fresh checkout).", ] for key in sorted(eb.values): lines.append("") comment = eb.comments.get(key, "").strip() if comment: lines.append(f"# {comment}") lines.append(f"{key}={quote_for_dotenv(eb.values[key])}") tmp = dest.with_name(f"{dest.name}.{os.getpid()}.tmp") try: tmp.write_text("\n".join(lines) + "\n", encoding="utf-8") tmp.replace(dest) except BaseException: tmp.unlink(missing_ok=True) raise