1# -*- coding: utf-8 -*- 2""" 3Utility functions for RT-Thread build system. 4 5This module provides common utility functions used throughout the build system. 6""" 7 8import os 9import sys 10import platform 11from typing import List, Tuple, Optional 12 13 14class PathService: 15 """Service for path manipulation and normalization.""" 16 17 def __init__(self, base_path: str = None): 18 self.base_path = base_path or os.getcwd() 19 20 def normalize_path(self, path: str) -> str: 21 """ 22 Normalize path for cross-platform compatibility. 23 24 Args: 25 path: Path to normalize 26 27 Returns: 28 Normalized path 29 """ 30 # Convert to absolute path if relative 31 if not os.path.isabs(path): 32 path = os.path.abspath(os.path.join(self.base_path, path)) 33 34 # Normalize separators 35 path = os.path.normpath(path) 36 37 # Convert to forward slashes for consistency 38 if platform.system() == 'Windows': 39 path = path.replace('\\', '/') 40 41 return path 42 43 def make_relative(self, path: str, base: str = None) -> str: 44 """ 45 Make path relative to base. 46 47 Args: 48 path: Path to make relative 49 base: Base path (defaults to self.base_path) 50 51 Returns: 52 Relative path 53 """ 54 if base is None: 55 base = self.base_path 56 57 path = self.normalize_path(path) 58 base = self.normalize_path(base) 59 60 try: 61 rel_path = os.path.relpath(path, base) 62 # Convert to forward slashes 63 if platform.system() == 'Windows': 64 rel_path = rel_path.replace('\\', '/') 65 return rel_path 66 except ValueError: 67 # Different drives on Windows 68 return path 69 70 def split_path(self, path: str) -> List[str]: 71 """ 72 Split path into components. 73 74 Args: 75 path: Path to split 76 77 Returns: 78 List of path components 79 """ 80 path = self.normalize_path(path) 81 parts = [] 82 83 while True: 84 head, tail = os.path.split(path) 85 if tail: 86 parts.insert(0, tail) 87 if head == path: # Reached root 88 if head: 89 parts.insert(0, head) 90 break 91 path = head 92 93 return parts 94 95 def common_prefix(self, paths: List[str]) -> str: 96 """ 97 Find common prefix of multiple paths. 98 99 Args: 100 paths: List of paths 101 102 Returns: 103 Common prefix path 104 """ 105 if not paths: 106 return "" 107 108 # Normalize all paths 109 normalized = [self.normalize_path(p) for p in paths] 110 111 # Find common prefix 112 prefix = os.path.commonpath(normalized) 113 114 return self.normalize_path(prefix) 115 116 117class PlatformInfo: 118 """Platform and system information.""" 119 120 @staticmethod 121 def get_platform() -> str: 122 """Get platform name (Windows, Linux, Darwin).""" 123 return platform.system() 124 125 @staticmethod 126 def get_architecture() -> str: 127 """Get system architecture.""" 128 return platform.machine() 129 130 @staticmethod 131 def is_windows() -> bool: 132 """Check if running on Windows.""" 133 return platform.system() == 'Windows' 134 135 @staticmethod 136 def is_linux() -> bool: 137 """Check if running on Linux.""" 138 return platform.system() == 'Linux' 139 140 @staticmethod 141 def is_macos() -> bool: 142 """Check if running on macOS.""" 143 return platform.system() == 'Darwin' 144 145 @staticmethod 146 def get_python_version() -> Tuple[int, int, int]: 147 """Get Python version tuple.""" 148 return sys.version_info[:3] 149 150 @staticmethod 151 def check_python_version(min_version: Tuple[int, int]) -> bool: 152 """ 153 Check if Python version meets minimum requirement. 154 155 Args: 156 min_version: Minimum version tuple (major, minor) 157 158 Returns: 159 True if version is sufficient 160 """ 161 current = sys.version_info[:2] 162 return current >= min_version 163 164 165class FileUtils: 166 """File operation utilities.""" 167 168 @staticmethod 169 def read_file(filepath: str, encoding: str = 'utf-8') -> str: 170 """ 171 Read file content. 172 173 Args: 174 filepath: File path 175 encoding: File encoding 176 177 Returns: 178 File content 179 """ 180 with open(filepath, 'r', encoding=encoding) as f: 181 return f.read() 182 183 @staticmethod 184 def write_file(filepath: str, content: str, encoding: str = 'utf-8') -> None: 185 """ 186 Write content to file. 187 188 Args: 189 filepath: File path 190 content: Content to write 191 encoding: File encoding 192 """ 193 # Ensure directory exists 194 directory = os.path.dirname(filepath) 195 if directory: 196 os.makedirs(directory, exist_ok=True) 197 198 with open(filepath, 'w', encoding=encoding) as f: 199 f.write(content) 200 201 @staticmethod 202 def copy_file(src: str, dst: str) -> None: 203 """ 204 Copy file from src to dst. 205 206 Args: 207 src: Source file path 208 dst: Destination file path 209 """ 210 import shutil 211 212 # Ensure destination directory exists 213 dst_dir = os.path.dirname(dst) 214 if dst_dir: 215 os.makedirs(dst_dir, exist_ok=True) 216 217 shutil.copy2(src, dst) 218 219 @staticmethod 220 def find_files(directory: str, pattern: str, recursive: bool = True) -> List[str]: 221 """ 222 Find files matching pattern. 223 224 Args: 225 directory: Directory to search 226 pattern: File pattern (supports wildcards) 227 recursive: Search recursively 228 229 Returns: 230 List of matching file paths 231 """ 232 import fnmatch 233 234 matches = [] 235 236 if recursive: 237 for root, dirnames, filenames in os.walk(directory): 238 for filename in filenames: 239 if fnmatch.fnmatch(filename, pattern): 240 matches.append(os.path.join(root, filename)) 241 else: 242 try: 243 filenames = os.listdir(directory) 244 for filename in filenames: 245 if fnmatch.fnmatch(filename, pattern): 246 filepath = os.path.join(directory, filename) 247 if os.path.isfile(filepath): 248 matches.append(filepath) 249 except OSError: 250 pass 251 252 return sorted(matches) 253 254 255class VersionUtils: 256 """Version comparison utilities.""" 257 258 @staticmethod 259 def parse_version(version_str: str) -> Tuple[int, ...]: 260 """ 261 Parse version string to tuple. 262 263 Args: 264 version_str: Version string (e.g., "1.2.3") 265 266 Returns: 267 Version tuple 268 """ 269 try: 270 parts = version_str.split('.') 271 return tuple(int(p) for p in parts if p.isdigit()) 272 except (ValueError, AttributeError): 273 return (0,) 274 275 @staticmethod 276 def compare_versions(v1: str, v2: str) -> int: 277 """ 278 Compare two version strings. 279 280 Args: 281 v1: First version 282 v2: Second version 283 284 Returns: 285 -1 if v1 < v2, 0 if equal, 1 if v1 > v2 286 """ 287 t1 = VersionUtils.parse_version(v1) 288 t2 = VersionUtils.parse_version(v2) 289 290 # Pad shorter version with zeros 291 if len(t1) < len(t2): 292 t1 = t1 + (0,) * (len(t2) - len(t1)) 293 elif len(t2) < len(t1): 294 t2 = t2 + (0,) * (len(t1) - len(t2)) 295 296 if t1 < t2: 297 return -1 298 elif t1 > t2: 299 return 1 300 else: 301 return 0 302 303 @staticmethod 304 def version_satisfies(version: str, requirement: str) -> bool: 305 """ 306 Check if version satisfies requirement. 307 308 Args: 309 version: Version string 310 requirement: Requirement string (e.g., ">=1.2.0") 311 312 Returns: 313 True if satisfied 314 """ 315 import re 316 317 # Parse requirement 318 match = re.match(r'([<>=]+)\s*(.+)', requirement) 319 if not match: 320 # Exact match required 321 return version == requirement 322 323 op, req_version = match.groups() 324 cmp = VersionUtils.compare_versions(version, req_version) 325 326 if op == '>=': 327 return cmp >= 0 328 elif op == '<=': 329 return cmp <= 0 330 elif op == '>': 331 return cmp > 0 332 elif op == '<': 333 return cmp < 0 334 elif op == '==': 335 return cmp == 0 336 elif op == '!=': 337 return cmp != 0 338 else: 339 return False