Skip to content

Commit

Permalink
Implement separate augmentation phase for Cargo packages (#48)
Browse files Browse the repository at this point in the history
Co-authored-by: Luca Della Vedova <[email protected]>
  • Loading branch information
cottsay and luca-della-vedova authored Dec 9, 2024
1 parent 48a4245 commit a6ab446
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 77 deletions.
Empty file.
89 changes: 89 additions & 0 deletions colcon_cargo/package_augmentation/cargo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2024 Open Source Robotics Foundation, Inc.
# Licensed under the Apache License, Version 2.0

from colcon_cargo.package_identification.cargo import read_cargo_toml
from colcon_core.dependency_descriptor import DependencyDescriptor
from colcon_core.package_augmentation \
import PackageAugmentationExtensionPoint
from colcon_core.plugin_system import satisfies_version


class CargoPackageAugmentation(PackageAugmentationExtensionPoint):
"""Augment cargo packages with information from Cargo.toml files."""

def __init__(self): # noqa: D107
super().__init__()
satisfies_version(
PackageAugmentationExtensionPoint.EXTENSION_POINT_VERSION,
'^1.0')

def augment_package( # noqa: D102
self, metadata, *, additional_argument_names=None
):
if metadata.type != 'cargo':
return

self._augment_package(
metadata, additional_argument_names=additional_argument_names)

def _augment_package(
self, metadata, *, additional_argument_names=None
):
cargo_toml = metadata.path / 'Cargo.toml'
if not cargo_toml.is_file():
return

content = read_cargo_toml(cargo_toml)
package = content.get('package', {})
if not package:
return

version = package.get('version', '0.0.0')
if not metadata.metadata.get('version'):
metadata.metadata['version'] = version

dependencies = extract_dependencies(content)
for k, v in dependencies.items():
metadata.dependencies[k] |= v

authors = package.get('authors', ())
if authors:
metadata.metadata.setdefault('maintainers', [])
metadata.metadata['maintainers'] += authors


def extract_dependencies(content):
"""
Get the dependencies of a Cargo package.
:param content: The dictionary content of the Cargo.toml file
:returns: The dependencies
:rtype: dict(string, set(DependencyDescriptor))
"""
name = content.get('name')
depends = {
create_dependency_descriptor(k, v)
for k, v in content.get('dependencies', {}).items()
if k != name
}
return {
'build': depends,
'run': depends,
}


def create_dependency_descriptor(name, constraints):
"""
Create a dependency descriptor from a Cargo dependency specification.
:param name: The name of the dependee
:param constraints: The dependency constraints, either a string or
a dict
:rtype: DependencyDescriptor
"""
metadata = {
'origin': 'cargo',
}
# TODO: Interpret SemVer constraints and add appropriate constraint
# metadata. Handling arbitrary wildcards will be non-trivial.
return DependencyDescriptor(name, metadata=metadata)
95 changes: 19 additions & 76 deletions colcon_cargo/package_identification/cargo.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from toml import TomlDecodeError as TOMLDecodeError

logger = colcon_logger.getChild(__name__)
WORKSPACE = 'WORKSPACE'


class CargoPackageIdentification(PackageIdentificationExtensionPoint):
Expand All @@ -39,90 +38,34 @@ def identify(self, metadata): # noqa: D102
if not cargo_toml.is_file():
return

data = extract_data(cargo_toml)
if data == WORKSPACE:
content = read_cargo_toml(cargo_toml)
if 'workspace' in content:
logger.debug(
f'Ignoring unsupported Cargo Workspace at {metadata.path}')
return
if not data:

package = content.get('package', {})
name = package.get('name')
if not name and not metadata.name:
raise RuntimeError(
'Failed to extract Rust package information from "%s"'
% cargo_toml.absolute())
f"Failed to extract project name from '{cargo_toml}'")

metadata.type = 'cargo'
if metadata.name is None:
metadata.name = data['name']
metadata.metadata['version'] = data['version']

metadata.dependencies['build'] |= data['depends']
metadata.dependencies['run'] |= data['depends']
metadata.name = name


def extract_data(cargo_toml):
def read_cargo_toml(cargo_toml):
"""
Extract the project name and dependencies from a Cargo.toml file.
Read the contents of a Cargo.toml file.
:param Path cargo_toml: The path of the Cargo.toml file
:rtype: dict
:param cargo_toml: Path to a Cargo.toml file to read
:returns: Dictionary containing the processed content of the Cargo.toml
:raises ValueError: if the content of Cargo.toml is not valid
"""
content = {}
try:
with cargo_toml.open('rb') as f:
content = toml_loads(f.read().decode())
except TOMLDecodeError:
logger.error('Decoding error when processing "%s"'
% cargo_toml.absolute())
return

if 'workspace' in content.keys():
return WORKSPACE

# set the project name - fall back to use the directory name
data = {}
toml_name_attr = extract_project_name(content)
data['name'] = toml_name_attr if toml_name_attr is not None else \
cargo_toml.parent.name
data['version'] = extract_project_version(content)

depends = extract_dependencies(content)
# exclude self references
data['depends'] = set(depends) - {data['name']}

return data


def extract_project_name(content):
"""
Extract the Cargo project name from the Cargo.toml file.
:param str content: The Cargo.toml parsed dictionary
:returns: The project name, otherwise None
:rtype: str
"""
try:
return content['package']['name']
except KeyError:
return None


def extract_project_version(content):
"""
Extract the Cargo project version from the Cargo.toml file.
:param str content: The Cargo.toml parsed dictionary
:returns: The project version, otherwise None
:rtype: str
"""
try:
return content['package']['version']
except KeyError:
return None


def extract_dependencies(content):
"""
Extract the dependencies from the Cargo.toml file.
:param str content: The Cargo.toml parsed dictionary
:returns: The dependencies name
:rtype: list
"""
return list(content.get('dependencies', {}).keys())
return toml_loads(f.read().decode())
except TOMLDecodeError as e:
raise ValueError(
f"Failed to parse Cargo.toml file at '{cargo_toml}'") from e
2 changes: 2 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ markers =
[options.entry_points]
colcon_argcomplete.argcomplete_completer =
cargo_args = colcon_cargo.argcomplete_completer.cargo_args:CargoArgcompleteCompleter
colcon_core.package_augmentation =
cargo = colcon_cargo.package_augmentation.cargo:CargoPackageAugmentation
colcon_core.package_identification =
cargo = colcon_cargo.package_identification.cargo:CargoPackageIdentification
colcon_core.task.build =
Expand Down
3 changes: 2 additions & 1 deletion test/rust-pure-library/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
[package]
name = "rust-pure-library"
version = "0.1.0"
authors = ["Luca Della Vedova<[email protected]>"]
authors = ["Test<[email protected]>"]
edition = "2018"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
either = "1.13.0"
1 change: 1 addition & 0 deletions test/rust-sample-package/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
either = "1.13.0"
2 changes: 2 additions & 0 deletions test/spell_check.words
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ asyncio
autouse
colcon
completers
dependee
deps
easymov
etree
Expand Down Expand Up @@ -41,4 +42,5 @@ tomli
tomllib
toprettyxml
tostring
wildcards
xmlstr
16 changes: 16 additions & 0 deletions test/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from types import SimpleNamespace
import xml.etree.ElementTree as eTree

from colcon_cargo.package_augmentation.cargo import CargoPackageAugmentation
from colcon_cargo.package_identification.cargo import CargoPackageIdentification # noqa: E501
from colcon_cargo.task.cargo.build import CargoBuildTask
from colcon_cargo.task.cargo.test import CargoTestTask
Expand Down Expand Up @@ -43,6 +44,21 @@ def test_package_identification():
assert desc.name == TEST_PACKAGE_NAME


def test_package_augmentation():
cpi = CargoPackageIdentification()
aug = CargoPackageAugmentation()
desc = PackageDescriptor(pure_library_path)
cpi.identify(desc)
aug.augment_package(desc)
print(desc)
assert desc.metadata['version'] == '0.1.0'
assert len(desc.metadata['maintainers']) == 1
assert desc.metadata['maintainers'][0] == 'Test<[email protected]>'
assert len(desc.dependencies['build']) == 1
assert 'either' in desc.dependencies['build']
assert desc.dependencies['run'] == desc.dependencies['build']


@pytest.mark.skipif(
not shutil.which('cargo'),
reason='Rust must be installed to run this test')
Expand Down

0 comments on commit a6ab446

Please sign in to comment.