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
2 changes: 2 additions & 0 deletions launch_ros/launch_ros/substitutions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

"""substitutions Module."""

from .ament_index import AmentIndexResource
from .executable_in_package import ExecutableInPackage
from .find_package import FindPackage
from .find_package import FindPackagePrefix
Expand All @@ -22,6 +23,7 @@


__all__ = [
'AmentIndexResource',
'ExecutableInPackage',
'FindPackage',
'FindPackagePrefix',
Expand Down
84 changes: 84 additions & 0 deletions launch_ros/launch_ros/substitutions/ament_index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2026 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Module for the AmentIndex substitution."""

from typing import Any, Dict, List, Sequence, Text, Tuple, Type

from ament_index_python.resources import get_resource

from launch.frontend import expose_substitution
from launch.launch_context import LaunchContext
from launch.some_substitutions_type import SomeSubstitutionsType
from launch.substitution import Substitution
from launch.substitutions import PathSubstitution, SubstitutionFailure
from launch.utilities import normalize_to_list_of_substitutions, perform_substitutions


@expose_substitution('ament-index-resource')
class AmentIndexResource(PathSubstitution):
"""
Substitution that looks up the path for the given resource type and name.

The resource is located using ament_index_python.
"""

def __init__(
self,
resource_type: SomeSubstitutionsType,
resource_name: SomeSubstitutionsType,
) -> None:
"""Create an AmentIndexResource substitution."""
super().__init__(self)

self.__type = normalize_to_list_of_substitutions(resource_type)
self.__name = normalize_to_list_of_substitutions(resource_name)

@classmethod
def parse(
cls, data: Sequence[SomeSubstitutionsType]
) -> Tuple[Type['AmentIndexResource'], Dict[str, Any]]:
"""Parse an AmentIndexResource subtitution."""
if len(data) != 2:
raise TypeError('ament-index-resource expects 2 arguments')
kwargs = {
'resource_type': data[0],
'resource_name': data[1],
}
return cls, kwargs

@property
def resource_type(self) -> List[Substitution]:
"""Getter for resource_type."""
return self.__type

@property
def resource_name(self) -> List[Substitution]:
"""Getter for resource_name."""
return self.__name

def describe(self) -> Text:
"""Return a description of this substitution as a string."""
type_str = ' + '.join(sub.describe() for sub in self.resource_type)
name_str = ' + '.join(sub.describe() for sub in self.resource_name)
return f'AmentIndexResource({type_str}, {name_str})'

def perform(self, context: LaunchContext) -> Text:
"""Perform the substitution by looking up the resource in the ament index."""
resource_type = perform_substitutions(context, self.resource_type)
resource_name = perform_substitutions(context, self.resource_name)
try:
return get_resource(resource_type, resource_name)[1]
except Exception as e:
raise SubstitutionFailure(e)
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Copyright 2026 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import io
from pathlib import Path
import textwrap

from launch import LaunchService
from launch.frontend import Parser
from launch.substitutions import SubstitutionFailure
from launch.utilities import perform_substitutions

import pytest


def test_ament_index_resource_substitution_yaml():
yaml_file = textwrap.dedent(
r"""
launch:
- let:
name: launch_ros_prefix
value: $(ament-index-resource packages launch_ros)
"""
)
with io.StringIO(yaml_file) as f:
check_ament_index_resource_substitution(f)


def test_ament_index_resource_substitution_xml():
xml_file = textwrap.dedent(
r"""
<launch>
<let name="launch_ros_prefix" value="$(ament-index-resource packages launch_ros)"/>
</launch>
"""
)
with io.StringIO(xml_file) as f:
check_ament_index_resource_substitution(f)


def check_ament_index_resource_substitution(file):
root_entity, parser = Parser.load(file)
ld = parser.parse_description(root_entity)
ls = LaunchService()
ls.include_launch_description(ld)
assert 0 == ls.run()

def perform(substitution):
return perform_substitutions(ls.context, substitution)

let, = ld.describe_sub_entities()
assert perform(let.name) == 'launch_ros_prefix'
assert Path(perform(let.value)).is_dir()


def test_ament_index_resource_substitution_yaml_nonexistent_name():
yaml_file = textwrap.dedent(
r"""
launch:
- let:
name: bad_name
value: $(ament-index-resource packages package_that_certainly_does_not_exist)
"""
)
with io.StringIO(yaml_file) as f:
check_ament_index_resource_substitution_failure(f)


def test_ament_index_resource_substitution_yaml_nonexistent_type():
yaml_file = textwrap.dedent(
r"""
launch:
- let:
name: bad_type
value: $(ament-index-resource resource_type_that_does_not_exist launch_ros)
"""
)
with io.StringIO(yaml_file) as f:
check_ament_index_resource_substitution_failure(f)


def test_ament_index_resource_substitution_xml_nonexistent_name():
xml_file = textwrap.dedent(
r"""
<launch>
<let name="launch_ros_prefix"
value="$(ament-index-resource packages package_that_certainly_does_not_exist)"/>
</launch>
"""
)
with io.StringIO(xml_file) as f:
check_ament_index_resource_substitution_failure(f)


def test_ament_index_resource_substitution_xml_nonexistent_type():
xml_file = textwrap.dedent(
r"""
<launch>
<let name="launch_ros_prefix"
value="$(ament-index-resource resource_type_that_does_not_exist launch_ros)"/>
</launch>
"""
)
with io.StringIO(xml_file) as f:
check_ament_index_resource_substitution_failure(f)


def check_ament_index_resource_substitution_failure(file):
root_entity, parser = Parser.load(file)
ld = parser.parse_description(root_entity)
ls = LaunchService()
ls.include_launch_description(ld)
assert 0 != ls.run()

def perform(substitution):
return perform_substitutions(ls.context, substitution)

let, = ld.describe_sub_entities()
with pytest.raises(SubstitutionFailure):
perform(let.value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Copyright 2026 Open Source Robotics Foundation, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Test for the AmentIndexResource substitution."""

from pathlib import Path

from launch import LaunchContext
from launch.substitutions import SubstitutionFailure
from launch_ros.substitutions import AmentIndexResource

import pytest


def test_ament_index_resource():
sub = AmentIndexResource('packages', 'launch_ros')
context = LaunchContext()
resource_path = Path(sub.perform(context))
assert resource_path.is_dir()


def test_ament_index_resource_nonexistent_name():
sub = AmentIndexResource('packages', 'package_that_certainly_does_not_exist')
context = LaunchContext()
with pytest.raises(SubstitutionFailure):
sub.perform(context)


def test_ament_index_resource_nonexistent_type():
sub = AmentIndexResource('resource_type_that_certainly_does_not_exist', 'launch_ros')
context = LaunchContext()
with pytest.raises(SubstitutionFailure):
sub.perform(context)