From 2efdd1d2e206c517851db5a6cbd0091c2b73474a Mon Sep 17 00:00:00 2001 From: Kyler Date: Fri, 19 Dec 2025 14:00:38 -0700 Subject: [PATCH] Created utils --- SLS_C/build_system/utils.py | 394 ++++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) diff --git a/SLS_C/build_system/utils.py b/SLS_C/build_system/utils.py index e69de29..3623f5d 100644 --- a/SLS_C/build_system/utils.py +++ b/SLS_C/build_system/utils.py @@ -0,0 +1,394 @@ +#!/usr/bin/env python3 +""" +Utility functions for the build system. +Provides common operations like directory creation, process execution, +file dependency checking, and more. +""" + +import subprocess +import shutil +from pathlib import Path +from typing import List, Union, Optional +import sys + + +# ----------------------------------------------------------------------------- +# Directory operations +# ----------------------------------------------------------------------------- + +def mkdir(path: Path) -> None: + """ + Create a directory and all parent directories if they don't exist. + + Args: + path: Directory path to create + """ + path.mkdir(parents=True, exist_ok=True) + + +def rmdir(path: Path, ignore_errors: bool = True) -> None: + """ + Recursively remove a directory and all its contents. + + Args: + path: Directory path to remove + ignore_errors: If True, ignore errors during removal + """ + if path.exists(): + shutil.rmtree(path, ignore_errors=ignore_errors) + + +# ----------------------------------------------------------------------------- +# Process execution +# ----------------------------------------------------------------------------- + +def run(cmd: List[Union[str, Path]], + cwd: Optional[Path] = None, + check: bool = True, + capture_output: bool = False, + verbose: bool = True) -> subprocess.CompletedProcess: + """ + Execute a command and optionally capture its output. + + Args: + cmd: Command and arguments to execute + cwd: Working directory for the command + check: If True, raise exception on non-zero exit code + capture_output: If True, capture stdout and stderr + verbose: If True, print the command before executing + + Returns: + CompletedProcess instance with return code and output + + Raises: + subprocess.CalledProcessError: If check=True and command fails + """ + # Convert all Path objects to strings + cmd_str = [str(c) for c in cmd] + + if verbose: + print(">>", " ".join(cmd_str)) + + return subprocess.run( + cmd_str, + cwd=cwd, + check=check, + capture_output=capture_output, + text=True if capture_output else None + ) + + +def run_quiet(cmd: List[Union[str, Path]], + cwd: Optional[Path] = None, + check: bool = False) -> subprocess.CompletedProcess: + """ + Execute a command silently (no output, no verbose). + + Args: + cmd: Command and arguments to execute + cwd: Working directory for the command + check: If True, raise exception on non-zero exit code + + Returns: + CompletedProcess instance + """ + cmd_str = [str(c) for c in cmd] + return subprocess.run( + cmd_str, + cwd=cwd, + check=check, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL + ) + + +# ----------------------------------------------------------------------------- +# File dependency checking +# ----------------------------------------------------------------------------- + +def is_up_to_date(source: Path, output: Path, dependency: Optional[Path] = None) -> bool: + """ + Check if an output file is up to date relative to its source and dependencies. + + Args: + source: Source file path + output: Output file path + dependency: Optional dependency file (e.g., .d file) + + Returns: + True if output is up to date, False if it needs rebuilding + """ + # Files with 'meta' in their name are never up to date (force rebuild) + if "meta" in source.stem or "meta" in output.stem: + return False + + if dependency and "meta" in dependency.stem: + return False + + # Output doesn't exist, needs building + if not output.exists(): + return False + + # Check dependency file if provided + if dependency: + if dependency.exists() and dependency.stat().st_mtime > output.stat().st_mtime: + return False + + # Check source file modification time + return source.stat().st_mtime < output.stat().st_mtime + + +def needs_rebuild(sources: List[Path], output: Path) -> bool: + """ + Check if any source file is newer than the output file. + + Args: + sources: List of source file paths + output: Output file path + + Returns: + True if rebuild is needed, False otherwise + """ + if not output.exists(): + return True + + output_time = output.stat().st_mtime + return any(src.stat().st_mtime > output_time for src in sources if src.exists()) + + +# ----------------------------------------------------------------------------- +# Tool detection +# ----------------------------------------------------------------------------- + +def which(tool: str) -> Optional[Path]: + """ + Find a tool in the system PATH. + + Args: + tool: Tool name to find + + Returns: + Path to the tool if found, None otherwise + """ + result = shutil.which(tool) + return Path(result) if result else None + + +def check_tool(tool: str, error_message: Optional[str] = None) -> bool: + """ + Check if a tool is available in the system PATH. + + Args: + tool: Tool name to check + error_message: Optional custom error message to print if not found + + Returns: + True if tool is found, False otherwise + """ + if which(tool): + return True + + if error_message: + print(f"ERROR: {error_message}") + else: + print(f"ERROR: Required tool '{tool}' not found in PATH") + + return False + + +def check_tools(tools: List[str], error_messages: Optional[List[str]] = None) -> bool: + """ + Check if multiple tools are available. + + Args: + tools: List of tool names to check + error_messages: Optional list of custom error messages (same length as tools) + + Returns: + True if all tools are found, False otherwise + """ + all_found = True + + for i, tool in enumerate(tools): + msg = error_messages[i] if error_messages and i < len(error_messages) else None + if not check_tool(tool, msg): + all_found = False + + return all_found + + +# ----------------------------------------------------------------------------- +# File operations +# ----------------------------------------------------------------------------- + +def copy_file(src: Path, dst: Path) -> None: + """ + Copy a file from source to destination. + + Args: + src: Source file path + dst: Destination file path + """ + mkdir(dst.parent) + shutil.copy2(src, dst) + + +def get_files_recursive(directory: Path, pattern: str = "*") -> List[Path]: + """ + Get all files matching a pattern recursively in a directory. + + Args: + directory: Directory to search + pattern: Glob pattern to match (default: "*") + + Returns: + List of matching file paths + """ + if not directory.exists(): + return [] + return list(directory.rglob(pattern)) + + +def get_files(directory: Path, pattern: str = "*") -> List[Path]: + """ + Get all files matching a pattern in a directory (non-recursive). + + Args: + directory: Directory to search + pattern: Glob pattern to match (default: "*") + + Returns: + List of matching file paths + """ + if not directory.exists(): + return [] + return list(directory.glob(pattern)) + + +# ----------------------------------------------------------------------------- +# String formatting +# ----------------------------------------------------------------------------- + +def format_list(items: List[str], indent: int = 2) -> str: + """ + Format a list of items as an indented bulleted list. + + Args: + items: List of strings to format + indent: Number of spaces to indent + + Returns: + Formatted string with bullet points + """ + indent_str = " " * indent + return "\n".join(f"{indent_str}- {item}" for item in items) + + +def print_header(text: str, char: str = "=") -> None: + """ + Print a header with surrounding decoration. + + Args: + text: Header text + char: Character to use for decoration + """ + print() + print(char * len(text)) + print(text) + print(char * len(text)) + print() + + +def print_success(text: str) -> None: + """Print a success message.""" + print(f"✓ {text}") + + +def print_error(text: str) -> None: + """Print an error message to stderr.""" + print(f"✗ {text}", file=sys.stderr) + + +def print_warning(text: str) -> None: + """Print a warning message.""" + print(f"⚠ {text}") + + +# ----------------------------------------------------------------------------- +# Path operations +# ----------------------------------------------------------------------------- + +def get_relative_to_project(path: Path, project_root: Optional[Path] = None) -> Path: + """ + Get a path relative to the project root. + + Args: + path: Absolute or relative path + project_root: Project root directory (default: current directory) + + Returns: + Path relative to project root + """ + if project_root is None: + project_root = Path.cwd() + + try: + return path.relative_to(project_root) + except ValueError: + # Path is not relative to project root, return as-is + return path + + +def ensure_absolute(path: Path, base: Optional[Path] = None) -> Path: + """ + Ensure a path is absolute. + + Args: + path: Path to make absolute + base: Base directory for relative paths (default: current directory) + + Returns: + Absolute path + """ + if path.is_absolute(): + return path + + if base is None: + base = Path.cwd() + + return (base / path).resolve() + + +# ----------------------------------------------------------------------------- +# Version comparison +# ----------------------------------------------------------------------------- + +def compare_versions(v1: str, v2: str) -> int: + """ + Compare two version strings. + + Args: + v1: First version string (e.g., "1.2.3") + v2: Second version string (e.g., "1.2.4") + + Returns: + -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2 + """ + def normalize(v): + return [int(x) for x in v.split(".") if x.isdigit()] + + parts1 = normalize(v1) + parts2 = normalize(v2) + + # Pad shorter version with zeros + max_len = max(len(parts1), len(parts2)) + parts1.extend([0] * (max_len - len(parts1))) + parts2.extend([0] * (max_len - len(parts2))) + + for p1, p2 in zip(parts1, parts2): + if p1 < p2: + return -1 + elif p1 > p2: + return 1 + + return 0