Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2025-12-14 09:20:24

0001 #!/usr/bin/env python3
0002 # /// script
0003 # requires-python = ">=3.11"
0004 # dependencies = [
0005 #     "typer",
0006 #     "rich",
0007 # ]
0008 # ///
0009 """
0010 Utility to bump and make consistent all references to base Docker images and dependency versions.
0011 
0012 This script can:
0013 1. Bump Docker image tags (e.g., ubuntu2404:83 -> ubuntu2404:84)
0014 2. Update spack-container versions in .devcontainer/Dockerfile
0015 3. Make all references consistent across the codebase
0016 """
0017 
0018 import re
0019 import sys
0020 import difflib
0021 from pathlib import Path
0022 from typing import Annotated
0023 
0024 import typer
0025 from rich.console import Console
0026 from rich.table import Table
0027 from rich.panel import Panel
0028 from rich.syntax import Syntax
0029 from rich import box
0030 
0031 app = typer.Typer(
0032     help="Bump and make consistent Docker image tags and dependency versions",
0033     add_completion=False,
0034 )
0035 console = Console()
0036 
0037 
0038 class VersionBumper:
0039     """Handles version bumping for Docker images and dependencies."""
0040 
0041     # Patterns for different version formats
0042     PATTERNS = {
0043         # Matches: ubuntu2404:83, ubuntu2404_gnn:83, etc.
0044         "docker_tag": re.compile(
0045             r"((?:registry\.cern\.ch/)?ghcr\.io/acts-project/[a-z0-9_]+):(\d+)"
0046         ),
0047         # Matches: spack-container:18.0.0_linux-ubuntu24.04_gcc-13.3.0
0048         "spack_container": re.compile(
0049             r"(ghcr\.io/acts-project/spack-container):(\d+\.\d+\.\d+)(_[a-z0-9._-]+)"
0050         ),
0051     }
0052 
0053     # File patterns to search
0054     FILE_PATTERNS = [
0055         ".gitlab-ci.yml",
0056         ".github/workflows/*.yml",
0057         ".github/workflows/*.yaml",
0058         ".devcontainer/Dockerfile",
0059         "CI/**/*.sh",
0060         "docs/**/*.md",
0061     ]
0062 
0063     def __init__(self, repo_root: Path):
0064         self.repo_root = repo_root
0065 
0066     def find_files(self) -> list[Path]:
0067         """Find all files that might contain version references."""
0068         files = set()
0069         for pattern in self.FILE_PATTERNS:
0070             files.update(self.repo_root.glob(pattern))
0071 
0072         # Filter out build directories and dependencies
0073         return [
0074             f
0075             for f in sorted(files)
0076             if f.is_file() and "build" not in f.parts and "_deps" not in f.parts
0077         ]
0078 
0079     def find_versions(self, content: str, pattern_name: str) -> set[str]:
0080         """Find all versions matching the given pattern."""
0081         pattern = self.PATTERNS[pattern_name]
0082         matches = pattern.findall(content)
0083 
0084         if pattern_name == "docker_tag":
0085             # Return unique tags (just the number part)
0086             return {match[1] for match in matches}
0087         elif pattern_name == "spack_container":
0088             # Return full version strings
0089             return {match[1] for match in matches}
0090 
0091         return set()
0092 
0093     def scan_versions(self) -> dict:
0094         """Scan the repository for all current versions."""
0095         versions = {
0096             "docker_tags": set(),
0097             "spack_container_versions": set(),
0098             "files_with_docker_tags": [],
0099             "files_with_spack_container": [],
0100         }
0101 
0102         files = self.find_files()
0103         console.print(f"[dim]Scanning {len(files)} files...[/dim]")
0104 
0105         for file_path in files:
0106             try:
0107                 content = file_path.read_text()
0108 
0109                 # Check for Docker tags
0110                 docker_tags = self.find_versions(content, "docker_tag")
0111                 if docker_tags:
0112                     versions["docker_tags"].update(docker_tags)
0113                     versions["files_with_docker_tags"].append(file_path)
0114 
0115                 # Check for spack-container versions
0116                 spack_versions = self.find_versions(content, "spack_container")
0117                 if spack_versions:
0118                     versions["spack_container_versions"].update(spack_versions)
0119                     versions["files_with_spack_container"].append(file_path)
0120 
0121             except Exception as e:
0122                 console.print(
0123                     f"[yellow]Warning: Error reading {file_path}: {e}[/yellow]"
0124                 )
0125 
0126         return versions
0127 
0128     def bump_docker_tag(
0129         self,
0130         file_path: Path,
0131         old_tags: set[str],
0132         new_tag: str,
0133         dry_run: bool = False,
0134         show_diff: bool = False,
0135     ) -> tuple[int, str, str]:
0136         """Bump Docker image tags in a file. Returns (replacements, old_content, new_content)."""
0137         content = file_path.read_text()
0138         pattern = self.PATTERNS["docker_tag"]
0139 
0140         replacements = 0
0141 
0142         def replace_tag(match):
0143             nonlocal replacements
0144             old_tag = match.group(2)
0145             if old_tag in old_tags and old_tag != new_tag:
0146                 replacements += 1
0147                 return f"{match.group(1)}:{new_tag}"
0148             return match.group(0)
0149 
0150         new_content = pattern.sub(replace_tag, content)
0151 
0152         if replacements > 0:
0153             if not dry_run:
0154                 file_path.write_text(new_content)
0155 
0156             prefix = "[dim][DRY RUN][/dim] " if dry_run else ""
0157             rel_path = file_path.relative_to(self.repo_root)
0158             console.print(
0159                 f"{prefix}[green]✓[/green] Updated {replacements} occurrence(s) in [cyan]{rel_path}[/cyan]"
0160             )
0161 
0162             if show_diff:
0163                 self._show_diff(file_path, content, new_content)
0164 
0165         return replacements, content, new_content
0166 
0167     def bump_spack_container(
0168         self,
0169         file_path: Path,
0170         old_versions: set[str],
0171         new_version: str,
0172         dry_run: bool = False,
0173         show_diff: bool = False,
0174     ) -> tuple[int, str, str]:
0175         """Bump spack-container version in a file. Returns (replacements, old_content, new_content)."""
0176         content = file_path.read_text()
0177         pattern = self.PATTERNS["spack_container"]
0178 
0179         replacements = 0
0180 
0181         def replace_version(match):
0182             nonlocal replacements
0183             old_version = match.group(2)
0184             if old_version in old_versions and old_version != new_version:
0185                 replacements += 1
0186                 return f"{match.group(1)}:{new_version}{match.group(3)}"
0187             return match.group(0)
0188 
0189         new_content = pattern.sub(replace_version, content)
0190 
0191         if replacements > 0:
0192             if not dry_run:
0193                 file_path.write_text(new_content)
0194 
0195             prefix = "[dim][DRY RUN][/dim] " if dry_run else ""
0196             rel_path = file_path.relative_to(self.repo_root)
0197             console.print(
0198                 f"{prefix}[green]✓[/green] Updated {replacements} occurrence(s) in [cyan]{rel_path}[/cyan]"
0199             )
0200 
0201             if show_diff:
0202                 self._show_diff(file_path, content, new_content)
0203 
0204         return replacements, content, new_content
0205 
0206     def _show_diff(self, file_path: Path, old_content: str, new_content: str):
0207         """Display a unified diff of the changes."""
0208         rel_path = file_path.relative_to(self.repo_root)
0209         diff = difflib.unified_diff(
0210             old_content.splitlines(),
0211             new_content.splitlines(),
0212             fromfile=str(rel_path),
0213             tofile=str(rel_path),
0214             lineterm="",
0215         )
0216 
0217         diff_lines = list(diff)
0218         if diff_lines:
0219             console.print()
0220             diff_text = "\n".join(diff_lines)
0221             syntax = Syntax(diff_text, "diff", theme="monokai", line_numbers=False)
0222             console.print(syntax)
0223 
0224     def bump_all_docker_tags(
0225         self,
0226         old_tags: set[str],
0227         new_tag: str,
0228         dry_run: bool = False,
0229         show_diff: bool = False,
0230     ) -> int:
0231         """Bump all Docker tags across the repository."""
0232         files = self.find_files()
0233         total_replacements = 0
0234 
0235         for file_path in files:
0236             try:
0237                 replacements, _, _ = self.bump_docker_tag(
0238                     file_path, old_tags, new_tag, dry_run, show_diff
0239                 )
0240                 total_replacements += replacements
0241             except Exception as e:
0242                 console.print(f"[red]Error processing {file_path}: {e}[/red]")
0243 
0244         return total_replacements
0245 
0246     def bump_all_spack_containers(
0247         self,
0248         old_versions: set[str],
0249         new_version: str,
0250         dry_run: bool = False,
0251         show_diff: bool = False,
0252     ) -> int:
0253         """Bump all spack-container versions across the repository."""
0254         files = self.find_files()
0255         total_replacements = 0
0256 
0257         for file_path in files:
0258             try:
0259                 replacements, _, _ = self.bump_spack_container(
0260                     file_path, old_versions, new_version, dry_run, show_diff
0261                 )
0262                 total_replacements += replacements
0263             except Exception as e:
0264                 console.print(f"[red]Error processing {file_path}: {e}[/red]")
0265 
0266         return total_replacements
0267 
0268 
0269 @app.command()
0270 def scan(
0271     repo_root: Annotated[
0272         Path, typer.Option(help="Root directory of the repository")
0273     ] = Path.cwd(),
0274 ):
0275     """
0276     Scan repository for current versions.
0277 
0278     This command scans the codebase and displays all Docker image tags
0279     and spack-container versions currently in use.
0280     """
0281     bumper = VersionBumper(repo_root)
0282     versions = bumper.scan_versions()
0283 
0284     console.print()
0285 
0286     # Docker tags
0287     if versions["docker_tags"]:
0288         console.print("[bold]Docker image tags:[/bold]")
0289         for tag in sorted(versions["docker_tags"]):
0290             console.print(f"  [cyan]{tag}[/cyan]")
0291         console.print(
0292             f"[dim]  Found in {len(versions['files_with_docker_tags'])} file(s)[/dim]"
0293         )
0294     else:
0295         console.print("[bold]Docker image tags:[/bold] [yellow]none found[/yellow]")
0296 
0297     console.print()
0298 
0299     # Spack-container versions
0300     if versions["spack_container_versions"]:
0301         console.print("[bold]Spack-container versions:[/bold]")
0302         for version in sorted(versions["spack_container_versions"]):
0303             console.print(f"  [cyan]{version}[/cyan]")
0304         console.print(
0305             f"[dim]  Found in {len(versions['files_with_spack_container'])} file(s)[/dim]"
0306         )
0307     else:
0308         console.print(
0309             "[bold]Spack-container versions:[/bold] [yellow]none found[/yellow]"
0310         )
0311 
0312 
0313 @app.command()
0314 def bump_docker_tag(
0315     new_tag: Annotated[str, typer.Argument(help="New Docker tag to use (e.g., 84)")],
0316     repo_root: Annotated[
0317         Path, typer.Option(help="Root directory of the repository")
0318     ] = Path.cwd(),
0319     dry_run: Annotated[
0320         bool, typer.Option("--dry-run", help="Preview changes without modifying files")
0321     ] = False,
0322     show_diff: Annotated[
0323         bool, typer.Option("--diff", help="Show diff of changes")
0324     ] = True,
0325 ):
0326     """
0327     Bump Docker image tags across the repository.
0328 
0329     This command finds all Docker images like 'ubuntu2404:83' and updates
0330     all tag numbers to the new value.
0331 
0332     Example:
0333         bump_versions.py bump-docker-tag 84
0334         bump_versions.py bump-docker-tag 84 --dry-run --diff
0335     """
0336     bumper = VersionBumper(repo_root)
0337 
0338     # Scan for existing tags
0339     versions = bumper.scan_versions()
0340     if not versions["docker_tags"]:
0341         console.print("[red]Error: No Docker tags found in repository[/red]")
0342         raise typer.Exit(1)
0343 
0344     # Replace ALL found tags
0345     old_tags = versions["docker_tags"]
0346     console.print(f"[dim]Found Docker tags: {', '.join(sorted(old_tags))}[/dim]")
0347 
0348     # Check if new tag is same as all old tags
0349     if len(old_tags) == 1 and new_tag in old_tags:
0350         console.print(f"[yellow]Tag is already {new_tag}, no changes needed[/yellow]")
0351         raise typer.Exit(0)
0352 
0353     console.print(f"[dim]Will replace ALL tags with: {new_tag}[/dim]")
0354 
0355     console.print()
0356     if dry_run:
0357         console.print(
0358             Panel(
0359                 f"[bold]DRY RUN:[/bold] Bumping Docker tags [cyan]{', '.join(sorted(old_tags))}[/cyan] → [cyan]{new_tag}[/cyan]",
0360                 border_style="yellow",
0361             )
0362         )
0363     else:
0364         console.print(
0365             Panel(
0366                 f"Bumping Docker tags [cyan]{', '.join(sorted(old_tags))}[/cyan] → [cyan]{new_tag}[/cyan]",
0367                 border_style="green",
0368             )
0369         )
0370 
0371     console.print()
0372     total = bumper.bump_all_docker_tags(old_tags, new_tag, dry_run, show_diff)
0373 
0374     console.print()
0375     if total > 0:
0376         if dry_run:
0377             console.print(
0378                 f"[green]✓[/green] Would update [bold]{total}[/bold] occurrence(s)"
0379             )
0380             console.print("[dim]Run without --dry-run to apply changes[/dim]")
0381         else:
0382             console.print(
0383                 f"[green]✓[/green] Updated [bold]{total}[/bold] occurrence(s)"
0384             )
0385     else:
0386         console.print(f"[yellow]No occurrences found[/yellow]")
0387 
0388 
0389 @app.command()
0390 def bump_spack(
0391     new_version: Annotated[
0392         str, typer.Argument(help="New spack-container version (e.g., 19.0.0)")
0393     ],
0394     repo_root: Annotated[
0395         Path, typer.Option(help="Root directory of the repository")
0396     ] = Path.cwd(),
0397     dry_run: Annotated[
0398         bool, typer.Option("--dry-run", help="Preview changes without modifying files")
0399     ] = False,
0400     show_diff: Annotated[
0401         bool, typer.Option("--diff", help="Show diff of changes")
0402     ] = True,
0403 ):
0404     """
0405     Bump spack-container version across the repository.
0406 
0407     This command updates the spack-container image version used in
0408     .devcontainer/Dockerfile and other configuration files. It replaces
0409     all found versions with the new version.
0410 
0411     Example:
0412         bump_versions.py bump-spack 19.0.0
0413         bump_versions.py bump-spack 19.0.0 --dry-run --diff
0414     """
0415     bumper = VersionBumper(repo_root)
0416 
0417     # Scan for existing versions
0418     versions = bumper.scan_versions()
0419     if not versions["spack_container_versions"]:
0420         console.print(
0421             "[red]Error: No spack-container versions found in repository[/red]"
0422         )
0423         raise typer.Exit(1)
0424 
0425     # Replace ALL found versions
0426     old_versions = versions["spack_container_versions"]
0427     console.print(
0428         f"[dim]Found spack-container versions: {', '.join(sorted(old_versions))}[/dim]"
0429     )
0430 
0431     # Check if new version is same as all old versions
0432     if len(old_versions) == 1 and new_version in old_versions:
0433         console.print(
0434             f"[yellow]Version is already {new_version}, no changes needed[/yellow]"
0435         )
0436         raise typer.Exit(0)
0437 
0438     console.print(f"[dim]Will replace ALL versions with: {new_version}[/dim]")
0439 
0440     console.print()
0441     if dry_run:
0442         console.print(
0443             Panel(
0444                 f"[bold]DRY RUN:[/bold] Bumping spack-container [cyan]{', '.join(sorted(old_versions))}[/cyan] → [cyan]{new_version}[/cyan]",
0445                 border_style="yellow",
0446             )
0447         )
0448     else:
0449         console.print(
0450             Panel(
0451                 f"Bumping spack-container [cyan]{', '.join(sorted(old_versions))}[/cyan] → [cyan]{new_version}[/cyan]",
0452                 border_style="green",
0453             )
0454         )
0455 
0456     console.print()
0457     total = bumper.bump_all_spack_containers(
0458         old_versions, new_version, dry_run, show_diff
0459     )
0460 
0461     console.print()
0462     if total > 0:
0463         if dry_run:
0464             console.print(
0465                 f"[green]✓[/green] Would update [bold]{total}[/bold] occurrence(s)"
0466             )
0467             console.print("[dim]Run without --dry-run to apply changes[/dim]")
0468         else:
0469             console.print(
0470                 f"[green]✓[/green] Updated [bold]{total}[/bold] occurrence(s)"
0471             )
0472     else:
0473         console.print(f"[yellow]No occurrences found[/yellow]")
0474 
0475 
0476 if __name__ == "__main__":
0477     app()