YREA-SLS/SLS_C/build_system/utils.py

260 lines
6.7 KiB
Python

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