diff --git a/rosidl_cli/rosidl_cli/cli.py b/rosidl_cli/rosidl_cli/cli.py index ce729a816..e7a65e81f 100644 --- a/rosidl_cli/rosidl_cli/cli.py +++ b/rosidl_cli/rosidl_cli/cli.py @@ -17,6 +17,7 @@ from typing import Any, List, Union from rosidl_cli.command.generate import GenerateCommand +from rosidl_cli.command.hash import HashCommand from rosidl_cli.command.translate import TranslateCommand from rosidl_cli.common import get_first_line_doc @@ -24,8 +25,8 @@ def add_subparsers( parser: argparse.ArgumentParser, cli_name: str, - commands: List[Union[GenerateCommand, TranslateCommand]] -) -> argparse._SubParsersAction[argparse.ArgumentParser]: + commands: List[Union[GenerateCommand, HashCommand, TranslateCommand]] +) -> argparse._SubParsersAction: """ Create argparse subparser for each command. @@ -79,8 +80,8 @@ def main() -> Union[str, signal.Signals, Any]: formatter_class=argparse.RawDescriptionHelpFormatter ) - commands: List[Union[GenerateCommand, TranslateCommand]] = \ - [GenerateCommand(), TranslateCommand()] + commands: List[Union[GenerateCommand, TranslateCommand, HashCommand]] = \ + [GenerateCommand(), TranslateCommand(), HashCommand()] # add arguments for command extension(s) add_subparsers( diff --git a/rosidl_cli/rosidl_cli/command/generate/__init__.py b/rosidl_cli/rosidl_cli/command/generate/__init__.py index b12b946c1..cf27d6cd4 100644 --- a/rosidl_cli/rosidl_cli/command/generate/__init__.py +++ b/rosidl_cli/rosidl_cli/command/generate/__init__.py @@ -39,6 +39,10 @@ def add_arguments(self, parser: argparse.ArgumentParser) -> None: '-ts', '--type-support', metavar='TYPESUPPORT', dest='typesupports', action='append', default=[], help='Target type supports for generation.') + parser.add_argument( + '-td', '--type-description-file', metavar='PATH', + dest='type_description_files', action='append', default=[], + help='Target type descriptions for generation.') parser.add_argument( '-I', '--include-path', type=pathlib.Path, metavar='PATH', dest='include_paths', action='append', default=[], @@ -58,5 +62,6 @@ def main(self, *, args: argparse.Namespace) -> None: include_paths=args.include_paths, output_path=args.output_path, types=args.types, - typesupports=args.typesupports + typesupports=args.typesupports, + type_description_files=args.type_description_files ) diff --git a/rosidl_cli/rosidl_cli/command/generate/api.py b/rosidl_cli/rosidl_cli/command/generate/api.py index ff7edbbcb..0c1b79e73 100644 --- a/rosidl_cli/rosidl_cli/command/generate/api.py +++ b/rosidl_cli/rosidl_cli/command/generate/api.py @@ -12,13 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import inspect import os import pathlib -from typing import List, Optional +from typing import Any, Callable, Dict, List, Optional -from .extensions import GenerateCommandExtension -from .extensions import load_type_extensions -from .extensions import load_typesupport_extensions +from .extensions import GenerateCommandExtension, load_type_extensions, load_typesupport_extensions def generate( @@ -28,7 +27,8 @@ def generate( include_paths: Optional[List[str]] = None, output_path: Optional[pathlib.Path] = None, types: Optional[List[str]] = None, - typesupports: Optional[List[str]] = None + typesupports: Optional[List[str]] = None, + type_description_files: Optional[List[str]] = None ) -> List[List[str]]: """ Generate source code from interface definition files. @@ -59,6 +59,7 @@ def generate( source code files, defaults to the current working directory :param types: optional list of type representations to generate :param typesupports: optional list of type supports to generate + :param type_description_files: Optional list of paths to type description files :returns: list of lists of paths to generated source code files, one group per type or type support extension invoked """ @@ -87,15 +88,39 @@ def generate( else: os.makedirs(output_path, exist_ok=True) - if len(extensions) > 1: - return [ + def extra_kwargs(func: Callable, **kwargs: Any) -> Dict[str, Any]: + matched_kwargs = {} + signature = inspect.signature(func) + for name, value in kwargs.items(): + if name in signature.parameters: + if signature.parameters[name].kind not in [ + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.VAR_KEYWORD + ]: + matched_kwargs[name] = value + return matched_kwargs + + generated_files = [] + if len(extensions) == 1: + extension = extensions[0] + generated_files.append( extension.generate( package_name, interface_files, include_paths, - output_path=output_path / extension.name) - for extension in extensions - ] - - return [extensions[0].generate( - package_name, interface_files, - include_paths, output_path - )] + output_path=output_path, + **extra_kwargs(extension.generate, type_description_files=type_description_files) + ) + ) + else: + for extension in extensions: + generated_files.append( + extension.generate( + package_name, interface_files, include_paths, + output_path=output_path / extension.name, + **extra_kwargs( + extension.generate, + type_description_files=type_description_files + ) + ) + ) + return generated_files diff --git a/rosidl_cli/rosidl_cli/command/generate/extensions.py b/rosidl_cli/rosidl_cli/command/generate/extensions.py index a89630d71..10ef37f83 100644 --- a/rosidl_cli/rosidl_cli/command/generate/extensions.py +++ b/rosidl_cli/rosidl_cli/command/generate/extensions.py @@ -32,7 +32,8 @@ def generate( package_name: str, interface_files: List[str], include_paths: List[str], - output_path: Path + output_path: Path, + type_description_files: Optional[List[str]] = None ) -> List[str]: """ Generate source code. @@ -46,6 +47,7 @@ def generate( :param include_paths: list of paths to include dependency interface definition files from. :param output_path: path to directory to hold generated source code files + :param type_description_files: Optional list of paths to type description files :returns: list of paths to generated source files """ raise NotImplementedError() diff --git a/rosidl_cli/rosidl_cli/command/hash/__init__.py b/rosidl_cli/rosidl_cli/command/hash/__init__.py new file mode 100644 index 000000000..0c5697cec --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/hash/__init__.py @@ -0,0 +1,51 @@ +# Copyright 2021 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 pathlib + +from rosidl_cli.command import Command + +from .api import generate_type_hashes + + +class HashCommand(Command): + """Generate type description hashes from interface definition files.""" + + name = 'hash' + + def add_arguments(self, parser): + parser.add_argument( + '-o', '--output-path', metavar='PATH', + type=pathlib.Path, default=None, + help=('Path to directory to hold generated ' + "source code files. Defaults to '.'.")) + parser.add_argument( + '-I', '--include-path', type=pathlib.Path, metavar='PATH', + dest='include_paths', action='append', default=[], + help='Paths to include dependency interface definition files from.') + parser.add_argument( + 'package_name', help='Name of the package to generate code for') + parser.add_argument( + 'interface_files', metavar='interface_file', nargs='+', + help=('Relative path to an interface definition file. ' + "If prefixed by another path followed by a colon ':', " + 'path resolution is performed against such path.')) + + def main(self, *, args): + generate_type_hashes( + package_name=args.package_name, + interface_files=args.interface_files, + include_paths=args.include_paths, + output_path=args.output_path, + ) diff --git a/rosidl_cli/rosidl_cli/command/hash/api.py b/rosidl_cli/rosidl_cli/command/hash/api.py new file mode 100644 index 000000000..ce17a0026 --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/hash/api.py @@ -0,0 +1,68 @@ +# Copyright 2021 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 pathlib + +from .extensions import load_hash_extensions + + +def generate_type_hashes( + *, + package_name, + interface_files, + include_paths=None, + output_path=None, +): + """ + Generate type hashes from interface definition files. + + To do so, this function leverages type description hash generation support + as provided by third-party package extensions. + + Each path to an interface definition file is a relative path optionally + prefixed by another path followed by a colon ':', against which the first + relative path is to be resolved. + + The directory structure that these relative paths exhibit will be replicated + on output (as opposed to the prefix path, which will be ignored). + + :param package_name: name of the package to generate hashes for + :param interface_files: list of paths to interface definition files + :param include_paths: optional list of paths to include dependency + interface definition files from + :param output_path: optional path to directory to hold generated + source code files, defaults to the current working directory + :returns: list of lists of paths to generated hashed json files, + one group per type or type support extension invoked + """ + extensions = [] + extensions.extend(load_hash_extensions()) + + if include_paths is None: + include_paths = [] + if output_path is None: + output_path = pathlib.Path.cwd() + else: + pathlib.Path.mkdir(output_path, parents=True, exist_ok=True) + + generated_hashes = [] + for extension in extensions: + generated_hashes.extend(extension.generate_type_hashes( + package_name, + interface_files, + include_paths, + output_path=output_path, + )) + + return generated_hashes diff --git a/rosidl_cli/rosidl_cli/command/hash/extensions.py b/rosidl_cli/rosidl_cli/command/hash/extensions.py new file mode 100644 index 000000000..4993fdeaf --- /dev/null +++ b/rosidl_cli/rosidl_cli/command/hash/extensions.py @@ -0,0 +1,55 @@ +# Copyright 2021 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. + +from rosidl_cli.extensions import Extension +from rosidl_cli.extensions import load_extensions + + +class HashCommandExtension(Extension): + """ + The extension point for source code generation. + + The following methods must be defined: + * `generate_type_hashes` + """ + + def generate_type_hashes( + self, + package_name, + interface_files, + include_paths, + output_path, + ): + """ + Generate type hashes from interface definition files. + + Paths to interface definition files are relative paths optionally + prefixed by an absolute path followed by a colon ':', in which case + path resolution is to be performed against that absolute path. + + :param package_name: name of the package to generate source code for + :param interface_files: list of paths to interface definition files + :param include_paths: list of paths to include dependency interface + definition files from. + :param output_path: path to directory to hold generated source code files + :returns: list of paths to generated source files + """ + raise NotImplementedError() + + +def load_hash_extensions(**kwargs): + """Load extensions for type hash generation.""" + return load_extensions( + 'rosidl_cli.command.hash.extensions', **kwargs + ) diff --git a/rosidl_cli/rosidl_cli/command/helpers.py b/rosidl_cli/rosidl_cli/command/helpers.py index d81b0cdc3..24f927fd7 100644 --- a/rosidl_cli/rosidl_cli/command/helpers.py +++ b/rosidl_cli/rosidl_cli/command/helpers.py @@ -17,7 +17,7 @@ import os import pathlib import tempfile -from typing import Generator, List, Tuple +from typing import Any, Dict, Generator, List, Tuple def package_name_from_interface_file_path(path: pathlib.Path) -> str: @@ -90,17 +90,73 @@ def idl_tuples_from_interface_files( return idl_tuples +def build_type_description_tuples(idl_interface_files, type_description_files): + """ + Create type description tuples from IDL interface files and type descriptions. + + :param idl_interface_files: List of IDL interface files either with or without prefix + :param type_description_files: List of type description files + :return: List of type description tuples of the form 'idl_file_path:type_description_file' + """ + def get_type_description_file(idl_file, type_description_files): + for type_description_file in type_description_files: + if pathlib.Path(idl_file).stem == pathlib.Path(type_description_file).stem: + return type_description_file + + type_description_tuples = [] + for idl_file in idl_interface_files: + type_description_file = get_type_description_file(idl_file, type_description_files) + if type_description_file is None: + raise ValueError(f'Type description file not found for {idl_file}') + _, path = interface_path_as_tuple(idl_file) + type_description_tuples.append(f'{path}:{type_description_file}') + return type_description_tuples + + +def ros_interface_file_from_idl(idl_file: str) -> pathlib.Path: + """ + Return the absolute path of the ROS interface file generated from the given IDL file. + + :param idl_file: The IDL file to generate the ROS interface file from. + Can be prefix:relative/path/to/file.idl or relative/path/to/file.idl + :return: The absolute path of the ROS interface file generated from the given IDL file. + """ + prefix, path = interface_path_as_tuple(idl_file) + return (prefix / path).absolute() + + @contextlib.contextmanager -def legacy_generator_arguments_file( +def generator_arguments_file(**kwargs) -> Generator[str, None, None]: + """ + Create a temporary file containing generator arguments. + + :param kwargs: Generator arguments to be written to the file. + :yields: Path to the temporary file containing the generator arguments. + """ + # NOTE(hidmic): named temporary files cannot be opened twice on Windows, + # so close it and manually remove it when leaving the context + with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp: + tmp.write(json.dumps(kwargs)) + path_to_file = os.path.abspath(tmp.name) + try: + yield path_to_file + finally: + try: + os.unlink(path_to_file) + except FileNotFoundError: + pass + + +def legacy_generator_arguments( *, package_name: str, interface_files: List[str], include_paths: List[str], templates_path: str, - output_path: str -) -> Generator[str, None, None]: + output_path: str, +) -> Dict[str, Any]: """ - Generate a temporary rosidl generator arguments file. + Return a dict containing the generator arguments for the legacy ROSIDL generator. :param package_name: Name of the ROS package for which to generate code :param interface_files: Relative paths to ROS interface definition files, @@ -113,30 +169,43 @@ def legacy_generator_arguments_file( generator script this arguments are for :param output_path: Path to the output directory for generated code """ - idl_tuples = idl_tuples_from_interface_files(interface_files) - interface_dependencies = dependencies_from_include_paths(include_paths) - output_path = os.path.abspath(output_path) - templates_path = os.path.abspath(templates_path) - # NOTE(hidmic): named temporary files cannot be opened twice on Windows, - # so close it and manually remove it when leaving the context - with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp: - tmp.write(json.dumps({ - 'package_name': package_name, - 'output_dir': output_path, - 'template_dir': templates_path, - 'idl_tuples': idl_tuples, - 'ros_interface_dependencies': interface_dependencies, - # TODO(hidmic): re-enable output file caching - 'target_dependencies': [] - })) - path_to_file = os.path.abspath(tmp.name) - try: - yield path_to_file - finally: - try: - os.unlink(path_to_file) - except FileNotFoundError: - pass + arguments: Dict[str, Any] = {} + arguments['package_name'] = package_name + arguments['output_dir'] = os.path.abspath(output_path) + arguments['template_dir'] = os.path.abspath(templates_path) + arguments['idl_tuples'] = idl_tuples_from_interface_files(interface_files) + arguments['ros_interface_dependencies'] = dependencies_from_include_paths(include_paths) + # TODO(hidmic): re-enable output file caching + arguments['target_dependencies'] = [] + + return arguments + + +@contextlib.contextmanager +def legacy_generator_arguments_file( + *, + package_name: str, + interface_files: List[str], + include_paths: List[str], + templates_path: str, + output_path: str +) -> Generator[str, None, None]: + """ + Create a temporary file containing legacy arguments only. + + This context manager is kept for backwards compatibility only, use + `generator_arguments_file` instead. + """ + with generator_arguments_file( + **legacy_generator_arguments( + package_name=package_name, + interface_files=interface_files, + include_paths=include_paths, + templates_path=templates_path, + output_path=output_path + ) + ) as path_to_arguments_file: + yield path_to_arguments_file def generate_visibility_control_file( @@ -165,3 +234,15 @@ def generate_visibility_control_file( with open(output_path, 'w') as fd: fd.write(content) + + +def split_idl_interface_files(interface_files): + """Split interface files into IDL and non-IDL files.""" + idl_interface_files = [] + non_idl_interface_files = [] + for path in interface_files: + if not path.endswith('.idl'): + non_idl_interface_files.append(path) + else: + idl_interface_files.append(path) + return idl_interface_files, non_idl_interface_files diff --git a/rosidl_generator_c/rosidl_generator_c/cli.py b/rosidl_generator_c/rosidl_generator_c/cli.py index f6c786d7d..b7fa0242b 100644 --- a/rosidl_generator_c/rosidl_generator_c/cli.py +++ b/rosidl_generator_c/rosidl_generator_c/cli.py @@ -16,8 +16,15 @@ from ament_index_python import get_package_share_directory from rosidl_cli.command.generate.extensions import GenerateCommandExtension -from rosidl_cli.command.helpers import generate_visibility_control_file -from rosidl_cli.command.helpers import legacy_generator_arguments_file +from rosidl_cli.command.hash.api import generate_type_hashes +from rosidl_cli.command.helpers import ( + build_type_description_tuples, + generate_visibility_control_file, + generator_arguments_file, + legacy_generator_arguments, + ros_interface_file_from_idl, + split_idl_interface_files +) from rosidl_cli.command.translate.api import translate from rosidl_generator_c import generate_c @@ -30,7 +37,8 @@ def generate( package_name, interface_files, include_paths, - output_path + output_path, + type_description_files=None ): generated_files = [] @@ -39,13 +47,7 @@ def generate( templates_path = package_share_path / 'resource' # Normalize interface definition format to .idl - idl_interface_files = [] - non_idl_interface_files = [] - for path in interface_files: - if not path.endswith('.idl'): - non_idl_interface_files.append(path) - else: - idl_interface_files.append(path) + idl_interface_files, non_idl_interface_files = split_idl_interface_files(interface_files) if non_idl_interface_files: idl_interface_files.extend(translate( package_name=package_name, @@ -55,6 +57,18 @@ def generate( output_path=output_path / 'tmp', )) + if not type_description_files: + type_description_files = generate_type_hashes( + package_name=package_name, + interface_files=idl_interface_files, + include_paths=include_paths, + output_path=output_path + ) + + type_description_tuples = build_type_description_tuples( + idl_interface_files, type_description_files + ) + # Generate visibility control file visibility_control_file_template_path = \ templates_path / 'rosidl_generator_c__visibility_control.h.in' @@ -68,13 +82,22 @@ def generate( ) generated_files.append(visibility_control_file_path) + ros_interface_files = [ + str(ros_interface_file_from_idl(idl_file)) + for idl_file in idl_interface_files + ] + # Generate code - with legacy_generator_arguments_file( - package_name=package_name, - interface_files=idl_interface_files, - include_paths=include_paths, - templates_path=templates_path, - output_path=output_path + with generator_arguments_file( + **legacy_generator_arguments( + package_name=package_name, + interface_files=idl_interface_files, + include_paths=include_paths, + templates_path=templates_path, + output_path=output_path, + ), + type_description_tuples=type_description_tuples, + ros_interface_files=ros_interface_files ) as path_to_arguments_file: generated_files.extend(generate_c(path_to_arguments_file)) diff --git a/rosidl_generator_cpp/rosidl_generator_cpp/cli.py b/rosidl_generator_cpp/rosidl_generator_cpp/cli.py index 0d3044f7e..918095f90 100644 --- a/rosidl_generator_cpp/rosidl_generator_cpp/cli.py +++ b/rosidl_generator_cpp/rosidl_generator_cpp/cli.py @@ -16,7 +16,14 @@ from ament_index_python import get_package_share_directory from rosidl_cli.command.generate.extensions import GenerateCommandExtension -from rosidl_cli.command.helpers import legacy_generator_arguments_file +from rosidl_cli.command.hash.api import generate_type_hashes +from rosidl_cli.command.helpers import ( + build_type_description_tuples, + generate_visibility_control_file, + generator_arguments_file, + legacy_generator_arguments, + split_idl_interface_files, +) from rosidl_cli.command.translate.api import translate from rosidl_generator_cpp import generate_cpp @@ -29,20 +36,15 @@ def generate( package_name, interface_files, include_paths, - output_path + output_path, + type_description_files=None ): package_share_path = \ pathlib.Path(get_package_share_directory('rosidl_generator_cpp')) templates_path = package_share_path / 'resource' # Normalize interface definition format to .idl - idl_interface_files = [] - non_idl_interface_files = [] - for path in interface_files: - if not path.endswith('.idl'): - non_idl_interface_files.append(path) - else: - idl_interface_files.append(path) + idl_interface_files, non_idl_interface_files = split_idl_interface_files(interface_files) if non_idl_interface_files: idl_interface_files.extend(translate( package_name=package_name, @@ -52,12 +54,41 @@ def generate( output_path=output_path / 'tmp', )) - # Generate code - with legacy_generator_arguments_file( + if not type_description_files: + type_description_files = generate_type_hashes( + package_name=package_name, + interface_files=idl_interface_files, + include_paths=include_paths, + output_path=output_path + ) + + type_description_tuples = build_type_description_tuples( + idl_interface_files, type_description_files + ) + + generated_files = [] + # Generate visibility control file + visibility_control_file_template_path = \ + templates_path / 'rosidl_generator_cpp__visibility_control.hpp.in' + visibility_control_file_path = \ + output_path / 'msg' / 'rosidl_generator_cpp__visibility_control.hpp' + + generate_visibility_control_file( package_name=package_name, - interface_files=idl_interface_files, - include_paths=include_paths, - templates_path=templates_path, - output_path=output_path + template_path=visibility_control_file_template_path, + output_path=visibility_control_file_path + ) + generated_files.append(visibility_control_file_path) + + # Generate code + with generator_arguments_file( + **legacy_generator_arguments( + package_name=package_name, + interface_files=idl_interface_files, + include_paths=include_paths, + templates_path=templates_path, + output_path=output_path + ), + type_description_tuples=type_description_tuples, ) as path_to_arguments_file: return generate_cpp(path_to_arguments_file) diff --git a/rosidl_generator_type_description/rosidl_generator_type_description/__init__.py b/rosidl_generator_type_description/rosidl_generator_type_description/__init__.py index c62df1787..bbd790ede 100644 --- a/rosidl_generator_type_description/rosidl_generator_type_description/__init__.py +++ b/rosidl_generator_type_description/rosidl_generator_type_description/__init__.py @@ -201,6 +201,7 @@ def generate_type_hash(generator_arguments_file: str) -> List[str]: } rel_path = Path(*top_type_name.split('/')[1:]) json_path = output_dir / rel_path.with_suffix('.json') + json_path.parent.mkdir(parents=True, exist_ok=True) with json_path.open('w', encoding='utf-8') as json_file: json_file.write(json.dumps(json_content, indent=2)) generated_files.append(json_path) diff --git a/rosidl_generator_type_description/rosidl_generator_type_description/cli.py b/rosidl_generator_type_description/rosidl_generator_type_description/cli.py new file mode 100644 index 000000000..1c14f4f5f --- /dev/null +++ b/rosidl_generator_type_description/rosidl_generator_type_description/cli.py @@ -0,0 +1,81 @@ +# Copyright 2021 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 os +import pathlib + +from ament_index_python import get_package_share_directory +from rosidl_cli.command.hash.extensions import HashCommandExtension +from rosidl_cli.command.helpers import ( + generator_arguments_file, + legacy_generator_arguments, + package_name_from_interface_file_path, + split_idl_interface_files, +) +from rosidl_cli.command.translate.api import translate + +from rosidl_generator_type_description import generate_type_hash + + +def package_paths_from_include_paths(include_paths): + """ + Collect package paths, typically share paths, from include paths. + + Package paths are absolute paths prefixed by the name of package followed by a colon ':'. + """ + return list( + { + f'{package_name_from_interface_file_path(path)}:{path.parents[1]}' + for include_path in map(os.path.abspath, include_paths) + for path in pathlib.Path(include_path).glob('**/*.idl') + } + ) + + +class HashTypeDescription(HashCommandExtension): + def generate_type_hashes( + self, + package_name, + interface_files, + include_paths, + output_path, + ): + package_share_path = \ + pathlib.Path(get_package_share_directory('rosidl_generator_type_description')) + templates_path = package_share_path / 'resource' + + idl_interface_files, non_idl_interface_files = split_idl_interface_files(interface_files) + if non_idl_interface_files: + idl_interface_files.extend(translate( + package_name=package_name, + interface_files=non_idl_interface_files, + include_paths=include_paths, + output_format='idl', + output_path=output_path / 'tmp', + )) + + include_path_tuples = package_paths_from_include_paths(include_paths) + + # Generate code + with generator_arguments_file( + **legacy_generator_arguments( + package_name=package_name, + interface_files=idl_interface_files, + include_paths=include_paths, + templates_path=templates_path, + output_path=output_path, + ), + include_paths=include_path_tuples + ) as path_to_arguments_file: + return generate_type_hash(path_to_arguments_file) diff --git a/rosidl_generator_type_description/setup.cfg b/rosidl_generator_type_description/setup.cfg new file mode 100644 index 000000000..6fe6a3853 --- /dev/null +++ b/rosidl_generator_type_description/setup.cfg @@ -0,0 +1,3 @@ +[options.entry_points] +rosidl_cli.command.hash.extensions = + type_description = rosidl_generator_type_description.cli:HashTypeDescription diff --git a/rosidl_typesupport_introspection_c/rosidl_typesupport_introspection_c/cli.py b/rosidl_typesupport_introspection_c/rosidl_typesupport_introspection_c/cli.py index 3cee143ce..00a971472 100644 --- a/rosidl_typesupport_introspection_c/rosidl_typesupport_introspection_c/cli.py +++ b/rosidl_typesupport_introspection_c/rosidl_typesupport_introspection_c/cli.py @@ -17,8 +17,11 @@ from ament_index_python import get_package_share_directory from rosidl_cli.command.generate.extensions import GenerateCommandExtension -from rosidl_cli.command.helpers import generate_visibility_control_file -from rosidl_cli.command.helpers import legacy_generator_arguments_file +from rosidl_cli.command.helpers import ( + generate_visibility_control_file, + legacy_generator_arguments_file, + split_idl_interface_files +) from rosidl_cli.command.translate.api import translate from rosidl_typesupport_introspection_c import generate_c @@ -42,13 +45,7 @@ def generate( templates_path = package_share_path / 'resource' # Normalize interface definition format to .idl - idl_interface_files = [] - non_idl_interface_files = [] - for path in interface_files: - if not path.endswith('.idl'): - non_idl_interface_files.append(path) - else: - idl_interface_files.append(path) + idl_interface_files, non_idl_interface_files = split_idl_interface_files(interface_files) if non_idl_interface_files: idl_interface_files.extend(translate( package_name=package_name, diff --git a/rosidl_typesupport_introspection_cpp/rosidl_typesupport_introspection_cpp/cli.py b/rosidl_typesupport_introspection_cpp/rosidl_typesupport_introspection_cpp/cli.py index d9091680d..f9f5c64c0 100644 --- a/rosidl_typesupport_introspection_cpp/rosidl_typesupport_introspection_cpp/cli.py +++ b/rosidl_typesupport_introspection_cpp/rosidl_typesupport_introspection_cpp/cli.py @@ -18,7 +18,7 @@ from ament_index_python import get_package_share_directory from rosidl_cli.command.generate.extensions import GenerateCommandExtension -from rosidl_cli.command.helpers import legacy_generator_arguments_file +from rosidl_cli.command.helpers import legacy_generator_arguments_file, split_idl_interface_files from rosidl_cli.command.translate.api import translate from rosidl_typesupport_introspection_cpp import generate_cpp @@ -40,13 +40,7 @@ def generate( templates_path = package_share_path / 'resource' # Normalize interface definition format to .idl - idl_interface_files = [] - non_idl_interface_files = [] - for path in interface_files: - if not path.endswith('.idl'): - non_idl_interface_files.append(path) - else: - idl_interface_files.append(path) + idl_interface_files, non_idl_interface_files = split_idl_interface_files(interface_files) if non_idl_interface_files: idl_interface_files.extend(translate( package_name=package_name,