From 7ec34448a1f06103df28a448533988dbcf1d7c23 Mon Sep 17 00:00:00 2001 From: Tobias Fischer Date: Fri, 9 Oct 2026 18:17:11 +1000 Subject: [PATCH 1/2] Mutex: honour mutex_package.build_number A new build of the mutex at the same version (mutex_package.build_number) was never generated: the skip-existing check compared the published mutex with the distribution's build_number, and write_recipe overwrote the recipe's build number with it too (build string lyrical_28, number 27). Both now use the mutex's own build number; other packages are unchanged. Co-Authored-By: Claude Opus 5.5 --- vinca/main.py | 12 ++++++- vinca/template.py | 6 +++- vinca/test_mutex_build_number.py | 58 ++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 vinca/test_mutex_build_number.py diff --git a/vinca/main.py b/vinca/main.py index 8aa17e2..96d210a 100644 --- a/vinca/main.py +++ b/vinca/main.py @@ -341,6 +341,16 @@ def conda_index_shadowed_packages(distro, vinca_conf): return names +def expected_build_number(default_build_number, pkg_name, vinca_conf): + """The build number a package of this configuration gets: the mutex has its own + (mutex_package.build_number, e.g. a new build at the same version), the others + the distribution's unless pkg_additional_info.yaml overrides it.""" + mutex_config = parse_mutex_package_config(vinca_conf) + if mutex_config and pkg_name == mutex_config["name"]: + return mutex_config["build_number"] + return get_pkg_build_number(default_build_number, pkg_name, vinca_conf) + + def get_selected_packages(distro, vinca_conf): selected_packages = set() skipped_packages = set() @@ -742,7 +752,7 @@ def main(): for _, pkg in all_pkgs.items(): is_built = False if selected_bn is not None: - pkg_build_number = get_pkg_build_number( + pkg_build_number = expected_build_number( selected_bn, pkg["name"], vinca_conf ) if pkg["build_number"] == pkg_build_number: diff --git a/vinca/template.py b/vinca/template.py index 6873139..b4ab96c 100644 --- a/vinca/template.py +++ b/vinca/template.py @@ -131,8 +131,12 @@ def write_recipe(source, outputs, vinca_conf, distro, single_file=True): o, distro, vinca_conf ) + # an output's own build number (e.g. mutex_package.build_number) wins + # over the distribution's; pkg_additional_info.yaml overrides both meta["build"]["number"] = get_pkg_build_number( - vinca_conf.get("build_number", 0), package_name, vinca_conf + o.get("build", {}).get("number", vinca_conf.get("build_number", 0)), + package_name, + vinca_conf, ) meta["build"]["post_process"] = post_process_items diff --git a/vinca/test_mutex_build_number.py b/vinca/test_mutex_build_number.py new file mode 100644 index 0000000..87e5c4a --- /dev/null +++ b/vinca/test_mutex_build_number.py @@ -0,0 +1,58 @@ +"""mutex_package.build_number: a new build of the mutex at the same version.""" + +from unittest.mock import Mock + +import yaml + +from vinca.main import expected_build_number +from vinca.mutex import generate_mutex_package_recipe +from vinca.template import write_recipe + + +def _conf(**extra): + conf = { + "ros_distro": "lyrical", + "build_number": 27, + "mutex_package": { + "name": "ros2-distro-mutex", + "version": "0.21.0", + "upper_bound": "x.x", + "run_constraints": [], + "build_number": 28, + }, + "_pkg_additional_info": {"rclcpp": {"build_number": 30}}, + "_tests": {}, + } + conf.update(extra) + return conf + + +def test_expected_build_number_of_the_mutex_is_its_own(): + assert expected_build_number(27, "ros2-distro-mutex", _conf()) == 28 + + +def test_expected_build_number_of_other_packages(): + conf = _conf() + assert expected_build_number(27, "ros2-rclcpp", conf) == 30 + assert expected_build_number(27, "ros2-std-msgs", conf) == 27 + + +def test_mutex_without_own_build_number_uses_the_distribution_one(): + conf = _conf() + del conf["mutex_package"]["build_number"] + assert expected_build_number(27, "ros2-distro-mutex", conf) == 27 + + +def test_mutex_recipe_keeps_its_build_number(tmp_path, monkeypatch): + monkeypatch.chdir(tmp_path) + conf = _conf() + distro = Mock() + distro.name = "lyrical" + recipe = generate_mutex_package_recipe(conf, distro) + write_recipe({}, [recipe], conf, distro, single_file=False) + meta = yaml.safe_load( + (tmp_path / "recipes" / "ros2-distro-mutex" / "recipe.yaml").read_text() + ) + assert meta["build"]["number"] == 28 + assert meta["build"]["string"] == "lyrical_28" + assert meta["requirements"]["run_constraints"] == [] From a8fdf51f844d9107016c69126c6a4aa83b23e2f3 Mon Sep 17 00:00:00 2001 From: Tobias Fischer Date: Sat, 10 Oct 2026 14:05:48 +1000 Subject: [PATCH 2/2] Mutex: run_constraints is optional A mutex only has to keep one distribution per environment; constraining other packages from it is optional, so mutex_package no longer requires the key, and the recipe lists run_constraints only when there are any. Co-Authored-By: Claude Opus 5.5 --- vinca/mutex.py | 9 +++++++-- vinca/test_mutex_build_number.py | 13 +++++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/vinca/mutex.py b/vinca/mutex.py index b69a76f..8efd305 100644 --- a/vinca/mutex.py +++ b/vinca/mutex.py @@ -14,7 +14,8 @@ from vinca.distro import Distro -_REQUIRED_FIELDS = ("name", "version", "upper_bound", "run_constraints") +# run_constraints is optional: a mutex only has to keep one distribution per environment +_REQUIRED_FIELDS = ("name", "version", "upper_bound") def parse_mutex_package_config(vinca_conf: dict[str, Any]) -> dict[str, Any] | None: @@ -102,7 +103,11 @@ def generate_mutex_package_recipe( "script": "", }, "requirements": { - "run_constraints": mutex_config["run_constraints"], + **( + {"run_constraints": mutex_config["run_constraints"]} + if mutex_config.get("run_constraints") + else {} + ), "run_exports": { "weak": [ f"${{{{ pin_subpackage('{name}', upper_bound='{mutex_config['upper_bound']}') }}}}" diff --git a/vinca/test_mutex_build_number.py b/vinca/test_mutex_build_number.py index 87e5c4a..6232e98 100644 --- a/vinca/test_mutex_build_number.py +++ b/vinca/test_mutex_build_number.py @@ -17,7 +17,6 @@ def _conf(**extra): "name": "ros2-distro-mutex", "version": "0.21.0", "upper_bound": "x.x", - "run_constraints": [], "build_number": 28, }, "_pkg_additional_info": {"rclcpp": {"build_number": 30}}, @@ -55,4 +54,14 @@ def test_mutex_recipe_keeps_its_build_number(tmp_path, monkeypatch): ) assert meta["build"]["number"] == 28 assert meta["build"]["string"] == "lyrical_28" - assert meta["requirements"]["run_constraints"] == [] + # no (empty) run_constraints in the recipe + assert "run_constraints" not in meta["requirements"] + + +def test_mutex_with_run_constraints_keeps_them(): + conf = _conf() + conf["mutex_package"]["run_constraints"] = ["libfoo 1.*"] + distro = Mock() + distro.name = "lyrical" + recipe = generate_mutex_package_recipe(conf, distro) + assert recipe["requirements"]["run_constraints"] == ["libfoo 1.*"]