Skip to content

Commit 0a67d22

Browse files
authored
Merge pull request #29 from esteve/fix/issue-19-idl-directory-structure
fix(generator): allow IDL files outside standard folders
2 parents de743fc + 3397e38 commit 0a67d22

4 files changed

Lines changed: 140 additions & 153 deletions

File tree

rosidl_generator_rs/cmake/custom_command.cmake

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ set(PYTHON_EXECUTABLE ${Python3_EXECUTABLE})
1818
add_custom_command(
1919
OUTPUT
2020
${_generated_common_rs_files}
21-
${_generated_msg_rs_files}
22-
${_generated_srv_rs_files}
23-
${_generated_action_rs_files}
2421
COMMAND ${PYTHON_EXECUTABLE} ${rosidl_generator_rs_BIN}
2522
--generator-arguments-file "${generator_arguments_file}"
2623
--typesupport-impls "${_typesupport_impls}"
@@ -36,8 +33,5 @@ else()
3633
${rosidl_generate_interfaces_TARGET}${_target_suffix} ALL
3734
DEPENDS
3835
${_generated_common_rs_files}
39-
${_generated_msg_rs_files}
40-
${_generated_srv_rs_files}
41-
${_generated_action_rs_files}
4236
)
4337
endif()

rosidl_generator_rs/cmake/rosidl_generator_rs_generate_interfaces.cmake

Lines changed: 5 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -16,31 +16,8 @@ set(_output_path
1616
"${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_rs/${PROJECT_NAME}")
1717
set(_generated_common_rs_files "")
1818

19-
set(_generated_msg_rs_files "")
20-
set(_generated_srv_rs_files "")
21-
set(_generated_action_rs_files "")
22-
23-
set(_has_msg FALSE)
24-
set(_has_srv FALSE)
25-
set(_has_action FALSE)
26-
2719
foreach(_idl_file ${rosidl_generate_interfaces_ABS_IDL_FILES})
28-
get_filename_component(_parent_folder "${_idl_file}" DIRECTORY)
29-
get_filename_component(_parent_folder "${_parent_folder}" NAME)
30-
get_filename_component(_module_name "${_idl_file}" NAME_WE)
31-
32-
if(_parent_folder STREQUAL "msg")
33-
set(_has_msg TRUE)
34-
set(_idl_files ${_idl_files} ${_idl_file})
35-
elseif(_parent_folder STREQUAL "srv")
36-
set(_has_srv TRUE)
37-
set(_idl_files ${_idl_files} ${_idl_file})
38-
elseif(_parent_folder STREQUAL "action")
39-
set(_has_action TRUE)
40-
set(_idl_files ${_idl_files} ${_idl_file})
41-
else()
42-
message(FATAL_ERROR "Interface file with unknown parent folder: ${_idl_file}")
43-
endif()
20+
list(APPEND _idl_files ${_idl_file})
4421
endforeach()
4522

4623
list(APPEND _generated_common_rs_files
@@ -49,27 +26,6 @@ list(APPEND _generated_common_rs_files
4926
"${_output_path}/rust/Cargo.toml"
5027
)
5128

52-
if(${_has_msg})
53-
list(APPEND _generated_msg_rs_files
54-
"${_output_path}/rust/src/msg.rs"
55-
"${_output_path}/rust/src/msg/rmw.rs"
56-
)
57-
endif()
58-
59-
if(${_has_srv})
60-
list(APPEND _generated_srv_rs_files
61-
"${_output_path}/rust/src/srv.rs"
62-
"${_output_path}/rust/src/srv/rmw.rs"
63-
)
64-
endif()
65-
66-
if(${_has_action})
67-
list(APPEND _generated_action_rs_files
68-
"${_output_path}/rust/src/action.rs"
69-
"${_output_path}/rust/src/action/rmw.rs"
70-
)
71-
endif()
72-
7329
set(_dependency_files "")
7430
set(_dependencies "")
7531
foreach(_pkg_name ${rosidl_generate_interfaces_DEPENDENCY_PACKAGE_NAMES})
@@ -86,6 +42,9 @@ set(target_dependencies
8642
${rosidl_generator_rs_GENERATOR_FILES}
8743
"${rosidl_generator_rs_TEMPLATE_DIR}/action.rs.em"
8844
"${rosidl_generator_rs_TEMPLATE_DIR}/action/rmw.rs.em"
45+
"${rosidl_generator_rs_TEMPLATE_DIR}/build.rs.em"
46+
"${rosidl_generator_rs_TEMPLATE_DIR}/Cargo.toml.em"
47+
"${rosidl_generator_rs_TEMPLATE_DIR}/lib.rs.em"
8948
"${rosidl_generator_rs_TEMPLATE_DIR}/msg.rs.em"
9049
"${rosidl_generator_rs_TEMPLATE_DIR}/msg/rmw.rs.em"
9150
"${rosidl_generator_rs_TEMPLATE_DIR}/srv.rs.em"
@@ -94,7 +53,6 @@ set(target_dependencies
9453
"${rosidl_generator_rs_TEMPLATE_DIR}/templates/msg_rmw.rs.em"
9554
"${rosidl_generator_rs_TEMPLATE_DIR}/templates/srv_idiomatic.rs.em"
9655
"${rosidl_generator_rs_TEMPLATE_DIR}/templates/srv_rmw.rs.em"
97-
${rosidl_generate_interfaces_ABS_IDL_FILES}
9856
${_idl_files}
9957
${_dependency_files})
10058
foreach(dep ${target_dependencies})
@@ -137,9 +95,6 @@ add_dependencies(${rosidl_generate_interfaces_TARGET} ${rosidl_generate_interfac
13795
set_property(
13896
SOURCE
13997
${_generated_common_rs_files}
140-
${_generated_msg_rs_files}
141-
${_generated_srv_rs_files}
142-
${_generated_action_rs_files}
14398
PROPERTY GENERATED 1)
14499

145100
set(_rsext_suffix "__rsext")
@@ -152,11 +107,7 @@ if(NOT rosidl_generate_interfaces_SKIP_INSTALL)
152107
endif()
153108

154109
if(BUILD_TESTING AND rosidl_generate_interfaces_ADD_LINTER_TESTS)
155-
if(
156-
NOT _generated_msg_rs_files STREQUAL "" OR
157-
NOT _generated_srv_rs_files STREQUAL "" OR
158-
NOT _generated_action_rs_files STREQUAL ""
159-
)
110+
if(NOT _idl_files STREQUAL "")
160111
# TODO(esteve): add linters for Rust files
161112
endif()
162113
endif()

rosidl_generator_rs/resource/lib.rs.em

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,12 @@
22
#![allow(clippy::derive_partial_eq_without_eq)]
33
#![allow(clippy::upper_case_acronyms)]
44

5-
@[if len(msg_specs) > 0]@
6-
#[path = "msg.rs"]
7-
mod msg_idiomatic;
8-
pub mod msg {
9-
pub use super::msg_idiomatic::*;
5+
@[for namespace in generated_namespaces]@
6+
#[path = "@(namespace).rs"]
7+
mod @(namespace)_idiomatic;
8+
pub mod @(namespace) {
9+
pub use super::@(namespace)_idiomatic::*;
1010
pub mod rmw;
1111
}
12-
@[end if]@
1312

14-
@[if len(srv_specs) > 0]@
15-
#[path = "srv.rs"]
16-
mod srv_idiomatic;
17-
pub mod srv {
18-
pub use super::srv_idiomatic::*;
19-
pub mod rmw;
20-
}
21-
@[end if]@
22-
23-
@[if len(action_specs) > 0]@
24-
#[path = "action.rs"]
25-
mod action_idiomatic;
26-
pub mod action {
27-
pub use super::action_idiomatic::*;
28-
pub mod rmw;
29-
}
30-
@[end if]@
13+
@[end for]@

rosidl_generator_rs/rosidl_generator_rs/__init__.py

Lines changed: 129 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,88 @@ def convert_lower_case_underscore_to_camel_case(word):
6464
return ''.join(x.capitalize() or '_' for x in word.split('_'))
6565

6666

67+
def _namespace_from_namespaced_type(namespaced_type):
68+
namespaces = list(namespaced_type.namespaces)
69+
if not namespaces or namespaces[0] != package_name:
70+
raise ValueError(
71+
f"Expected namespace to start with package '{package_name}', got {namespaces}")
72+
if len(namespaces) != 2:
73+
raise ValueError(
74+
f"Expected exactly one namespace component after package '{package_name}', got {namespaces}")
75+
namespace = namespaces[1]
76+
if not namespace.isidentifier() or get_rs_name(namespace) != namespace:
77+
raise ValueError(
78+
f"Namespace '{namespace}' cannot be emitted as a Rust module name")
79+
return namespace
80+
81+
82+
def _namespace_from_message(message):
83+
return _namespace_from_namespaced_type(message.structure.namespaced_type)
84+
85+
86+
def _namespace_from_service(service):
87+
return _namespace_from_namespaced_type(service.namespaced_type)
88+
89+
90+
def _namespace_from_action(action):
91+
return _namespace_from_namespaced_type(action.namespaced_type)
92+
93+
94+
def _group_specs_by_namespace(specs, namespace_getter):
95+
grouped = {}
96+
for spec in specs:
97+
namespace = namespace_getter(spec)
98+
grouped.setdefault(namespace, []).append(spec)
99+
return grouped
100+
101+
102+
def _validate_single_kind_per_namespace(namespace_to_kinds):
103+
for namespace, kinds in namespace_to_kinds.items():
104+
direct_kinds = {kind for kind, _ in kinds}
105+
if len(direct_kinds) > 1:
106+
raise ValueError(
107+
f"Namespace '{namespace}' contains multiple top-level kinds: {sorted(direct_kinds)}")
108+
109+
110+
def _expand_namespace_templates(template_dir, output_dir, namespace, spec_kind, specs,
111+
latest_target_timestamp, data):
112+
if not specs:
113+
return
114+
115+
if spec_kind == 'msg':
116+
mappings = {
117+
os.path.join(template_dir, 'msg.rs.em'): [f'rust/src/{namespace}.rs'],
118+
os.path.join(template_dir, 'msg/rmw.rs.em'): [f'rust/src/{namespace}/rmw.rs'],
119+
}
120+
template_specs = 'msg_specs'
121+
elif spec_kind == 'srv':
122+
mappings = {
123+
os.path.join(template_dir, 'srv.rs.em'): [f'rust/src/{namespace}.rs'],
124+
os.path.join(template_dir, 'srv/rmw.rs.em'): [f'rust/src/{namespace}/rmw.rs'],
125+
}
126+
template_specs = 'srv_specs'
127+
elif spec_kind == 'action':
128+
mappings = {
129+
os.path.join(template_dir, 'action.rs.em'): [f'rust/src/{namespace}.rs'],
130+
os.path.join(template_dir, 'action/rmw.rs.em'): [f'rust/src/{namespace}/rmw.rs'],
131+
}
132+
template_specs = 'action_specs'
133+
else:
134+
raise ValueError(f'Unknown spec kind {spec_kind}')
135+
136+
namespace_data = data.copy()
137+
namespace_data[template_specs] = [(namespace, spec) for spec in specs]
138+
139+
for template_file, generated_filenames in mappings.items():
140+
for generated_filename in generated_filenames:
141+
generated_file = os.path.join(output_dir, generated_filename)
142+
rosidl_pycommon.expand_template(
143+
os.path.join(template_dir, template_file),
144+
namespace_data.copy(),
145+
generated_file,
146+
minimum_timestamp=latest_target_timestamp)
147+
148+
67149
def generate_rs(generator_arguments_file, typesupport_impls):
68150
args = rosidl_pycommon.read_generator_arguments(generator_arguments_file)
69151

@@ -99,31 +181,17 @@ def generate_rs(generator_arguments_file, typesupport_impls):
99181

100182
template_dir = args['template_dir']
101183

102-
mapping_msgs = {
103-
os.path.join(template_dir, 'msg.rs.em'): ['rust/src/%s'],
104-
os.path.join(template_dir, 'msg/rmw.rs.em'): ['rust/src/msg/%s'],
105-
}
106-
107-
mapping_srvs = {
108-
os.path.join(template_dir, 'srv.rs.em'): ['rust/src/%s'],
109-
os.path.join(template_dir, 'srv/rmw.rs.em'): ['rust/src/srv/%s'],
110-
}
111-
112-
mapping_actions = {
113-
os.path.join(template_dir, 'action.rs.em'): ['rust/src/%s'],
114-
os.path.join(template_dir, 'action/rmw.rs.em'): ['rust/src/action/%s'],
115-
}
116-
117184
# Ensure the required templates exist
118-
for template_file in mapping_msgs.keys():
119-
assert os.path.exists(template_file), \
120-
'Messages template file %s not found' % template_file
121-
for template_file in mapping_srvs.keys():
122-
assert os.path.exists(template_file), \
123-
'Services template file %s not found' % template_file
124-
for template_file in mapping_actions.keys():
185+
for template_file in [
186+
os.path.join(template_dir, 'msg.rs.em'),
187+
os.path.join(template_dir, 'msg/rmw.rs.em'),
188+
os.path.join(template_dir, 'srv.rs.em'),
189+
os.path.join(template_dir, 'srv/rmw.rs.em'),
190+
os.path.join(template_dir, 'action.rs.em'),
191+
os.path.join(template_dir, 'action/rmw.rs.em'),
192+
]:
125193
assert os.path.exists(template_file), \
126-
'Actions template file %s not found' % template_file
194+
'Template file %s not found' % template_file
127195

128196
data = {
129197
'pre_field_serde': pre_field_serde,
@@ -146,53 +214,44 @@ def generate_rs(generator_arguments_file, typesupport_impls):
146214
latest_target_timestamp = rosidl_pycommon.get_newest_modification_time(
147215
args['target_dependencies'])
148216

149-
for message in idl_content.get_elements_of_type(Message):
150-
data['msg_specs'].append(('msg', message))
151-
152-
for service in idl_content.get_elements_of_type(Service):
153-
data['srv_specs'].append(('srv', service))
154-
155-
for action in idl_content.get_elements_of_type(Action):
156-
data['action_specs'].append(('action', action))
157-
158-
if data['msg_specs']:
159-
for template_file, generated_filenames in mapping_msgs.items():
160-
stem = _removesuffix(Path(template_file).stem, ".em")
161-
162-
for generated_filename in generated_filenames:
163-
generated_file = os.path.join(args['output_dir'],
164-
generated_filename % stem)
165-
rosidl_pycommon.expand_template(
166-
os.path.join(template_dir, template_file),
167-
data.copy(),
168-
generated_file,
169-
minimum_timestamp=latest_target_timestamp)
170-
171-
if data['srv_specs']:
172-
for template_file, generated_filenames in mapping_srvs.items():
173-
stem = _removesuffix(Path(template_file).stem, ".em")
174-
175-
for generated_filename in generated_filenames:
176-
generated_file = os.path.join(args['output_dir'],
177-
generated_filename % stem)
178-
rosidl_pycommon.expand_template(
179-
os.path.join(template_dir, template_file),
180-
data.copy(),
181-
generated_file,
182-
minimum_timestamp=latest_target_timestamp)
183-
184-
if data['action_specs']:
185-
for template_file, generated_filenames in mapping_actions.items():
186-
stem = _removesuffix(Path(template_file).stem, ".em")
187-
188-
for generated_filename in generated_filenames:
189-
generated_file = os.path.join(args['output_dir'],
190-
generated_filename % stem)
191-
rosidl_pycommon.expand_template(
192-
os.path.join(template_dir, template_file),
193-
data.copy(),
194-
generated_file,
195-
minimum_timestamp=latest_target_timestamp)
217+
message_specs = list(idl_content.get_elements_of_type(Message))
218+
service_specs = list(idl_content.get_elements_of_type(Service))
219+
action_specs = list(idl_content.get_elements_of_type(Action))
220+
221+
messages_by_namespace = _group_specs_by_namespace(
222+
message_specs, _namespace_from_message)
223+
services_by_namespace = _group_specs_by_namespace(
224+
service_specs, _namespace_from_service)
225+
actions_by_namespace = _group_specs_by_namespace(
226+
action_specs, _namespace_from_action)
227+
228+
namespace_to_kinds = {}
229+
for namespace, specs in messages_by_namespace.items():
230+
namespace_to_kinds.setdefault(namespace, []).append(('msg', specs))
231+
for namespace, specs in services_by_namespace.items():
232+
namespace_to_kinds.setdefault(namespace, []).append(('srv', specs))
233+
for namespace, specs in actions_by_namespace.items():
234+
namespace_to_kinds.setdefault(namespace, []).append(('action', specs))
235+
236+
_validate_single_kind_per_namespace(namespace_to_kinds)
237+
238+
for namespace in sorted(namespace_to_kinds):
239+
kinds = namespace_to_kinds[namespace]
240+
kind, specs = kinds[0]
241+
_expand_namespace_templates(
242+
template_dir, args['output_dir'], namespace, kind, specs,
243+
latest_target_timestamp, data)
244+
245+
data['generated_namespaces'] = sorted(namespace_to_kinds)
246+
data['msg_specs'] = [
247+
(_namespace_from_message(message), message)
248+
for message in message_specs]
249+
data['srv_specs'] = [
250+
(_namespace_from_service(service), service)
251+
for service in service_specs]
252+
data['action_specs'] = [
253+
(_namespace_from_action(action), action)
254+
for action in action_specs]
196255

197256
rosidl_pycommon.expand_template(
198257
os.path.join(template_dir, 'lib.rs.em'),

0 commit comments

Comments
 (0)