Ctrl K
ComunidadGuías…

versioning.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# ═══════════════════════════════════════════════════════════════════
#   ✦  T L A M A T I N I  ✦   —   "one who knows"
#
#   Created by  Angela López Mendoza   ·   @angelahack1
#   Developer · Architect · Creator of Tlamatini
#
#   Every line of this file was written by Angela López Mendoza.
# ═══════════════════════════════════════════════════════════════════
#   Tlamatini Author Banner — do not remove (releases scrub the name automatically)
"""Top-level versioning helper for Tlamatini's build scripts.

This module is the **build-time** entry point for the SemVer system.
Source-of-truth implementation lives in ``Tlamatini/agent/version.py``
(runtime resolution); this shim re-exports the public API for
``build.py`` / ``build_installer.py`` / ``build_uninstaller.py`` and
adds the build-time wrapper ``resolve_build_version()`` that handles
the CLI flag / env var / git fallbacks.

Precedence for the build version (highest to lowest):

  1. Explicit ``--version`` CLI flag.
  2. ``$env:TLAMATINI_VERSION`` supplied by the calling shell.
  3. ``git describe --tags --abbrev=0 --match 'v[0-9]*'``.
  4. ``0.0.0+unknown`` sentinel.

Complete release wrappers resolve once and pass ``--version`` to all three
build stages. A child process cannot export environment changes to its parent
or siblings. See ``VERSIONING.md`` at the repo root for the full contract.
"""
from __future__ import annotations

import os
import re
import sys
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional


# ── Wire ``agent.version`` onto sys.path ───────────────────────────────────

_REPO_ROOT = Path(__file__).resolve().parent
_DJANGO_ROOT = _REPO_ROOT / "Tlamatini"
if str(_DJANGO_ROOT) not in sys.path:
    sys.path.insert(0, str(_DJANGO_ROOT))

from agent.version import (  # noqa: E402  (sys.path mutated above)
    UNKNOWN_VERSION,
    derive_version_from_git,
    get_version,
    get_version_info,
    parse_semver,
    render_pyinstaller_version_file,
    semver_to_win32_tuple,
    write_version_module,
)

# ── Path constants the build scripts use ────────────────────────────────────

REPO_ROOT = _REPO_ROOT
DJANGO_ROOT = _DJANGO_ROOT
VERSION_MODULE_PATH = DJANGO_ROOT / "agent" / "_version.py"
PYINSTALLER_VERSION_FILE = REPO_ROOT / "Tlamatini.version.txt"

ENV_VAR_NAME = "TLAMATINI_VERSION"


# ── Build-time orchestrators ───────────────────────────────────────────────

def extract_cli_version(argv: list[str]) -> Optional[str]:
    """Pluck ``--version X.Y.Z`` (or ``--version=X.Y.Z``) out of *argv*.

    Mutates ``argv`` in place so the build script's own ``argparse`` (if
    any) doesn't see the flag.  Returns ``None`` if not present.
    """
    out: Optional[str] = None
    i = 1
    while i < len(argv):
        token = argv[i]
        if token == "--version" and i + 1 < len(argv):
            out = argv[i + 1].strip()
            del argv[i:i + 2]
            continue
        if token.startswith("--version="):
            out = token.split("=", 1)[1].strip()
            del argv[i]
            continue
        i += 1
    return out or None


def extract_cli_release_type(argv: list[str]) -> str:
    """Consume the explicit wrapper-selected type; no environment inference."""
    result = "unspecified"
    seen = False
    index = 1
    while index < len(argv):
        token = argv[index]
        if token == "--release-type" or token.startswith("--release-type="):
            if seen:
                raise ValueError("--release-type may only be specified once")
            seen = True
            if token == "--release-type":
                if index + 1 >= len(argv):
                    raise ValueError("--release-type requires private or public")
                value = argv[index + 1]
                del argv[index:index + 2]
            else:
                value = token.split("=", 1)[1]
                del argv[index]
            if value not in ("private", "public"):
                raise ValueError("--release-type requires private or public")
            result = value
        else:
            index += 1
    return result


def _sanitize_version(raw: str) -> str:
    """Drop a leading 'v'/'V'/' ' but otherwise pass the string through."""
    return raw.lstrip(" vV").strip()


def resolve_build_version(cli_arg: Optional[str] = None) -> str:
    """Return the build version using the documented precedence.

    Pass the result of ``extract_cli_version(sys.argv)`` as *cli_arg*.
    """
    # 1. Explicit CLI flag wins.
    if cli_arg:
        return _sanitize_version(cli_arg)
    # 2. Honour TLAMATINI_VERSION supplied by the calling environment.
    env_v = os.environ.get(ENV_VAR_NAME, "").strip()
    if env_v:
        return _sanitize_version(env_v)
    # 3. Derive from git.
    derived = derive_version_from_git()
    if derived:
        return derived
    # 4. Sentinel.
    return UNKNOWN_VERSION


def emit_build_artifacts(
    version: str,
    *,
    product_name: str = "Tlamatini",
    original_filename: str = "Tlamatini.exe",
    release_type: str = "unspecified",
) -> Path:
    """Write ``_version.py`` and the Win32 VERSIONINFO file to disk.

    Also sets ``TLAMATINI_VERSION`` for this process and its descendants.
    The calling shell and sibling build stages do not inherit that change;
    complete release wrappers pass the pinned version explicitly.

    Returns the absolute path to the PyInstaller ``--version-file`` so the
    caller can append ``--version-file=<path>`` to its argv.
    """
    # 1. Derive metadata.
    public = version.split("+", 1)[0]
    commit_match = re.search(r"\+(?:g)?([0-9a-f]{7,})", version)
    commit = commit_match.group(1) if commit_match else "unknown"
    iso_date = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")

    # 2. Runtime version module bundled into the frozen app.
    write_version_module(
        VERSION_MODULE_PATH,
        version=public,
        build=version,
        commit=commit,
        date=iso_date,
        release_type=release_type,
    )

    # 3. Win32 VERSIONINFO file for PyInstaller --version-file.
    PYINSTALLER_VERSION_FILE.write_text(
        render_pyinstaller_version_file(
            version,
            product_name=product_name,
            original_filename=original_filename,
        ),
        encoding="utf-8",
    )

    # 4. Set for this process and any children it launches, not the parent shell.
    os.environ[ENV_VAR_NAME] = version

    return PYINSTALLER_VERSION_FILE


def render_versioninfo_for(
    version: str,
    target: Path,
    *,
    product_name: str,
    original_filename: str,
) -> Path:
    """Render a VERSIONINFO file at *target* (used by Installer/Uninstaller).

    Does NOT touch ``_version.py`` — that file is owned exclusively by
    ``build.py`` because it ends up inside the main application bundle.
    """
    target = Path(target)
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(
        render_pyinstaller_version_file(
            version,
            product_name=product_name,
            original_filename=original_filename,
        ),
        encoding="utf-8",
    )
    return target


__all__ = [
    "UNKNOWN_VERSION",
    "ENV_VAR_NAME",
    "REPO_ROOT",
    "DJANGO_ROOT",
    "VERSION_MODULE_PATH",
    "PYINSTALLER_VERSION_FILE",
    "derive_version_from_git",
    "get_version",
    "get_version_info",
    "parse_semver",
    "semver_to_win32_tuple",
    "extract_cli_version",
    "resolve_build_version",
    "emit_build_artifacts",
    "render_versioninfo_for",
]