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
23 changes: 22 additions & 1 deletion ros2pkg/ros2pkg/api/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ def create_package_environment(package, destination_directory):
'package_license': package.licenses[0],
'buildtool_dependencies': package.buildtool_depends,
'dependencies': package.build_depends,
'exec_dependencies': package.exec_depends,
'member_of_group': package.member_of_groups,
'test_dependencies': package.test_depends,
'exports': package.exports,
}
Expand Down Expand Up @@ -259,12 +261,14 @@ def populate_cmake(package, package_directory, cpp_node_name, cpp_library_name):
version_config)


def populate_ament_cmake(package, package_directory, cpp_node_name, cpp_library_name):
def populate_ament_cmake(package, package_directory, cpp_node_name, cpp_library_name,
message_names=None):
cmakelists_config = {
'project_name': package.name,
'dependencies': [str(dep) for dep in package.build_depends],
'cpp_node_name': cpp_node_name,
'cpp_library_name': cpp_library_name,
'message_names': message_names or [],
}
_create_template_file(
'ament_cmake',
Expand Down Expand Up @@ -355,3 +359,20 @@ def populate_rust_node(package, source_directory, node_name):
source_directory,
'main.rs',
cargo_node_config)


def populate_messages(package_directory: str, message_names: list) -> None:
"""
Create stub message files in the msg/ subdirectory of the package.

:param package_directory: path to the package directory.
:param message_names: list of message names to create.
"""
msg_directory = _create_folder('msg', package_directory)
for msg_name in message_names:
_create_template_file(
'msg',
'message.msg.em',
msg_directory,
msg_name + '.msg',
{})
16 changes: 16 additions & 0 deletions ros2pkg/ros2pkg/resource/ament_cmake/CMakeLists.txt.em
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ find_package(ament_cmake REQUIRED)
@[if cpp_library_name]@
find_package(ament_cmake_ros REQUIRED)
@[end if]@
@[if message_names]@
find_package(rosidl_default_generators REQUIRED)
@[end if]@
@[if dependencies]@
@[ for dep in dependencies]@
find_package(@dep REQUIRED)
Expand All @@ -19,6 +22,19 @@ find_package(@dep REQUIRED)
# further dependencies manually.
# find_package(<dependency> REQUIRED)
@[end if]@
@[if message_names]@

# do not forget to find_package all dependencies of your custom messages
# (if they were not already found earlier)
# find_package(<msg_dependency> REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
@[ for msg in message_names]@
"msg/@(msg).msg"
@[ end for]@
DEPENDENCIES # list all package dependencies of your messages
)
@[end if]@
@[if cpp_library_name]@

add_library(@(cpp_library_name) src/@(cpp_library_name).cpp)
Expand Down
13 changes: 13 additions & 0 deletions ros2pkg/ros2pkg/resource/msg/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2026 Leander Stephen Desouza
#
# 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.
6 changes: 6 additions & 0 deletions ros2pkg/ros2pkg/resource/msg/message.msg.em
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# TODO: Define the message fields here.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

License text is missing. However, the other template files also don't have it, so maybe let's leave it to another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, sure.
We can add the license texts to all the template files in a future PR.

# For example:
# bool my_flag
# string my_string
# int32 my_number
# if you use a non-trivial message type, do not forget to add its package to package.xml and CMakeLists.txt
12 changes: 12 additions & 0 deletions ros2pkg/ros2pkg/resource/package_environment/package.xml.em
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,24 @@
<depend>@dep</depend>
@[ end for]@

@[end if]@
@[if exec_dependencies]@
@[ for dep in exec_dependencies]@
<exec_depend>@dep</exec_depend>
@[ end for]@

@[end if]@
@[if test_dependencies]@
@[ for dep in test_dependencies]@
<test_depend>@dep</test_depend>
@[ end for]@

@[end if]@
@[if member_of_group]@
@[ for group in member_of_group]@
<member_of_group>@group</member_of_group>
@[ end for]@

@[end if]@
<export>
@[if exports]@
Expand Down
38 changes: 37 additions & 1 deletion ros2pkg/ros2pkg/verb/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import getpass
import os
import re
import shutil
import subprocess
import sys
Expand All @@ -33,6 +34,7 @@
from ros2pkg.api.create import populate_cmake
from ros2pkg.api.create import populate_cpp_library
from ros2pkg.api.create import populate_cpp_node
from ros2pkg.api.create import populate_messages
from ros2pkg.api.create import populate_python_libary
from ros2pkg.api.create import populate_python_node
from ros2pkg.api.create import populate_rust_node
Expand Down Expand Up @@ -90,6 +92,12 @@ def add_arguments(self, parser, cli_name):
parser.add_argument(
'--library-name',
help='name of the empty library')
parser.add_argument(
'--message',
nargs='+',
default=[],
metavar='MESSAGE_NAME',
help='name(s) of message files to create in msg/')

def main(self, *, args):

Expand Down Expand Up @@ -136,12 +144,26 @@ def get_git_config(key: str) -> Optional[str]:
print('[WARNING] node name can not be equal to the library name', file=sys.stderr)
print('[WARNING] renaming node to %s' % node_name, file=sys.stderr)

if args.message:
if args.build_type != 'ament_cmake':
return "Aborted: --message is only supported with 'ament_cmake' build type."

invalid = [
name for name in args.message
if not re.match(r'^[A-Z][A-Za-z0-9]*$', name)
]
if invalid:
return 'Aborted: invalid message name(s): ' + ', '.join(invalid) + \
'. Message names must be CamelCase and alphanumeric (e.g. MyMsg).'

buildtool_depends = []
if args.build_type == 'ament_cmake':
if args.library_name:
buildtool_depends = ['ament_cmake_ros']
else:
buildtool_depends = ['ament_cmake']
if args.message:
buildtool_depends.append('rosidl_default_generators')

if args.build_type == 'ament_cargo':
buildtool_depends = ['ament_cargo']
Expand All @@ -153,6 +175,12 @@ def get_git_config(key: str) -> Optional[str]:
test_dependencies = ['ament_copyright', 'ament_flake8', 'ament_mypy', 'ament_pep257',
'ament_xmllint', 'python3-pytest']

member_of_group_depends = []
exec_depends = []
if args.message:
member_of_group_depends.append('rosidl_interface_packages')
exec_depends.append('rosidl_default_runtime')

if args.build_type == 'ament_python' and args.package_name == 'test':
# If the package name is 'test', there will be a conflict between
# the directory the source code for the package goes in and the
Expand All @@ -169,6 +197,8 @@ def get_git_config(key: str) -> Optional[str]:
licenses=[args.license],
buildtool_depends=[Dependency(dep) for dep in buildtool_depends],
build_depends=[Dependency(dep) for dep in args.dependencies],
exec_depends=[Dependency(dep) for dep in exec_depends],
member_of_groups=member_of_group_depends,
test_depends=[Dependency(dep) for dep in test_dependencies],
exports=[Export('build_type', content=args.build_type)]
)
Expand All @@ -192,6 +222,8 @@ def get_git_config(key: str) -> Optional[str]:
print('node_name:', node_name)
if library_name:
print('library_name:', library_name)
if args.message:
print('messages:', args.message)

package_directory, source_directory, include_directory = \
create_package_environment(package, args.destination_directory)
Expand All @@ -202,7 +234,8 @@ def get_git_config(key: str) -> Optional[str]:
populate_cmake(package, package_directory, node_name, library_name)

if args.build_type == 'ament_cmake':
populate_ament_cmake(package, package_directory, node_name, library_name)
populate_ament_cmake(package, package_directory, node_name, library_name,
message_names=args.message)

if args.build_type == 'ament_cargo':
populate_ament_cargo(package, package_directory, library_name)
Expand Down Expand Up @@ -239,6 +272,9 @@ def get_git_config(key: str) -> Optional[str]:
node_name
)

if args.message:
populate_messages(package_directory, args.message)

if args.license in available_licenses:
with open(os.path.join(package_directory, 'LICENSE'), 'w') as outfp:
for lic in available_licenses[args.license]:
Expand Down
47 changes: 33 additions & 14 deletions ros2pkg/test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@ def test_create_package(self):
'--maintainer-email', '[email protected]',
'--maintainer-name', 'Nobody',
'--node-name', 'test_node',
'--library-name', 'test_library'
'--library-name', 'test_library',
'--message', 'MyMsg', 'OtherMsg',
], cwd=tmpdir
) as pkg_command:
assert pkg_command.wait_for_shutdown(timeout=5)
Expand All @@ -151,6 +152,7 @@ def test_create_package(self):
"dependencies: ['ros2pkg']",
'node_name: test_node',
'library_name: test_library',
"messages: ['MyMsg', 'OtherMsg']",
'creating folder ' + os.path.join('.', 'a_test_package'),
'creating ' + os.path.join('.', 'a_test_package', 'package.xml'),
'creating source and include folder',
Expand All @@ -171,29 +173,31 @@ def test_create_package(self):
'creating ' + os.path.join(
'.', 'a_test_package', 'include', 'a_test_package', 'visibility_control.h'
),
'creating folder ' + os.path.join('.', 'a_test_package', 'msg'),
'creating ' + os.path.join('.', 'a_test_package', 'msg', 'MyMsg.msg'),
'creating ' + os.path.join('.', 'a_test_package', 'msg', 'OtherMsg.msg'),
],
text=pkg_command.output,
strict=True
)
# Check layout
assert os.path.isdir(os.path.join(tmpdir, 'a_test_package'))
assert os.path.isfile(os.path.join(tmpdir, 'a_test_package', 'package.xml'))
assert os.path.isfile(os.path.join(tmpdir, 'a_test_package', 'CMakeLists.txt'))
assert os.path.isfile(os.path.join(tmpdir, 'a_test_package', 'LICENSE'))
assert os.path.isfile(
os.path.join(tmpdir, 'a_test_package', 'src', 'test_node.cpp')
)
assert os.path.isfile(
os.path.join(tmpdir, 'a_test_package', 'src', 'test_library.cpp')
)
pkg_dir = os.path.join(tmpdir, 'a_test_package')
assert os.path.isdir(pkg_dir)
assert os.path.isfile(os.path.join(pkg_dir, 'package.xml'))
assert os.path.isfile(os.path.join(pkg_dir, 'CMakeLists.txt'))
assert os.path.isfile(os.path.join(pkg_dir, 'LICENSE'))
assert os.path.isfile(os.path.join(pkg_dir, 'src', 'test_node.cpp'))
assert os.path.isfile(os.path.join(pkg_dir, 'src', 'test_library.cpp'))
assert os.path.isfile(os.path.join(
tmpdir, 'a_test_package', 'include', 'a_test_package', 'test_library.hpp'
pkg_dir, 'include', 'a_test_package', 'test_library.hpp'
))
assert os.path.isfile(os.path.join(
tmpdir, 'a_test_package', 'include', 'a_test_package', 'visibility_control.h'
pkg_dir, 'include', 'a_test_package', 'visibility_control.h'
))
assert os.path.isfile(os.path.join(pkg_dir, 'msg', 'MyMsg.msg'))
assert os.path.isfile(os.path.join(pkg_dir, 'msg', 'OtherMsg.msg'))
# Check package.xml
tree = ET.parse(os.path.join(tmpdir, 'a_test_package', 'package.xml'))
tree = ET.parse(os.path.join(pkg_dir, 'package.xml'))
root = tree.getroot()
assert root.tag == 'package'
assert root.attrib['format'] == '3'
Expand All @@ -204,3 +208,18 @@ def test_create_package(self):
assert root.find('license').text == 'Apache-2.0'
assert root.find('depend').text == 'ros2pkg'
assert root.find('.//build_type').text == 'ament_cmake'

# Check rosidl message dependencies
buildtool_deps = [e.text for e in root.findall('buildtool_depend')]
assert 'rosidl_default_generators' in buildtool_deps
exec_deps = [e.text for e in root.findall('exec_depend')]
assert 'rosidl_default_runtime' in exec_deps
assert root.find('member_of_group').text == 'rosidl_interface_packages'

# Check CMakeLists.txt
with open(os.path.join(pkg_dir, 'CMakeLists.txt'), 'r', encoding='utf-8') as f:
cmake_content = f.read()
assert 'find_package(rosidl_default_generators REQUIRED)' in cmake_content
assert 'rosidl_generate_interfaces(${PROJECT_NAME}' in cmake_content
assert '"msg/MyMsg.msg"' in cmake_content
assert '"msg/OtherMsg.msg"' in cmake_content