Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 72 additions & 2 deletions colcon_cargo/package_augmentation/cargo.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from colcon_cargo.package_identification.cargo import read_cargo_toml
from colcon_core.dependency_descriptor import DependencyDescriptor
from colcon_core.package_augmentation import logger
from colcon_core.package_augmentation \
import PackageAugmentationExtensionPoint
from colcon_core.plugin_system import satisfies_version
Expand Down Expand Up @@ -122,6 +123,44 @@ def filter_dependency_list(dependencies, filter_out=None):
return filtered_dependencies.items()


def _next_caret(version):
# Drop pre-release
version = next(iter(version.split('-', 1)))
new_parts = []
for part in version.split('.'):
value = int(part)
if value != 0:
new_parts.append(str(value + 1))
break
new_parts.append(part)
else:
new_parts[-1] = '1'
return '.'.join(new_parts)


def _next_tilde(version):
# Drop pre-release
version = next(iter(version.split('-', 1)))
parts = version.split('.', 2)
if len(parts) == 1:
return str(int(parts[0]) + 1)
return parts[0] + '.' + str(int(parts[1]) + 1)


def _convert_wildcards(version):
while version.endswith('.*'):
version = version[:-2]
if version == '*':
return '>=0'
elif '*' in version or not version:
logger.warning(f"Ignoring unsupported version '{version}'")
return None
elif version[0].isnumeric():
return '~' + version
else:
return version


def create_dependency_descriptor(dependency_name, constraints, path):
"""
Create a dependency descriptor from a Cargo dependency specification.
Expand All @@ -133,6 +172,17 @@ def create_dependency_descriptor(dependency_name, constraints, path):
resolved
:rtype: DependencyDescriptor
"""
# The checking order matters, so we use tuple instead of dict
symbol_mappings = (
('^', 'version_gte', _next_caret),
('~', 'version_gte', _next_tilde),
('>=', 'version_gte', None),
('<=', 'version_lte', None),
('=', 'version_eq', None),
('>', 'version_gt', None),
('<', 'version_lt', None),
)

if isinstance(constraints, dict):
dep_path = constraints.get('path')
if dep_path:
Expand All @@ -141,12 +191,32 @@ def create_dependency_descriptor(dependency_name, constraints, path):
else:
source = constraints.get('git') or \
constraints.get('registry')
versions = constraints.get('version')
else:
source = None
versions = constraints

metadata = {
'origin': 'cargo',
'cargo_source': source,
}
# TODO: Interpret SemVer constraints and add appropriate constraint
# metadata. Handling arbitrary wildcards will be non-trivial.
for version in (versions or '').split(','):
# Ignore version metadata during comparison
version = next(iter(version.split('+', 1))).strip()
if '*' in version:
version = _convert_wildcards(version)
if not version:
continue
for symbol, mapping, version_lt in symbol_mappings:
if version.startswith(symbol):
version = version[len(symbol):].lstrip()
metadata[mapping] = version
if version_lt:
metadata['version_lt'] = version_lt(version)
break
else:
# Bare versions are the same as caret
metadata['version_gte'] = version
metadata['version_lt'] = _next_caret(version)

return DependencyDescriptor(dependency_name, metadata=metadata)
2 changes: 2 additions & 0 deletions test/spell_check.words
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ descs
easymov
etree
getroot
isnumeric
iterdir
linter
localhost
Expand Down Expand Up @@ -47,6 +48,7 @@ tomli
tomllib
toprettyxml
tostring
unittest
wildcards
workspaces
xmlstr
75 changes: 75 additions & 0 deletions test/test_package_augmentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Copyright 2026 Open Source Robotics Foundation, Inc.
# Licensed under the Apache License, Version 2.0

from pathlib import Path
from unittest.mock import patch

from colcon_cargo.package_augmentation.cargo \
import create_dependency_descriptor
import pytest


VERSION_CONSTRAINTS = {
None: {},
'': {},
'1.2.3': {'version_gte': '1.2.3', 'version_lt': '2'},
'1.2': {'version_gte': '1.2', 'version_lt': '2'},
'1': {'version_gte': '1', 'version_lt': '2'},
'0.2.3': {'version_gte': '0.2.3', 'version_lt': '0.3'},
'0.2': {'version_gte': '0.2', 'version_lt': '0.3'},
'0.0.3': {'version_gte': '0.0.3', 'version_lt': '0.0.4'},
'0.0': {'version_gte': '0.0', 'version_lt': '0.1'},
'0': {'version_gte': '0', 'version_lt': '1'},
'^ 1.2.3': {'version_gte': '1.2.3', 'version_lt': '2'},
'^ 1.2': {'version_gte': '1.2', 'version_lt': '2'},
'^ 1': {'version_gte': '1', 'version_lt': '2'},
'^ 0.2.3': {'version_gte': '0.2.3', 'version_lt': '0.3'},
'^ 0.2': {'version_gte': '0.2', 'version_lt': '0.3'},
'^ 0.0.3': {'version_gte': '0.0.3', 'version_lt': '0.0.4'},
'^ 0.0': {'version_gte': '0.0', 'version_lt': '0.1'},
'^ 0': {'version_gte': '0', 'version_lt': '1'},
'~ 1.2.3': {'version_gte': '1.2.3', 'version_lt': '1.3'},
'~ 1.2': {'version_gte': '1.2', 'version_lt': '1.3'},
'~ 1': {'version_gte': '1', 'version_lt': '2'},
'*': {'version_gte': '0'},
'1.*': {'version_gte': '1', 'version_lt': '2'},
'1.2.*': {'version_gte': '1.2', 'version_lt': '1.3'},
'>=1.*': {'version_gte': '1'},
'<1.2.*': {'version_lt': '1.2'},
'>= 1.2.3': {'version_gte': '1.2.3'},
'<= 1.2.3': {'version_lte': '1.2.3'},
'= 1.2.3': {'version_eq': '1.2.3'},
'> 1.2.3': {'version_gt': '1.2.3'},
'< 1.2.3': {'version_lt': '1.2.3'},
'>1.2.3, <=2.0': {'version_gt': '1.2.3', 'version_lte': '2.0'},
}


@pytest.mark.parametrize('constraints', list(VERSION_CONSTRAINTS.keys()))
def test_create_dependency_descriptor(constraints):
metadata = {
'origin': 'cargo',
**VERSION_CONSTRAINTS[constraints],
}

dep = create_dependency_descriptor('dependency', constraints, Path.cwd())
assert 'dependency' == dep.name
assert metadata == {k: v for k, v in dep.metadata.items() if k in metadata}


@pytest.mark.parametrize('constraints', ['1.*.3', '*.*.3', '*.2'])
def test_create_dependency_descriptor_unsupported(constraints):
with patch(
'colcon_cargo.package_augmentation.cargo.logger.warning',
) as log:
dep = create_dependency_descriptor(
'dependency', constraints, Path.cwd())

# No constraint in the metadata
assert 'dependency' == dep.name
assert not any(k.startswith('version_') for k in dep.metadata.keys())

# Single call to logger.warning()
assert log.call_count == 1
assert len(log.call_args[0]) >= 1
assert 'unsupported' in log.call_args[0][0]
Loading