From 4b9b7b6336ea7beafd845cc0f991d9499a11f8bc Mon Sep 17 00:00:00 2001 From: Maik Knof Date: Mon, 29 Dec 2025 16:25:08 +0000 Subject: [PATCH] add list verb and rosdistro_loader --- buildfarm/command.py | 58 +++++++++++++++++++++++ buildfarm/package.py | 12 +++++ buildfarm/rosdistro_loader.py | 86 +++++++++++++++++++++++++++++++++++ buildfarm/verb/__init__.py | 0 buildfarm/verb/list.py | 41 +++++++++++++++++ package.xml | 2 +- setup.py | 14 +++--- 7 files changed, 205 insertions(+), 8 deletions(-) create mode 100644 buildfarm/command.py create mode 100644 buildfarm/package.py create mode 100644 buildfarm/rosdistro_loader.py create mode 100644 buildfarm/verb/__init__.py create mode 100644 buildfarm/verb/list.py diff --git a/buildfarm/command.py b/buildfarm/command.py new file mode 100644 index 0000000..ca3680b --- /dev/null +++ b/buildfarm/command.py @@ -0,0 +1,58 @@ +import os +import sys + +from ros2cli.command import CommandExtension + +from .rosdistro_loader import fetch_rosdistro_packages +from .verb.list import add_list_verb + + +class BuildfarmCommand(CommandExtension): + """https://git.maikknof.de/ros2/buildfarm""" + + def add_arguments(self, parser, cli_name): + parser.add_argument( + "--base-url", + default="https://git.maikknof.de", + help="Base URL of Git instance", + ) + parser.add_argument( + "--owner", default="ros2", help="Repo owner or organization" + ) + parser.add_argument("--repo", default="rosdistro", help="Repository name") + parser.add_argument("--branch", default="main", help="Branch name") + + env_rosdistro = os.environ.get("ROS_DISTRO") or os.environ.get("ROSDISTRO") + parser.add_argument( + "--rosdistro", + default=env_rosdistro, + required=(env_rosdistro is None), + help="ROS distro name (defaults to env ROS_DISTRO/ROSDISTRO)", + ) + + subparsers = parser.add_subparsers(dest="verb", metavar="verb") + subparsers.required = True + + add_list_verb(subparsers) + + self._parser = parser + + def main(self, *, parser, args): + if not hasattr(args, "main"): + self._parser.print_help() + return 0 + + try: + packages = fetch_rosdistro_packages( + base_url=args.base_url, + owner=args.owner, + repo=args.repo, + branch=args.branch, + rosdistro=args.rosdistro, + ) + except Exception as e: + print(f"Error loading rosdistro packages: {e}", file=sys.stderr) + return 1 + + setattr(args, "packages", packages) + return args.main(args) diff --git a/buildfarm/package.py b/buildfarm/package.py new file mode 100644 index 0000000..d24c882 --- /dev/null +++ b/buildfarm/package.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass +class Package: + name: str + owner: str + repo: str + ref: str diff --git a/buildfarm/rosdistro_loader.py b/buildfarm/rosdistro_loader.py new file mode 100644 index 0000000..4051796 --- /dev/null +++ b/buildfarm/rosdistro_loader.py @@ -0,0 +1,86 @@ +from __future__ import annotations + +import re +from typing import Dict + +import requests +import yaml + +from .package import Package + + +def _raw_url(base_url: str, owner: str, repo: str, branch: str, file_path: str) -> str: + return ( + f"{base_url.rstrip('/')}/" + f"{owner}/{repo}/raw/branch/{branch}/{file_path.lstrip('/')}" + ) + + +def _parse_package_ref(value: str) -> tuple[str, str, str]: + """ + Parses "owner/repo@ref" into (owner, repo, ref). + """ + value = value.strip() + + if "@" not in value: + raise ValueError(f"Invalid package reference '{value}': missing '@'") + + left, ref = value.rsplit("@", 1) + left = left.strip() + ref = ref.strip() + + if "/" not in left: + raise ValueError(f"Invalid package reference '{value}': missing '/'") + + owner, repo = left.split("/", 1) + owner = owner.strip() + repo = repo.strip() + + if not owner or not repo or not ref: + raise ValueError(f"Invalid package reference '{value}': empty owner/repo/ref") + + if re.search(r"\s", value): + raise ValueError(f"Invalid package reference '{value}': contains whitespace") + + return owner, repo, ref + + +def fetch_rosdistro_packages( + *, + base_url: str, + owner: str, + repo: str, + branch: str, + rosdistro: str, + timeout_s: float = 20.0, +) -> Dict[str, Package]: + """ + Fetches rosdistro//distro.yaml from a Gitea repo and parses it into: + { package_name: Package(name, owner, repo, ref) } + """ + file_path = f"rosdistro/{rosdistro}/distro.yaml" + url = _raw_url(base_url, owner, repo, branch, file_path) + + resp = requests.get(url, timeout=timeout_s) + if resp.status_code != 200: + raise RuntimeError( + f"Failed to fetch '{url}' (HTTP {resp.status_code}): {resp.text}" + ) + + data = yaml.safe_load(resp.text) + if not isinstance(data, dict): + raise ValueError( + f"Invalid YAML structure in {file_path}: expected mapping at root" + ) + + packages: Dict[str, Package] = {} + for name, ref in data.items(): + if not isinstance(name, str): + raise ValueError(f"Invalid package key in YAML: {name!r} (expected string)") + if not isinstance(ref, str): + raise ValueError(f"Invalid ref for '{name}': {ref!r} (expected string)") + + pkg_owner, pkg_repo, pkg_ref = _parse_package_ref(ref) + packages[name] = Package(name=name, owner=pkg_owner, repo=pkg_repo, ref=pkg_ref) + + return packages diff --git a/buildfarm/verb/__init__.py b/buildfarm/verb/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/buildfarm/verb/list.py b/buildfarm/verb/list.py new file mode 100644 index 0000000..61d247a --- /dev/null +++ b/buildfarm/verb/list.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +from typing import Any, Dict + +from ros2cli.verb import VerbExtension + +from ..package import Package + + +class ListVerb(VerbExtension): + """List packages from rosdistro//distro.yaml.""" + + def add_arguments(self, parser, cli_name): + parser.add_argument( + "--format", + default="plain", + choices=("plain", "yaml"), + help="Output format", + ) + + def main(self, *, args): + packages: Dict[str, Package] = getattr(args, "packages", {}) + + if args.format == "yaml": + for name in sorted(packages.keys()): + pkg = packages[name] + print(f"{name}: {pkg.owner}/{pkg.repo}@{pkg.ref}") + return 0 + + for name in sorted(packages.keys()): + pkg = packages[name] + print(f"{name}: {pkg.owner}/{pkg.repo}@{pkg.ref}") + + return 0 + + +def add_list_verb(subparsers): + parser = subparsers.add_parser("list", help="List packages from rosdistro") + verb = ListVerb() + verb.add_arguments(parser, "buildfarm") + parser.set_defaults(main=verb.main) diff --git a/package.xml b/package.xml index fe6cd33..d5996be 100644 --- a/package.xml +++ b/package.xml @@ -1,7 +1,7 @@ buildfarm - 0.0.0 + 0.0.1 Scripts to build ROS2 packages as .deb packages. Maik Knof TODO diff --git a/setup.py b/setup.py index 55c77fd..fe9dab5 100644 --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ data_files = [ setup( name=package_name, - version="0.0.0", + version="0.0.1", packages=[package_name], data_files=data_files, install_requires=["setuptools", "ros2cli", "bringup_cli"], @@ -20,11 +20,11 @@ setup( license="TODO: License declaration", tests_require=["pytest"], entry_points={ - # "ros2cli.command": [ - # "buildfarm = buildfarm.command:BuildfarmCommand", - # ], - # "ros2cli.extension_point": [ - # "buildfarm = ros2cli.command.CommandExtension", - # ], + "ros2cli.command": [ + "buildfarm = buildfarm.command:BuildfarmCommand", + ], + "ros2cli.extension_point": [ + "buildfarm = ros2cli.command.CommandExtension", + ], }, )