From b5fbb5612668bff64e3b88cff0422c1c29756bff Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Mon, 23 Feb 2026 15:08:28 -0600 Subject: [PATCH 1/4] Optionally break version-incompatible dependencies This flag is intended to support scenarios where a developer intentionally includes packages in a workspace where the underlying build system will not use dependencies which have incompatible versions. Because this scenario is somewhat of a deviation from the established expectations of colcon behavior, we'll print a warning to the developer (at least for now). At this stage in package enumeration, we may encounter multiple packages with a given name. This change also re-structures the enumeration so that all packages with a given name can be checked, where previously only a single instance of the package was checked. --- .../check_dependency_constraint.py | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/colcon_package_information/package_augmentation/check_dependency_constraint.py b/colcon_package_information/package_augmentation/check_dependency_constraint.py index d3357b3..c2d6463 100644 --- a/colcon_package_information/package_augmentation/check_dependency_constraint.py +++ b/colcon_package_information/package_augmentation/check_dependency_constraint.py @@ -29,18 +29,37 @@ def __init__(self): # noqa: D107 def augment_packages( # noqa: D102 self, descs, *, additional_argument_names=None ): - descs_dict = {desc.name: desc for desc in descs} - for name, desc in descs_dict.items(): + descs_dict = {} + for desc in descs: + descs_dict.setdefault(desc.name, []).append(desc) + for desc in descs: deps = desc.get_dependencies() for dep in deps: # skip dependencies which don't have a descriptor if dep.name not in descs_dict: continue - self._check_version_constraints( - desc, dep, descs_dict[dep.name]) + miss = all( + self._check_version_constraints(desc, dep, dep_desc) + for dep_desc in descs_dict.get(dep.name, ())) + if miss and dep.metadata.get('skip_incompatible'): + logger.warning( + 'Dropping version-incompatible dependency on ' + + dep.name + ' from ' + desc.name) + for category in dep.metadata.get('categories') or (): + desc.dependencies[category].remove(dep) def _check_version_constraints(self, desc, dep, dep_desc): + """ + Check dependency version constraints against another package. + + :param desc: The package declaring the dependency being checked + :param dep: The dependency being checked + :param dep_desc: The package to which the dependency refers + + :returns: True if one or more constraints fail, False if all + constraints pass, and None if no constraints were checked. + """ # if the dependency descriptor doesn't have a version there is nothing # to compare to if 'version' not in dep_desc.metadata: @@ -65,6 +84,7 @@ def gte(a, b): 'version_gte': (gte, 'greater than or equal to'), 'version_gt': (operator.gt, 'greater than'), } + miss = None for key, value in dep.metadata.items(): # only consider version operator metadata if key not in operators: @@ -81,12 +101,15 @@ def gte(a, b): continue if not op(dep_version, version_constraint): + miss = miss or True logger.warning( desc.name + ' depends on ' + dep.name + ' which has version ' + dep_desc.metadata['version'] + ' but expects it to be ' + msg + ' ' + value) else: + miss = miss or False logger.debug( desc.name + ' depends on ' + dep.name + ' which has version ' + dep_desc.metadata['version'] + ' which satisfies to be ' + msg + ' ' + value) + return miss From 6985d996a6abe5afe4cc52ddc592a1857c1ba9e8 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Tue, 3 Mar 2026 14:28:42 -0600 Subject: [PATCH 2/4] Simplify miss flag Co-authored-by: Jorge J. Perez --- .../package_augmentation/check_dependency_constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colcon_package_information/package_augmentation/check_dependency_constraint.py b/colcon_package_information/package_augmentation/check_dependency_constraint.py index c2d6463..4959659 100644 --- a/colcon_package_information/package_augmentation/check_dependency_constraint.py +++ b/colcon_package_information/package_augmentation/check_dependency_constraint.py @@ -101,7 +101,7 @@ def gte(a, b): continue if not op(dep_version, version_constraint): - miss = miss or True + miss = True logger.warning( desc.name + ' depends on ' + dep.name + ' which has version ' + dep_desc.metadata['version'] + From bd2adb2f0fd3d6c681803bbd5d74a796c2089c67 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Tue, 24 Mar 2026 11:47:36 -0500 Subject: [PATCH 3/4] Add some tests --- test/test_check_dependency_constraint.py | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/test/test_check_dependency_constraint.py b/test/test_check_dependency_constraint.py index 4e7a9a9..a75034d 100644 --- a/test/test_check_dependency_constraint.py +++ b/test/test_check_dependency_constraint.py @@ -116,3 +116,47 @@ def test_dependency_invalid_version_operator(): extension = CheckDependencyConstraintPackageAugmentation() extension.augment_packages({pkg_a, pkg_b}) + + +@pytest.mark.parametrize( + 'a_versions,skip_incompatible,dep_categories,expect_present', [ + (['1.0'], True, {'build'}, False), + (['1.0', '3.0'], True, {'build'}, False), + (['1.0', '2.0'], True, {'build'}, True), + (['1.0'], False, {'build'}, True), + (['1.0'], True, {'build', 'run'}, False), + ([None], True, {'build'}, True), + ]) +def test_skip_incompatible( + a_versions, skip_incompatible, dep_categories, expect_present +): + descs = set() + for i, v in enumerate(a_versions): + pkg_a = PackageDescriptor(f'/tmp/pkg_a_{i}') + pkg_a.name = 'pkg_a' + if v is not None: + pkg_a.metadata['version'] = v + descs.add(pkg_a) + + metadata = { + 'version_eq': '2.0', + 'skip_incompatible': skip_incompatible, + 'categories': dep_categories, + } + pkg_a_dep = DependencyDescriptor('pkg_a', metadata=metadata) + + pkg_b = PackageDescriptor('/tmp/pkg_b') + pkg_b.name = 'pkg_b' + for cat in dep_categories: + pkg_b.dependencies[cat] = {pkg_a_dep} + + descs.add(pkg_b) + + extension = CheckDependencyConstraintPackageAugmentation() + extension.augment_packages(descs) + + for cat in dep_categories: + if expect_present: + assert pkg_a_dep in pkg_b.dependencies[cat] + else: + assert pkg_a_dep not in pkg_b.dependencies.get(cat, set()) From 76beea903ab9bd5fbac340e77434efbd2aedb3be Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Tue, 24 Mar 2026 11:55:25 -0500 Subject: [PATCH 4/4] Use `discard()` instead of `remove()` --- .../package_augmentation/check_dependency_constraint.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/colcon_package_information/package_augmentation/check_dependency_constraint.py b/colcon_package_information/package_augmentation/check_dependency_constraint.py index 4959659..429e0ff 100644 --- a/colcon_package_information/package_augmentation/check_dependency_constraint.py +++ b/colcon_package_information/package_augmentation/check_dependency_constraint.py @@ -47,7 +47,7 @@ def augment_packages( # noqa: D102 'Dropping version-incompatible dependency on ' + dep.name + ' from ' + desc.name) for category in dep.metadata.get('categories') or (): - desc.dependencies[category].remove(dep) + desc.dependencies[category].discard(dep) def _check_version_constraints(self, desc, dep, dep_desc): """