diff --git a/launch_ros/launch_ros/substitutions/__init__.py b/launch_ros/launch_ros/substitutions/__init__.py
index c3a2d133..a224c888 100644
--- a/launch_ros/launch_ros/substitutions/__init__.py
+++ b/launch_ros/launch_ros/substitutions/__init__.py
@@ -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
@@ -22,6 +23,7 @@
__all__ = [
+ 'AmentIndexResource',
'ExecutableInPackage',
'FindPackage',
'FindPackagePrefix',
diff --git a/launch_ros/launch_ros/substitutions/ament_index.py b/launch_ros/launch_ros/substitutions/ament_index.py
new file mode 100644
index 00000000..e2510908
--- /dev/null
+++ b/launch_ros/launch_ros/substitutions/ament_index.py
@@ -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)
diff --git a/test_launch_ros/test/test_launch_ros/frontend/test_ament_index_substitution_frontend.py b/test_launch_ros/test/test_launch_ros/frontend/test_ament_index_substitution_frontend.py
new file mode 100644
index 00000000..a19b3684
--- /dev/null
+++ b/test_launch_ros/test/test_launch_ros/frontend/test_ament_index_substitution_frontend.py
@@ -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"""
+
+
+
+ """
+ )
+ 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"""
+
+
+
+ """
+ )
+ 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"""
+
+
+
+ """
+ )
+ 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)
diff --git a/test_launch_ros/test/test_launch_ros/substitutions/test_ament_index.py b/test_launch_ros/test/test_launch_ros/substitutions/test_ament_index.py
new file mode 100644
index 00000000..3e2ea17e
--- /dev/null
+++ b/test_launch_ros/test/test_launch_ros/substitutions/test_ament_index.py
@@ -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)