#!/usr/bin/env python3 """ Script to update the version in pyproject.toml and Makefile from the .env file. This script reads the VERSION variable from .env and updates the version field in pyproject.toml and Makefile to keep them synchronized. """ import argparse import re import subprocess import sys from pathlib import Path def read_version_from_env(env_path: Path) -> str | None: """ Read the VERSION variable from the .env file. Args: env_path: Path to the .env file Returns: The version string or None if not found """ try: with open(env_path, encoding="utf-8") as f: content = f.read() # Look for VERSION="x.y.z" pattern match = re.search(r'VERSION\s*=\s*["\']([^"\']+)["\']', content) if match: return match.group(1) else: print("ERROR: VERSION not found in .env file") return None except FileNotFoundError: print(f"ERROR: .env file not found at {env_path}") return None except Exception as e: print(f"ERROR: Failed to read .env file: {e}") return None def update_pyproject_version(pyproject_path: Path, new_version: str) -> bool: """ Update the version in pyproject.toml. Args: pyproject_path: Path to the pyproject.toml file new_version: The new version string Returns: True if successful, False otherwise """ try: with open(pyproject_path, encoding="utf-8") as f: content = f.read() # Split content into lines for more precise matching lines = content.split("\n") in_project_section = False version_line_index = None current_version = None # Find the version line specifically in the [project] section for i, line in enumerate(lines): line_stripped = line.strip() # Check if we're entering the [project] section if line_stripped == "[project]": in_project_section = True continue # Check if we're leaving the [project] section (entering a new section) if ( in_project_section and line_stripped.startswith("[") and line_stripped != "[project]" ): in_project_section = False continue # Look for version = "x.y.z" only within [project] section if in_project_section and line_stripped.startswith("version"): version_pattern = r'^version\s*=\s*["\']([^"\']+)["\']' version_match = re.match(version_pattern, line_stripped) if version_match: current_version = version_match.group(1) version_line_index = i break if current_version is None or version_line_index is None: print( "ERROR: version field not found in [project] section of pyproject.toml" ) return False if current_version == new_version: print(f"pyproject.toml version is already up to date: {current_version}") return True # Replace only the specific version line in the [project] section old_line = lines[version_line_index] new_line = re.sub( r'^(\s*version\s*=\s*["\'])([^"\']+)(["\'])(.*)$', f"\\g<1>{new_version}\\g<3>\\g<4>", old_line, ) lines[version_line_index] = new_line # Reconstruct the content new_content = "\n".join(lines) # Write back to file with open(pyproject_path, "w", encoding="utf-8") as f: f.write(new_content) print(f"Updated pyproject.toml version from {current_version} to {new_version}") return True except FileNotFoundError: print(f"ERROR: pyproject.toml file not found at {pyproject_path}") return False except Exception as e: print(f"ERROR: Failed to update pyproject.toml: {e}") return False def update_makefile_version(makefile_path: Path, new_version: str) -> bool: """ Update the version in Makefile. Args: makefile_path: Path to the Makefile new_version: The new version string Returns: True if successful, False otherwise """ try: with open(makefile_path, encoding="utf-8") as f: content = f.read() # Split content into lines for processing lines = content.split("\n") version_line_index = None current_version = None # Find the VERSION= line for i, line in enumerate(lines): # Look for VERSION=x.y.z pattern (at start of line or after whitespace) version_pattern = r"^(\s*)VERSION\s*=\s*(.+)$" version_match = re.match(version_pattern, line) if version_match: current_version = version_match.group(2).strip() version_line_index = i break if current_version is None or version_line_index is None: print("ERROR: VERSION variable not found in Makefile") return False if current_version == new_version: print(f"Makefile version is already up to date: {current_version}") return True # Replace the VERSION line old_line = lines[version_line_index] new_line = re.sub( r"^(\s*VERSION\s*=\s*)(.+)$", f"\\g<1>{new_version}", old_line, ) lines[version_line_index] = new_line # Reconstruct the content new_content = "\n".join(lines) # Write back to file with open(makefile_path, "w", encoding="utf-8") as f: f.write(new_content) print(f"Updated Makefile version from {current_version} to {new_version}") return True except FileNotFoundError: print(f"ERROR: Makefile not found at {makefile_path}") return False except Exception as e: print(f"ERROR: Failed to update Makefile: {e}") return False def update_uv_lock(project_root: Path) -> bool: """ Update uv.lock file to reflect changes in pyproject.toml. Args: project_root: Path to the project root directory Returns: True if successful, False otherwise """ try: print("Updating uv.lock file...") # Run uv lock to update the lock file result = subprocess.run( ["uv", "lock"], cwd=project_root, capture_output=True, text=True, timeout=60, # 60 second timeout ) if result.returncode == 0: print("Successfully updated uv.lock") return True else: print(f"ERROR: Failed to update uv.lock: {result.stderr}") return False except subprocess.TimeoutExpired: print("ERROR: uv lock command timed out after 60 seconds") return False except FileNotFoundError: print( "ERROR: 'uv' command not found. Please ensure uv is installed and in PATH" ) return False except Exception as e: print(f"ERROR: Failed to run uv lock: {e}") return False def main() -> int: """ Main function to update version from .env to pyproject.toml and Makefile. Returns: Exit code: 0 for success, 1 for failure """ parser = argparse.ArgumentParser( description="Update version in pyproject.toml and Makefile from .env file" ) parser.add_argument( "--skip-uv-lock", action="store_true", help="Skip updating uv.lock file after version update", ) args = parser.parse_args() # Get the project root directory (assuming script is in scripts/ folder) script_dir = Path(__file__).parent project_root = script_dir.parent env_path = project_root / ".env" pyproject_path = project_root / "pyproject.toml" makefile_path = project_root / "Makefile" print(f"Reading version from: {env_path}") print(f"Updating version in: {pyproject_path}") print(f"Updating version in: {makefile_path}") # Read version from .env version = read_version_from_env(env_path) if not version: return 1 print(f"Found version in .env: {version}") # Track if any updates were made _updates_made = False # Update pyproject.toml pyproject_updated = update_pyproject_version(pyproject_path, version) if not pyproject_updated: return 1 # Update Makefile makefile_updated = update_makefile_version(makefile_path, version) if not makefile_updated: return 1 print("Version update completed successfully!") # Update uv.lock unless explicitly skipped if args.skip_uv_lock: print("Skipping uv.lock update (--skip-uv-lock specified)") return 0 # Update uv.lock to reflect the changes if update_uv_lock(project_root): print("All updates completed successfully!") return 0 else: print("⚠️ Version updated but uv.lock update failed") print(" Please run 'uv lock' manually to update the lock file") return 1 if __name__ == "__main__": sys.exit(main())