Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Scripts

## manage_aliases.py

Unified tool for managing Homebrew aliases in the Gazebo tap.

### Subcommands

#### `bump` — Next-major-version aliases

Creates aliases that point the *next* major version to the *current* latest
formula. For example, `Aliases/gz-cmake6 -> ../Formula/gz-cmake5.rb`.

```bash
# Bump all discovered gz-*/sdformat libraries
python3 scripts/manage_aliases.py bump

# Preview without creating anything
python3 scripts/manage_aliases.py bump --dry-run

# Bump specific libraries only
python3 scripts/manage_aliases.py bump gz-cmake sdformat
```

Libraries are auto-discovered from `Formula/` — no hardcoded list.

#### `collection-aliases` — Collection-scoped aliases

Creates aliases that give a collection-prefixed name to each dependency of a
collection formula. For example,
`Aliases/gz-jetty-cmake -> ../Formula/gz-cmake5.rb`.

```bash
# Create aliases for the Jetty collection
python3 scripts/manage_aliases.py collection-aliases jetty

# Preview first
python3 scripts/manage_aliases.py collection-aliases jetty --dry-run
```

Dependencies are parsed from `Formula/gz-{collection}.rb`.

### Behavior

- **Auto-discovery**: `bump` scans `Formula/` so new libraries are picked up
automatically.
- **Idempotent**: re-running reports "already exists, correct" for valid
aliases.
- **Auto-fix**: wrong symlink targets are corrected automatically (the old
script required manual intervention).
- **`--dry-run`**: preview all changes without touching the filesystem.
- Alias files have no `.rb` extension (Homebrew convention).
- Symlink targets are relative (`../Formula/…`).
206 changes: 206 additions & 0 deletions scripts/manage_aliases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
#!/usr/bin/env python3

"""Unified alias management for the Gazebo Homebrew tap.

Subcommands:
bump Create next-major-version aliases for gz-*/sdformat libraries.
collection-aliases Create collection-scoped aliases (e.g. gz-jetty-cmake).
"""

import argparse
import re
import sys
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent
FORMULA_DIR = REPO_ROOT / "Formula"
ALIAS_DIR = REPO_ROOT / "Aliases"

# Matches versioned gz-* and sdformat* formulas, excluding collection formulas
# (gz-jetty.rb has no trailing digits) and ignition-* formulas.
VERSIONED_FORMULA_RE = re.compile(r"^((?:gz-[a-z][-a-z]*|sdformat))(\d+)\.rb$")

# Matches gz-* and sdformat* dependency strings inside a collection formula.
DEPENDS_ON_RE = re.compile(r'depends_on\s+"((?:gz-[a-z][-a-z]*|sdformat)\d+)"')


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def discover_libraries(formula_dir: Path) -> dict[str, int]:
"""Scan Formula/ and return {library_name: latest_version}."""
libs: dict[str, int] = {}
for path in sorted(formula_dir.iterdir()):
m = VERSIONED_FORMULA_RE.match(path.name)
if m:
name, version = m.group(1), int(m.group(2))
if name not in libs or version > libs[name]:
libs[name] = version
return libs


def dep_to_short_name(dep: str) -> str:
"""Strip version and gz- prefix to get a short name.

gz-fuel-tools11 -> fuel-tools
sdformat16 -> sdformat
"""
m = re.match(r"^(?:gz-)?(.+?)\d+$", dep)
return m.group(1) if m else dep


def parse_collection_deps(formula_path: Path) -> list[str]:
"""Extract gz-*/sdformat* dependency names from a collection formula."""
text = formula_path.read_text()
return DEPENDS_ON_RE.findall(text)


def create_symlink(alias_path: Path, target: str, *, dry_run: bool) -> None:
"""Create, validate, or fix a symlink at *alias_path* -> *target*."""
if alias_path.is_symlink():
current = alias_path.readlink().as_posix()
if current == target:
print(f" {alias_path.name}: already exists, correct")
return
# Wrong target — fix it.
if dry_run:
print(f" {alias_path.name}: would fix ({current} -> {target})")
else:
alias_path.unlink()
alias_path.symlink_to(target)
print(f" {alias_path.name}: fixed ({current} -> {target})")
return

if alias_path.exists():
print(f" {alias_path.name}: WARNING — exists but is not a symlink, skipping",
file=sys.stderr)
return

if dry_run:
print(f" {alias_path.name}: would create -> {target}")
else:
alias_path.symlink_to(target)
print(f" {alias_path.name}: created -> {target}")


# ---------------------------------------------------------------------------
# Subcommands
# ---------------------------------------------------------------------------

def cmd_bump(args: argparse.Namespace) -> int:
"""Create next-major-version aliases for libraries."""
libs = discover_libraries(FORMULA_DIR)

if args.libraries:
# Filter to requested libraries only.
selected = {}
for name in args.libraries:
if name in libs:
selected[name] = libs[name]
else:
print(f"Warning: no versioned formula found for '{name}', skipping",
file=sys.stderr)
libs = selected

if not libs:
print("No libraries to process.", file=sys.stderr)
return 1

if not args.dry_run:
ALIAS_DIR.mkdir(exist_ok=True)

print(f"Processing {len(libs)} libraries (dry_run={args.dry_run}):\n")
for name in sorted(libs):
current_ver = libs[name]
next_ver = current_ver + 1
alias_name = f"{name}{next_ver}"
target = f"../Formula/{name}{current_ver}.rb"
create_symlink(ALIAS_DIR / alias_name, target, dry_run=args.dry_run)

return 0


def cmd_collection_aliases(args: argparse.Namespace) -> int:
"""Create collection-scoped aliases for a given collection."""
collection = args.collection
formula_path = FORMULA_DIR / f"gz-{collection}.rb"

if not formula_path.exists():
print(f"Error: formula not found: {formula_path}", file=sys.stderr)
return 1

deps = parse_collection_deps(formula_path)
if not deps:
print(f"No gz-*/sdformat* dependencies found in {formula_path.name}",
file=sys.stderr)
return 1

if not args.dry_run:
ALIAS_DIR.mkdir(exist_ok=True)

print(f"Creating {len(deps)} collection aliases for gz-{collection} "
f"(dry_run={args.dry_run}):\n")
for dep in deps:
short = dep_to_short_name(dep)
alias_name = f"gz-{collection}-{short}"
formula_path = FORMULA_DIR / f"{dep}.rb"
alias_target_path = ALIAS_DIR / dep

if formula_path.exists():
target = f"../Formula/{dep}.rb"
elif alias_target_path.is_symlink():
target = f"../Aliases/{dep}"
else:
print(f" {alias_name}: ERROR — no Formula or Alias found for '{dep}'",
file=sys.stderr)
return 1

create_symlink(ALIAS_DIR / alias_name, target, dry_run=args.dry_run)

return 0


# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------

def main() -> int:
parser = argparse.ArgumentParser(
description="Manage Homebrew aliases for the Gazebo tap.")
sub = parser.add_subparsers(dest="command", required=True)

# -- bump --
bump_p = sub.add_parser(
"bump",
help="Create next-major-version aliases (e.g. Aliases/gz-cmake6 -> Formula/gz-cmake5.rb)")
bump_p.add_argument(
"libraries", nargs="*", metavar="LIBRARY",
help="Versionless library names to bump (default: all discovered libraries)")
bump_p.add_argument(
"--dry-run", action="store_true",
help="Print what would be done without creating symlinks")

# -- collection-aliases --
col_p = sub.add_parser(
"collection-aliases",
help="Create collection-scoped aliases (e.g. Aliases/gz-jetty-cmake -> Formula/gz-cmake5.rb)")
col_p.add_argument(
"collection", metavar="COLLECTION",
help="Collection name without gz- prefix (e.g. jetty, ionic, harmonic)")
col_p.add_argument(
"--dry-run", action="store_true",
help="Print what would be done without creating symlinks")

args = parser.parse_args()

if args.command == "bump":
return cmd_bump(args)
elif args.command == "collection-aliases":
return cmd_collection_aliases(args)

return 0


if __name__ == "__main__":
sys.exit(main())