add list verb and rosdistro_loader
This commit is contained in:
58
buildfarm/command.py
Normal file
58
buildfarm/command.py
Normal file
@@ -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)
|
||||
12
buildfarm/package.py
Normal file
12
buildfarm/package.py
Normal file
@@ -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
|
||||
86
buildfarm/rosdistro_loader.py
Normal file
86
buildfarm/rosdistro_loader.py
Normal file
@@ -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/<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
|
||||
0
buildfarm/verb/__init__.py
Normal file
0
buildfarm/verb/__init__.py
Normal file
41
buildfarm/verb/list.py
Normal file
41
buildfarm/verb/list.py
Normal file
@@ -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/<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)
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0"?>
|
||||
<package format="3">
|
||||
<name>buildfarm</name>
|
||||
<version>0.0.0</version>
|
||||
<version>0.0.1</version>
|
||||
<description>Scripts to build ROS2 packages as .deb packages.</description>
|
||||
<maintainer email="mail@maikknof.de">Maik Knof</maintainer>
|
||||
<license>TODO</license>
|
||||
|
||||
14
setup.py
14
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",
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user