""" Utility functions for the build system. This module provides common utilities used across the build system: - File and directory operations - Git integration - Dependency tracking - Process execution helpers """ import os import subprocess import shutil from pathlib import Path from typing import Optional # TODO: Is this needed? def mkdir(path: Path): """ Create a directory if it doesn't exist. Args: path: Directory path to create """ path.mkdir(parents=True, exist_ok=True) # TODO: Is this needed? def rm_tree(path: Path): """ Remove a directory tree if it exists. Args: path: Directory path to remove """ if path.exists(): shutil.rmtree(path, ignore_errors=True) # TODO: Is this needed? def run_command(cmd: list, cwd: Optional[Path] = None, capture_output: bool = False): """ Run a command and optionally capture its output. Args: cmd: Command and arguments as list cwd: Working directory (default: current directory) capture_output: If True, return output; if False, print to console Returns: Command output as string if capture_output=True, else None Raises: subprocess.CalledProcessError: If command fails """ if not capture_output: print(">>", " ".join(str(c) for c in cmd)) subprocess.check_call(cmd, cwd=cwd) return None else: result = subprocess.check_output( cmd, cwd=cwd, stderr=subprocess.DEVNULL, text=True ) return result.strip() def git_commit_hash() -> str: """ Get the current git commit hash and date. Returns: String like "a1b2c3d 2024-12-18 10:30:45 -0700" or "unknown" if not a git repo """ try: # Get short hash with dirty flag hash_result = subprocess.check_output( ["git", "describe", "--always", "--dirty", "--abbrev=7"], stderr=subprocess.DEVNULL, text=True ).strip() # Get commit date date_result = subprocess.check_output( ["git", "show", "-s", "--format=%ci"], stderr=subprocess.DEVNULL, text=True ).strip() return f"{hash_result} {date_result}" except (subprocess.CalledProcessError, FileNotFoundError): return "unknown" def is_up_to_date(source: Path, output: Path, deps: Optional[Path] = None) -> bool: """ Check if an output file is up to date relative to its source. Args: source: Source file path output: Output file path deps: Optional dependency file path Returns: True if output is newer than source (and deps), False otherwise """ # Special case: always rebuild files with "meta" in name if "meta" in source.stem or "meta" in output.stem: return False # If output doesn't exist, not up to date if not output.exists(): return False # Check if dependency file is newer than output if deps and deps.exists(): if deps.stat().st_mtime > output.stat().st_mtime: return False # Check if source is newer than output return source.stat().st_mtime < output.stat().st_mtime def find_sources(directory: Path, pattern: str = "*.c", exclude: Optional[list] = None) -> list: """ Find source files in a directory with optional exclusions. Args: directory: Directory to search pattern: Glob pattern for files (default: "*.c") exclude: List of filenames or patterns to exclude Returns: List of Path objects for matching files """ exclude = exclude or [] sources = list(directory.glob(pattern)) # Filter out excluded files filtered = [] for src in sources: # Check exact filename matches if src.name in exclude: continue # Check pattern matches in stem if any(excl.lower() in src.stem.lower() for excl in exclude): continue filtered.append(src) return filtered def detect_python() -> str: """ Detect the appropriate Python executable. Returns: 'python3' on Unix-like systems, 'python' on Windows """ if os.name == "nt": return "python" return "python3" def file_hash(path: Path) -> Optional[str]: """ Calculate SHA256 hash of a file. Args: path: File path Returns: Hex string of file hash, or None if file doesn't exist """ import hashlib if not path.exists(): return None sha256 = hashlib.sha256() with open(path, 'rb') as f: for chunk in iter(lambda: f.read(4096), b''): sha256.update(chunk) return sha256.hexdigest() def needs_rebuild(source: Path, output: Path, deps_file: Optional[Path] = None) -> bool: """ Advanced dependency checking including header dependencies. Args: source: Source file output: Output file deps_file: Dependency file (.d) generated by compiler Returns: True if rebuild is needed """ # Basic up-to-date check if not is_up_to_date(source, output, deps_file): return True # If we have a dependency file, check all dependencies if deps_file and deps_file.exists(): try: with open(deps_file, 'r') as f: content = f.read() # Parse Make-style dependencies # Format: target: dep1 dep2 dep3 if ':' in content: _, deps_str = content.split(':', 1) deps = deps_str.strip().split() output_mtime = output.stat().st_mtime # Check if any dependency is newer than output for dep in deps: dep_path = Path(dep.strip('\\').strip()) if dep_path.exists(): if dep_path.stat().st_mtime > output_mtime: return True except Exception: # If we can't parse deps, rebuild to be safe return True return False def copy_file(src: Path, dst: Path): """ Copy a file, creating destination directory if needed. Args: src: Source file dst: Destination file """ mkdir(dst.parent) shutil.copy2(src, dst) def which(executable: str) -> Optional[Path]: """ Find an executable in PATH. Args: executable: Name of executable Returns: Path to executable if found, None otherwise """ result = shutil.which(executable) return Path(result) if result else None