Coverage for src / gitversioned / utils / environment.py: 94%

51 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-14 20:55 +0000

1""" 

2Build environment introspection utilities. 

3 

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""" 

9 

10from __future__ import annotations 

11 

12import os 

13import platform 

14import socket 

15import uuid 

16from datetime import datetime, timezone 

17from pathlib import Path 

18 

19from pydantic import BaseModel, ConfigDict, Field 

20 

21from gitversioned.compat import psutil 

22 

23__all__ = ["BuildEnvironment", "get_ci_info", "get_ram_gb", "get_user"] 

24 

25 

26def get_user() -> str: 

27 """ 

28 Retrieve the current system or environment user. 

29 

30 Attempts to use standard library OS queries first, falling back to 

31 common environment variables. 

32 

33 Example: 

34 >>> get_user() 

35 'markkurtz' 

36 

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" 

43 

44 

45def get_ci_info() -> tuple[bool, str | None]: 

46 """ 

47 Determine if the current execution is within a recognized Continuous 

48 Integration environment. 

49 

50 Queries standard environment variables to identify platforms like 

51 GitHub Actions, GitLab CI, and others. 

52 

53 Example: 

54 >>> is_ci, provider = get_ci_info() 

55 >>> print(f"CI: {is_ci}, Provider: {provider}") 

56 CI: True, Provider: GitHub Actions 

57 

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 

72 

73 if os.environ.get("CI") in ("true", "1", "True"): 

74 return True, "Unknown CI" 

75 return False, None 

76 

77 

78def get_ram_gb() -> float: 

79 """ 

80 Calculate the total available system memory in gigabytes. 

81 

82 Relies on `psutil` if available in the environment. 

83 

84 Example: 

85 >>> get_ram_gb() 

86 16.0 

87 

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 

93 

94 

95class BuildEnvironment(BaseModel): 

96 """ 

97 Structured metadata representing the current system and build execution context. 

98 

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. 

101 

102 Example: 

103 >>> env = BuildEnvironment() 

104 >>> print(env.os_system) 

105 'Darwin' 

106 """ 

107 

108 model_config = ConfigDict(frozen=True) 

109 

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 ) 

131 

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 ) 

145 

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 ) 

163 

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 ) 

173 

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 ) 

179 

180 build_id: str = Field( 

181 default_factory=lambda: str(uuid.uuid4()), 

182 description="A unique identifier generated for this specific build execution.", 

183 ) 

184 

185 def __str__(self) -> str: 

186 """Return a concise string representation.""" 

187 

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 ) 

194 

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 )