Coverage for src / gitversioned / utils / environment.py: 0%
51 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-14 20:57 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-14 20:57 +0000
1"""
2Build environment introspection utilities.
4This module provides tools for extracting hardware, operating system, and
5Continuous Integration (CI) metadata during the build process. It enables
6reproducible and traceable builds by automatically capturing runtime
7context into structured Pydantic models.
8"""
10from __future__ import annotations
12import os
13import platform
14import socket
15import uuid
16from datetime import datetime, timezone
17from pathlib import Path
19from pydantic import BaseModel, ConfigDict, Field
21from gitversioned.compat import psutil
23__all__ = ["BuildEnvironment", "get_ci_info", "get_ram_gb", "get_user"]
26def get_user() -> str:
27 """
28 Retrieve the current system or environment user.
30 Attempts to use standard library OS queries first, falling back to
31 common environment variables.
33 Example:
34 >>> get_user()
35 'markkurtz'
37 :return: The resolved username or "unknown" if undetermined.
38 """
39 try:
40 return os.getlogin()
41 except (OSError, AttributeError):
42 return os.environ.get("USER") or os.environ.get("USERNAME") or "unknown"
45def get_ci_info() -> tuple[bool, str | None]:
46 """
47 Determine if the current execution is within a recognized Continuous
48 Integration environment.
50 Queries standard environment variables to identify platforms like
51 GitHub Actions, GitLab CI, and others.
53 Example:
54 >>> is_ci, provider = get_ci_info()
55 >>> print(f"CI: {is_ci}, Provider: {provider}")
56 CI: True, Provider: GitHub Actions
58 :return: Tuple indicating CI presence and provider name if found.
59 """
60 providers = {
61 "GITHUB_ACTIONS": ("true", "GitHub Actions"),
62 "GITLAB_CI": (None, "GitLab CI"),
63 "CIRCLECI": ("true", "CircleCI"),
64 "TRAVIS": ("true", "Travis CI"),
65 "JENKINS_URL": (None, "Jenkins"),
66 "BITBUCKET_COMMIT": (None, "Bitbucket Pipelines"),
67 }
68 for env_var, (expected, name) in providers.items():
69 val = os.environ.get(env_var)
70 if val and (expected is None or val == expected):
71 return True, name
73 if os.environ.get("CI") in ("true", "1", "True"):
74 return True, "Unknown CI"
75 return False, None
78def get_ram_gb() -> float:
79 """
80 Calculate the total available system memory in gigabytes.
82 Relies on `psutil` if available in the environment.
84 Example:
85 >>> get_ram_gb()
86 16.0
88 :return: The total RAM in GB, or 0.0 if `psutil` is unavailable.
89 """
90 if psutil:
91 return round(psutil.virtual_memory().total / (1024**3), 2)
92 return 0.0
95class BuildEnvironment(BaseModel):
96 """
97 Structured metadata representing the current system and build execution context.
99 Captures environmental data such as OS details, hardware specs, and CI presence.
100 Utilized to record the provenance of a build artifact for auditing and debugging.
102 Example:
103 >>> env = BuildEnvironment()
104 >>> print(env.os_system)
105 'Darwin'
106 """
108 model_config = ConfigDict(frozen=True)
110 # --- System & OS ---
111 hostname: str = Field(
112 default_factory=socket.gethostname,
113 description="The network hostname of the build machine.",
114 )
115 user: str = Field(
116 default_factory=get_user,
117 description="The system username executing the build process.",
118 )
119 os_system: str = Field(
120 default_factory=platform.system,
121 description="The operating system name (e.g., 'Linux', 'Darwin', 'Windows').",
122 )
123 os_release: str = Field(
124 default_factory=platform.release,
125 description="The operating system release version.",
126 )
127 os_version: str = Field(
128 default_factory=platform.version,
129 description="The operating system build or release date string.",
130 )
132 # --- Hardware ---
133 cpu_arch: str = Field(
134 default_factory=platform.machine,
135 description="Hardware architecture of the build machine (e.g., 'x86_64').",
136 )
137 cpu_cores: int = Field(
138 default_factory=lambda: os.cpu_count() or 0,
139 description="The number of logical CPU cores available.",
140 )
141 total_ram_gb: float = Field(
142 default_factory=get_ram_gb,
143 description="The total available system RAM in gigabytes.",
144 )
146 # --- Runtime ---
147 python_version: str = Field(
148 default_factory=platform.python_version,
149 description="The version of the Python runtime executing the build.",
150 )
151 python_implementation: str = Field(
152 default_factory=platform.python_implementation,
153 description="The specific Python implementation (e.g., 'CPython', 'PyPy').",
154 )
155 python_compiler: str = Field(
156 default_factory=platform.python_compiler,
157 description="The compiler string used to build the Python runtime.",
158 )
159 timestamp: datetime = Field(
160 default_factory=lambda: datetime.now(timezone.utc),
161 description="UTC timestamp when this context was captured.",
162 )
164 # --- CI Context ---
165 is_ci: bool = Field(
166 default_factory=lambda: get_ci_info()[0],
167 description="True if executing within a recognized CI environment.",
168 )
169 ci_provider: str | None = Field(
170 default_factory=lambda: get_ci_info()[1],
171 description="The name of the detected CI provider, or None if undetermined.",
172 )
174 # --- Path Context ---
175 project_root: Path = Field(
176 default_factory=Path.cwd,
177 description="The root directory of the project where the build was initiated.",
178 )
180 build_id: str = Field(
181 default_factory=lambda: str(uuid.uuid4()),
182 description="A unique identifier generated for this specific build execution.",
183 )
185 def __str__(self) -> str:
186 """Return a concise string representation."""
188 ci_str = f" [CI: {self.ci_provider}]" if self.is_ci else " [Local]"
189 return (
190 f"BuildEnvironment({self.os_system} {self.os_release} {self.cpu_arch}, "
191 f"Python {self.python_version}, project={self.project_root.name}, "
192 f"id={self.build_id}){ci_str}"
193 )
195 def __repr__(self) -> str:
196 """Return a detailed string representation."""
197 return (
198 f"BuildEnvironment("
199 f"hostname={self.hostname!r}, user={self.user!r}, "
200 f"os_system={self.os_system!r}, os_release={self.os_release!r}, "
201 f"os_version={self.os_version!r}, "
202 f"cpu_arch={self.cpu_arch!r}, cpu_cores={self.cpu_cores!r}, "
203 f"total_ram_gb={self.total_ram_gb!r}, "
204 f"python_version={self.python_version!r}, "
205 f"python_implementation={self.python_implementation!r}, "
206 f"python_compiler={self.python_compiler!r}, timestamp={self.timestamp!r}, "
207 f"is_ci={self.is_ci!r}, ci_provider={self.ci_provider!r}, "
208 f"project_root={self.project_root!r}, build_id={self.build_id!r}"
209 f")"
210 )