|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from importlib.metadata import distributions |
| 4 | +from pathlib import Path |
| 5 | +from typing import Any |
| 6 | + |
| 7 | +import click |
| 8 | + |
| 9 | +_BACKEND_DIST_PREFIX = "backend.ai-" |
| 10 | + |
| 11 | + |
| 12 | +def _collect_dist_versions() -> dict[str, str]: |
| 13 | + """Collect versions of installed backend.ai-* distributions.""" |
| 14 | + versions: dict[str, str] = {} |
| 15 | + for dist in distributions(): |
| 16 | + name = dist.metadata["Name"] |
| 17 | + if name and name.lower().startswith(_BACKEND_DIST_PREFIX): |
| 18 | + versions[name] = dist.version |
| 19 | + return versions |
| 20 | + |
| 21 | + |
| 22 | +def _collect_namespace_versions() -> dict[str, str]: |
| 23 | + """ |
| 24 | + Collect versions from VERSION files under the `ai.backend` namespace package. |
| 25 | +
|
| 26 | + This handles dev-mode layouts where subpackages are loaded directly from |
| 27 | + the source tree and are not registered as separate distributions. |
| 28 | + Walks one level into nested namespace packages (e.g., `ai.backend.appproxy.*`) |
| 29 | + so multi-distribution namespaces are covered. |
| 30 | + """ |
| 31 | + versions: dict[str, str] = {} |
| 32 | + try: |
| 33 | + import ai.backend as ns |
| 34 | + except ImportError: |
| 35 | + return versions |
| 36 | + seen: set[Path] = set() |
| 37 | + for root_str in ns.__path__: |
| 38 | + root = Path(root_str) |
| 39 | + if not root.is_dir(): |
| 40 | + continue |
| 41 | + for child in sorted(root.iterdir()): |
| 42 | + if not child.is_dir() or child in seen: |
| 43 | + continue |
| 44 | + seen.add(child) |
| 45 | + version_file = child / "VERSION" |
| 46 | + if version_file.is_file(): |
| 47 | + key = f"{_BACKEND_DIST_PREFIX}{child.name.replace('_', '-')}" |
| 48 | + versions.setdefault(key, version_file.read_text().strip()) |
| 49 | + continue |
| 50 | + # Recurse one level for namespace subpackages (e.g., appproxy/*). |
| 51 | + for grand in sorted(child.iterdir()): |
| 52 | + if not grand.is_dir(): |
| 53 | + continue |
| 54 | + version_file = grand / "VERSION" |
| 55 | + if not version_file.is_file(): |
| 56 | + continue |
| 57 | + parent = child.name.replace("_", "-") |
| 58 | + leaf = grand.name.replace("_", "-") |
| 59 | + key = f"{_BACKEND_DIST_PREFIX}{parent}-{leaf}" |
| 60 | + versions.setdefault(key, version_file.read_text().strip()) |
| 61 | + return versions |
| 62 | + |
| 63 | + |
| 64 | +def collect_versions() -> list[tuple[str, str]]: |
| 65 | + """Return sorted (name, version) pairs of backend.ai-* packages.""" |
| 66 | + merged = _collect_namespace_versions() |
| 67 | + for name, version in _collect_dist_versions().items(): |
| 68 | + merged[name] = version |
| 69 | + return sorted(merged.items()) |
| 70 | + |
| 71 | + |
| 72 | +def print_version(ctx: click.Context, _param: click.Parameter, value: Any) -> None: |
| 73 | + if not value or ctx.resilient_parsing: |
| 74 | + return |
| 75 | + versions = collect_versions() |
| 76 | + if not versions: |
| 77 | + click.echo("No backend.ai-* packages found.") |
| 78 | + else: |
| 79 | + width = max(len(name) for name, _ in versions) |
| 80 | + for name, version in versions: |
| 81 | + click.echo(f"{name:<{width}} {version}") |
| 82 | + ctx.exit() |
0 commit comments