From 48e70105e9dac8c5f0d6d4b7ffab917ffe3f5431 Mon Sep 17 00:00:00 2001 From: Ruben Arts Date: Fri, 2 Oct 2026 13:35:05 +0200 Subject: [PATCH] check the ros2 prefix to fix humble packages --- .github/workflows/checks.yml | 1 + .gitignore | 12 ++++ pixi.lock | 50 +++++++++++++++++ pixi.toml | 5 ++ scripts/copy-to-distro-specific-channel.py | 32 ++++++++--- .../test_copy_to_distro_specific_channel.py | 55 +++++++++++++++++++ 6 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 scripts/test_copy_to_distro_specific_channel.py diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 707cd0cc..cc651533 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -15,6 +15,7 @@ jobs: persist-credentials: false - uses: prefix-dev/setup-pixi@d3f436a425481402e6a95a1d1fc10331c708cd9e # v0.10.2 - run: pixi run lint + - run: pixi run test - run: pixi run build-docs dependabot: diff --git a/.gitignore b/.gitignore index 071114f9..b7a0fcef 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,18 @@ RoboStack Website/ .pixi *.egg-info +# Python artifacts +__pycache__/ +*.py[cod] +*.pyo +*.pyd +*.pycache +*.pytest_cache +*.mypy_cache +*.coverage +coverage.xml +htmlcov/ + # astro node_modules dist diff --git a/pixi.lock b/pixi.lock index dfaee83d..65b79444 100644 --- a/pixi.lock +++ b/pixi.lock @@ -117,6 +117,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.1.2-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.6.0-pyhcf101f3_0.conda @@ -133,12 +134,14 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - conda: https://prefix.dev/conda-forge/noarch/pkce-1.0.3-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.11.1-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://prefix.dev/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pydantic-settings-2.15.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/pyjwt-2.13.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://prefix.dev/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda - conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://prefix.dev/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/python-fastjsonschema-2.22.1-pyhcf101f3_0.conda @@ -200,6 +203,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.1.2-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.6.0-pyhcf101f3_0.conda @@ -215,12 +219,14 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - conda: https://prefix.dev/conda-forge/noarch/pkce-1.0.3-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.11.1-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://prefix.dev/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pydantic-settings-2.15.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/pyjwt-2.13.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://prefix.dev/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda - conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://prefix.dev/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/python-fastjsonschema-2.22.1-pyhcf101f3_0.conda @@ -337,6 +343,7 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/idna-3.18-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib-metadata-9.0.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/importlib_resources-7.1.0-pyhd8ed1ab_0.conda + - conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.context-6.1.2-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/jaraco.functools-4.6.0-pyhcf101f3_0.conda @@ -352,12 +359,14 @@ environments: - conda: https://prefix.dev/conda-forge/noarch/packaging-26.3-pyhc364b38_0.conda - conda: https://prefix.dev/conda-forge/noarch/pkce-1.0.3-pyhd8ed1ab_1.conda - conda: https://prefix.dev/conda-forge/noarch/platformdirs-4.11.1-pyhcf101f3_0.conda + - conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda - conda: https://prefix.dev/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pydantic-2.13.4-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pydantic-settings-2.15.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pygments-2.20.0-pyhd8ed1ab_0.conda - conda: https://prefix.dev/conda-forge/noarch/pyjwt-2.13.0-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://prefix.dev/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda - conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://prefix.dev/conda-forge/noarch/python-dotenv-1.2.2-pyhcf101f3_0.conda - conda: https://prefix.dev/conda-forge/noarch/python-fastjsonschema-2.22.1-pyhcf101f3_0.conda @@ -1824,6 +1833,16 @@ packages: run_exports: {} size: 34809 timestamp: 1776068839274 +- conda: https://prefix.dev/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 + md5: 9614359868482abba1bd15ce465e3c42 + depends: + - python >=3.10 + license: MIT + license_family: MIT + run_exports: {} + size: 13387 + timestamp: 1760831448842 - conda: https://prefix.dev/conda-forge/noarch/jaraco.classes-3.4.0-pyhcf101f3_3.conda sha256: 3cc991f0f09dfd00d2626e745ba68da03e4f1dcbb7b36dd20f7a7373643cd5d5 md5: d59568bad316413c89831456e691de29 @@ -2072,6 +2091,17 @@ packages: run_exports: {} size: 26817 timestamp: 1786197727673 +- conda: https://prefix.dev/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e + md5: d7585b6550ad04c8c5e21097ada2888e + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + run_exports: {} + size: 25877 + timestamp: 1764896838868 - conda: https://prefix.dev/conda-forge/noarch/pycparser-3.0-pyhcf101f3_0.conda sha256: e27e0473fc6723311a0bd48b89b616fa1b996a2f7a2b555338cbbcfb9c640568 md5: 9c5491066224083c41b6d5635ed7107b @@ -2159,6 +2189,26 @@ packages: run_exports: {} size: 21085 timestamp: 1733217331982 +- conda: https://prefix.dev/conda-forge/noarch/pytest-9.1.1-pyhc364b38_2.conda + sha256: 430051d80765207a7d782b2b188230ba1489d35c6e75fd9903f76cb9fda4af16 + md5: 64c98a12c4e23eb238bf66bbecafdf3c + depends: + - colorama + - pygments >=2.7.2 + - python >=3.10 + - iniconfig >=1.0.1 + - packaging >=22 + - pluggy >=1.5,<2 + - tomli >=1 + - exceptiongroup >=1 + - python + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + run_exports: {} + size: 306724 + timestamp: 1782127176429 - conda: https://prefix.dev/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 md5: 5b8d21249ff20967101ffa321cab24e8 diff --git a/pixi.toml b/pixi.toml index da7e6adb..58382447 100644 --- a/pixi.toml +++ b/pixi.toml @@ -85,8 +85,13 @@ anaconda-client = ">=1.14.1,<2" pyyaml = ">=6,<7" py-rattler = ">=0.25,<0.26" niquests = ">=3.21.0,<4" +pytest = ">=9.1.1,<10" [feature.scripts.tasks] +test = { + cmd = "pytest scripts", + description = "Run the Python regression tests", +} check-dependabot-merge = { cmd = "python scripts/check_dependabot_merge.py", description = "Check whether every Dependabot update qualifies for auto-merge", diff --git a/scripts/copy-to-distro-specific-channel.py b/scripts/copy-to-distro-specific-channel.py index 0f3137b1..6b2e9a09 100644 --- a/scripts/copy-to-distro-specific-channel.py +++ b/scripts/copy-to-distro-specific-channel.py @@ -3,6 +3,7 @@ import subprocess import niquests +from rattler import MatchSpec from urllib3.util.retry import Retry # Configuration @@ -88,6 +89,29 @@ def get_timestamp_with_workaround_for_timestamp_in_seconds_instead_of_millisecon return timestamp +def belongs_to_distro(pkg_data: dict, distro: str) -> bool: + """Identify renamed ROS packages by their distro-specific mutex build pin. + + A version-only mutex pin is not enough: different distros can publish the + same mutex version. Require the literal distro name in the build selector. + """ + name = pkg_data["name"] + if name.startswith(f"ros-{distro}-"): + return True + if name in ("ros-distro-mutex", "ros2-distro-mutex"): + return pkg_data["build"].split("_", 1)[0] == distro + if not name.startswith("ros2-"): + return False + + for dependency in pkg_data.get("depends", []): + if dependency.split(" ", 1)[0] != "ros2-distro-mutex": + continue + build = MatchSpec(dependency).build + if build and build.split("_", 1)[0] == distro: + return True + return False + + def main() -> None: # Parse command-line arguments parser = argparse.ArgumentParser( @@ -135,16 +159,10 @@ def main() -> None: # Filter packages that belong to the given distro # and are newer then the specified cutoff date - prefix = "ros-" + distro filtered_packages = { pkg_name: pkg_data for pkg_name, pkg_data in source_packages.items() - # This should cover both packages that start with 'ros-' - # '(ros|ros2)--mutex' packages whose build string contains - if ( - pkg_name.startswith(prefix) - or (pkg_data["name"].endswith("distro-mutex") and distro in pkg_data["build"]) - ) + if belongs_to_distro(pkg_data, distro) and ( get_timestamp_with_workaround_for_timestamp_in_seconds_instead_of_milliseconds( pkg_data diff --git a/scripts/test_copy_to_distro_specific_channel.py b/scripts/test_copy_to_distro_specific_channel.py new file mode 100644 index 00000000..ccae0597 --- /dev/null +++ b/scripts/test_copy_to_distro_specific_channel.py @@ -0,0 +1,55 @@ +import runpy +from pathlib import Path + +import pytest + +promotion = runpy.run_path(str(Path(__file__).with_name("copy-to-distro-specific-channel.py"))) +belongs_to_distro = promotion["belongs_to_distro"] + + +@pytest.mark.parametrize( + ("dependencies", "expected"), + [ + (["ros2-distro-mutex 0.10.* humble_*"], True), + ( + [ + "ros2-distro-mutex >=0.10.0,<0.11.0a0", + "ros2-distro-mutex 0.10.* humble_*", + ], + True, + ), + (["ros2-distro-mutex 0.10.* jazzy_*"], False), + (["ros2-distro-mutex >=0.10.0,<0.11.0a0"], False), + (["ros2-distro-mutex 0.10.* *"], False), + (["ros2-distro-mutex 0.10.* humble-other_*"], False), + (["ros2-rcl 16.*"], False), + ([], False), + ], +) +def test_renamed_package_requires_distro_build_pin(dependencies: list[str], expected: bool) -> None: + artifact = {"name": "ros2-rclcpp", "depends": dependencies} + assert belongs_to_distro(artifact, "humble") is expected + + +def test_legacy_packages_and_shims_remain_distro_specific() -> None: + shim = {"name": "ros-humble-rclcpp", "depends": ["ros2-rclcpp ==16.0.19"]} + assert belongs_to_distro(shim, "humble") + assert not belongs_to_distro(shim, "jazzy") + assert belongs_to_distro({"name": "ros-noetic-roscpp"}, "noetic") + assert not belongs_to_distro({"name": "ros-humble-other-rclcpp"}, "hum") + + +@pytest.mark.parametrize( + ("name", "build", "distro", "expected"), + [ + ("ros2-distro-mutex", "humble_20", "humble", True), + ("ros2-distro-mutex", "humble", "humble", True), + ("ros2-distro-mutex", "jazzy_20", "humble", False), + ("ros2-distro-mutex", "not-humble_20", "humble", False), + ("ros-distro-mutex", "noetic_1", "noetic", True), + ], +) +def test_mutex_build_identifies_its_distro( + name: str, build: str, distro: str, expected: bool +) -> None: + assert belongs_to_distro({"name": name, "build": build}, distro) is expected