diff --git a/.bazelrc b/.bazelrc index c158b345..ae982be8 100644 --- a/.bazelrc +++ b/.bazelrc @@ -53,6 +53,8 @@ build:clang --linkopt="-fuse-ld=lld" # Work around https://github.com/bazelbuild/rules_rust/issues/2125 build --action_env=CARGO_BAZEL_REPIN=1 +common --enable_bzlmod --noenable_workspace + # Load any settings specific to the current user. # user.bazelrc should appear in .gitignore so that settings are not shared with # team members. This needs to be last statement in this config, diff --git a/MODULE.bazel b/MODULE.bazel index 597d4d9f..ba20bbc2 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -10,6 +10,7 @@ bazel_dep(name = "libyaml", version = "0.2.5") bazel_dep(name = "lz4", version = "1.10.0.bcr.1") bazel_dep(name = "nlohmann_json", version = "3.12.0.bcr.1") bazel_dep(name = "platforms", version = "1.0.0") +bazel_dep(name = "protobuf", version = "33.5", repo_name = "com_google_protobuf") bazel_dep(name = "pybind11_bazel", version = "3.0.0") bazel_dep(name = "readerwriterqueue", version = "1.0.6") bazel_dep(name = "rules_cc", version = "0.2.16") @@ -150,6 +151,7 @@ use_repo( "foxglove_bridge", "iceoryx", "osrf_pycommon", + "proto2ros", "ros2", "ros2_ament_index", "ros2_class_loader", diff --git a/examples/MODULE.bazel b/examples/MODULE.bazel index c92b4b7d..a8884ab6 100644 --- a/examples/MODULE.bazel +++ b/examples/MODULE.bazel @@ -6,6 +6,7 @@ local_path_override( path = "..", ) +bazel_dep(name = "protobuf", version = "33.5", repo_name = "com_google_protobuf") bazel_dep(name = "rules_cc", version = "0.2.9") bazel_dep(name = "rules_python", version = "1.6.3") bazel_dep(name = "rules_rust", version = "0.63.0") @@ -29,6 +30,8 @@ rules_ros2_non_module_deps = use_extension("@com_github_mvukov_rules_ros2//ros2: use_repo( rules_ros2_non_module_deps, # Check the rules_ros2 root MODULE.bazel for a full list of available non-module repos + # Required for proto2ros: + "proto2ros", # Required for bazel test: "ros2_common_interfaces", "ros2_geometry2", @@ -44,5 +47,6 @@ use_repo( "ros2cli", # Required for bazel run: "ros2_rclpy", + # Required for proto2ros: "ros2_rosidl", ) diff --git a/examples/proto_to_ros/BUILD.bazel b/examples/proto_to_ros/BUILD.bazel new file mode 100644 index 00000000..caf0fa86 --- /dev/null +++ b/examples/proto_to_ros/BUILD.bazel @@ -0,0 +1,59 @@ +load("@com_github_mvukov_rules_ros2//ros2:cc_defs.bzl", "ros2_cpp_binary") +load("@com_github_mvukov_rules_ros2//ros2:interfaces.bzl", "cpp_ros2_interface_library", "py_ros2_interface_library", "ros2_interface_library") +load("@com_github_mvukov_rules_ros2//ros2:launch.bzl", "ros2_launch") +load("@com_github_mvukov_rules_ros2//ros2:proto2ros.bzl", "proto2ros_message") +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@rules_python//python:defs.bzl", "py_binary") + +proto_library( + name = "chatter_proto", + srcs = ["message.proto"], +) + +proto2ros_message( + name = "chatter_msgs", + msg_names = ["ChatterMessage"], + proto_library = ":chatter_proto", +) + +ros2_interface_library( + name = "chatter_interfaces", + srcs = [":chatter_msgs"], +) + +cpp_ros2_interface_library( + name = "chatter_interfaces_cpp", + deps = [":chatter_interfaces"], +) + +py_ros2_interface_library( + name = "chatter_interfaces_py", + deps = [":chatter_interfaces"], +) + +py_binary( + name = "talker", + srcs = ["talker.py"], + deps = [ + ":chatter_interfaces_py", + "@ros2_rclpy//:rclpy", + ], +) + +ros2_cpp_binary( + name = "listener", + srcs = ["listener.cc"], + deps = [ + ":chatter_interfaces_cpp", + "@ros2_rclcpp//:rclcpp", + ], +) + +ros2_launch( + name = "proto_to_ros", + launch_file = "proto_to_ros.py", + nodes = [ + ":listener", + ":talker", + ], +) diff --git a/examples/proto_to_ros/README.md b/examples/proto_to_ros/README.md new file mode 100644 index 00000000..927d588e --- /dev/null +++ b/examples/proto_to_ros/README.md @@ -0,0 +1,63 @@ +# Proto2ros Example + +This example demonstrates how to use `proto2ros` to convert Protocol Buffer +definitions to ROS 2 messages, integrated with working ROS 2 nodes. + +## Overview + +The example shows: + +- Converting a `.proto` file to ROS 2 `.msg` format +- Using the `proto2ros_message` rule +- **Python talker node** publishing messages on the `chatter` topic +- **C++ listener node** subscribing to messages on the `chatter` topic + +## Files + +- `message.proto` - Protocol Buffer message definition +- `talker.py` - Python publisher node +- `listener.cc` - C++ subscriber node +- `proto2ros.py` - Launch file to run both nodes +- `BUILD.bazel` - Bazel build configuration + +## Usage + +### In BUILD.bazel + +```python +load("@com_google_protobuf//bazel:proto_library.bzl", "proto_library") +load("@com_github_mvukov_rules_ros2//ros2:proto2ros.bzl", "proto2ros_message") + +proto_library( + name = "chatter_proto", + srcs = ["message.proto"], +) + +proto2ros_message( + name = "chatter_msgs", + msg_names = ["ChatterMessage"], + proto_library = ":chatter_proto", +) + +ros2_interface_library( + name = "chatter_interfaces", + srcs = [":chatter_msgs"], +) +``` + +### Building + +```bash +bazel build //examples/proto_to_ros:all +bazel build //examples/proto_to_ros:talker # Python talker node +bazel build //examples/proto_to_ros:listener # C++ listener node +bazel build //examples/proto_to_ros:proto_to_ros # Launch file +``` + +### Running + +Run the demo to see the Python talker and C++ listener communicate: + +```bash +bazel run //examples/proto_to_ros:proto_to_ros +``` diff --git a/examples/proto_to_ros/listener.cc b/examples/proto_to_ros/listener.cc new file mode 100644 index 00000000..9c1af6aa --- /dev/null +++ b/examples/proto_to_ros/listener.cc @@ -0,0 +1,46 @@ +// Copyright 2026 Milan Vukov +// +// 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. + +#include + +#include "chatter_interfaces/msg/chatter_message.hpp" +#include "rclcpp/rclcpp.hpp" + +/** + * C++ listener node using proto2ros generated messages. + */ +class ProtoChatterListener : public rclcpp::Node { + public: + ProtoChatterListener() : Node("proto_chatter_listener") { + subscription_ = + create_subscription( + "chatter", 10, + [this](chatter_interfaces::msg::ChatterMessage::UniquePtr msg) { + RCLCPP_INFO(get_logger(), + "I heard: data='%s', timestamp=%ld", + msg->data.c_str(), msg->timestamp); + }); + } + + private: + rclcpp::Subscription::SharedPtr + subscription_; +}; + +int main(int argc, char* argv[]) { + rclcpp::init(argc, argv); + rclcpp::spin(std::make_shared()); + rclcpp::shutdown(); + return 0; +} diff --git a/examples/proto_to_ros/message.proto b/examples/proto_to_ros/message.proto new file mode 100644 index 00000000..ce9d3258 --- /dev/null +++ b/examples/proto_to_ros/message.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package chatter; + +message ChatterMessage { + string data = 1; + int64 timestamp = 2; +} diff --git a/examples/proto_to_ros/proto_to_ros.py b/examples/proto_to_ros/proto_to_ros.py new file mode 100644 index 00000000..35e2c895 --- /dev/null +++ b/examples/proto_to_ros/proto_to_ros.py @@ -0,0 +1,35 @@ +# Copyright 2026 Milan Vukov +# +# 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. +"""Launch proto2ros chatter demo: Python talker and C++ listener.""" + +import launch +import launch_ros.actions + + +def generate_launch_description(): + """Launch a Python talker and C++ listener using proto2ros messages.""" + return launch.LaunchDescription( + [ + launch_ros.actions.Node( + executable="proto_to_ros/talker", + output="screen", + name="proto_talker", + ), + launch_ros.actions.Node( + executable="proto_to_ros/listener", + output="screen", + name="proto_listener", + ), + ] + ) diff --git a/examples/proto_to_ros/talker.py b/examples/proto_to_ros/talker.py new file mode 100644 index 00000000..77072915 --- /dev/null +++ b/examples/proto_to_ros/talker.py @@ -0,0 +1,61 @@ +# Copyright 2026 Milan Vukov +# +# 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. + +"""Python talker node using proto2ros generated messages.""" + +import time + +import rclpy +from chatter_interfaces.msg import ChatterMessage +from rclpy import node + + +class ProtoChatterTalker(node.Node): + """Publishes ChatterMessage messages on the 'chatter' topic.""" + + def __init__(self): + super().__init__("proto_chatter_talker") + self.publisher_ = self.create_publisher(ChatterMessage, "chatter", 10) + timer_period = 0.5 # seconds + self.timer = self.create_timer(timer_period, self.timer_callback) + self.i = 0 + + def timer_callback(self): + """Publish a ChatterMessage with data and timestamp.""" + msg = ChatterMessage() + msg.data = f"Hello from proto2ros: {self.i}" + msg.timestamp = int(time.time() * 1000) # milliseconds since epoch + self.publisher_.publish(msg) + self.get_logger().info( + f'Publishing: data="{msg.data}", timestamp={msg.timestamp}' + ) + self.i += 1 + + +def main(): + rclpy.init() + + talker = ProtoChatterTalker() + + try: + rclpy.spin(talker) + except KeyboardInterrupt: + pass + finally: + talker.destroy_node() + rclpy.shutdown() + + +if __name__ == "__main__": + main() diff --git a/repositories/proto2ros.BUILD.bazel b/repositories/proto2ros.BUILD.bazel new file mode 100644 index 00000000..1cd33557 --- /dev/null +++ b/repositories/proto2ros.BUILD.bazel @@ -0,0 +1,39 @@ +"""Builds proto2ros from bdaiinstitute/proto2ros repository.""" + +load("@com_google_protobuf//bazel:py_proto_library.bzl", "py_proto_library") +load("@rules_python//python:defs.bzl", "py_binary", "py_library") +load("@rules_ros2_pip_deps//:requirements.bzl", "requirement") + +py_proto_library( + name = "protobuf_runtime", + deps = ["@com_google_protobuf//:any_proto"], +) + +# Core proto2ros Python library +py_library( + name = "proto2ros_lib", + srcs = glob(["proto2ros/proto2ros/**/*.py"]), + data = glob([ + "proto2ros/proto2ros/configuration/*.yaml", + "proto2ros/proto2ros/output/templates/**/*", + ]), + imports = ["proto2ros"], + deps = [ + ":protobuf_runtime", + "@ros2_rosidl//:rosidl_adapter_lib", + requirement("inflection"), + requirement("jinja2"), + requirement("networkx"), + requirement("numpy"), + requirement("pyyaml"), + ], +) + +# Main generation CLI binary +py_binary( + name = "generate", + srcs = ["proto2ros/proto2ros/cli/generate.py"], + main = "proto2ros/proto2ros/cli/generate.py", + visibility = ["//visibility:public"], + deps = [":proto2ros_lib"], +) diff --git a/repositories/repositories.bzl b/repositories/repositories.bzl index 60efc10e..c08e94ec 100644 --- a/repositories/repositories.bzl +++ b/repositories/repositories.bzl @@ -333,3 +333,14 @@ def ros2_repositories(): strip_prefix = "rcl_logging_syslog-e63257f2d5ca693f286bbcedf2b23720675b7f73", urls = ["https://github.com/fujitatomoya/rcl_logging_syslog/archive/e63257f2d5ca693f286bbcedf2b23720675b7f73.zip"], ) + +def proto2ros_repository(): + """Fetches proto2ros from GitHub.""" + maybe( + http_archive, + name = "proto2ros", + build_file = "@com_github_mvukov_rules_ros2//repositories:proto2ros.BUILD.bazel", + sha256 = "31fbebb35056266f2959fda4291862f063b21ea29a8209c5d87cde49443e71bd", + strip_prefix = "proto2ros-0cc24714f99d1538d20fc25c9312d6199c3ce662", + url = "https://github.com/bdaiinstitute/proto2ros/archive/0cc24714f99d1538d20fc25c9312d6199c3ce662.tar.gz", + ) diff --git a/requirements.txt b/requirements.txt index fad99e39..ff55f37c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,10 @@ catkin_pkg coverage empy==3.3.* +inflection +jinja2 lark-parser +networkx numpy~=1.23 packaging psutil diff --git a/requirements_lock.txt b/requirements_lock.txt index 84095de9..c6feb14c 100644 --- a/requirements_lock.txt +++ b/requirements_lock.txt @@ -78,14 +78,117 @@ exceptiongroup==1.0.4 \ --hash=sha256:542adf9dea4055530d6e1279602fa5cb11dab2395fa650b8674eaec35fc4a828 \ --hash=sha256:bd14967b79cd9bdb54d97323216f8fdf533e278df937aa2a90089e7d6e06e5ec # via pytest +inflection==0.5.1 \ + --hash=sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417 \ + --hash=sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2 + # via -r requirements.txt iniconfig==1.1.1 \ --hash=sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3 \ --hash=sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32 # via pytest +jinja2==3.1.6 \ + --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \ + --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + # via -r requirements.txt lark-parser==0.12.0 \ --hash=sha256:0eaf30cb5ba787fe404d73a7d6e61df97b21d5a63ac26c5008c78a494373c675 \ --hash=sha256:15967db1f1214013dca65b1180745047b9be457d73da224fcda3d9dd4e96a138 # via -r requirements.txt +markupsafe==3.0.3 \ + --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \ + --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \ + --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \ + --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \ + --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \ + --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \ + --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \ + --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \ + --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \ + --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \ + --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \ + --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \ + --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \ + --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \ + --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \ + --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \ + --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \ + --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \ + --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \ + --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \ + --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \ + --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \ + --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \ + --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \ + --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \ + --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \ + --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \ + --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \ + --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \ + --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \ + --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \ + --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \ + --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \ + --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \ + --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \ + --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \ + --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \ + --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \ + --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \ + --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \ + --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \ + --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \ + --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \ + --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \ + --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \ + --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \ + --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \ + --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \ + --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \ + --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \ + --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \ + --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \ + --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \ + --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \ + --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \ + --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \ + --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \ + --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \ + --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \ + --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \ + --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \ + --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \ + --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \ + --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \ + --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \ + --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \ + --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \ + --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \ + --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \ + --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \ + --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \ + --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \ + --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \ + --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \ + --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \ + --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \ + --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \ + --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \ + --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \ + --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \ + --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \ + --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \ + --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \ + --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \ + --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \ + --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \ + --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \ + --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \ + --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50 + # via jinja2 +networkx==3.4.2 \ + --hash=sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1 \ + --hash=sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f + # via -r requirements.txt numpy==1.26.4 \ --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \ --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \ diff --git a/ros2/BUILD.bazel b/ros2/BUILD.bazel index a16fe2e7..757e2cfb 100644 --- a/ros2/BUILD.bazel +++ b/ros2/BUILD.bazel @@ -19,6 +19,7 @@ exports_files([ "launch.py.tpl", "launch.sh.tpl", "plugin.bzl", + "proto2ros.bzl", "py_defs.bzl", "pytest_wrapper.py.tpl", "ros2_action.py", diff --git a/ros2/extensions.bzl b/ros2/extensions.bzl index a680c0d6..0d3c640c 100644 --- a/ros2/extensions.bzl +++ b/ros2/extensions.bzl @@ -1,9 +1,10 @@ load("//repositories:clang_configure.bzl", "clang_configure") -load("//repositories:repositories.bzl", "ros2_repositories") +load("//repositories:repositories.bzl", "proto2ros_repository", "ros2_repositories") load("//repositories:rust_setup_stage_1.bzl", "ros2_rust_repositories") def _non_module_deps_impl(mctx): ros2_repositories() + proto2ros_repository() clang_configure(name = "rules_ros2_config_clang") ros2_rust_repositories() diff --git a/ros2/proto2ros.bzl b/ros2/proto2ros.bzl new file mode 100644 index 00000000..80e404cf --- /dev/null +++ b/ros2/proto2ros.bzl @@ -0,0 +1,106 @@ +# Copyright 2026 Milan Vukov +# +# 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. +"""Rules for converting Protocol Buffers to ROS 2 messages.""" + +load("@com_google_protobuf//bazel/common:proto_info.bzl", "ProtoInfo") + +def _proto2ros_message_impl(ctx): + """Generates ROS 2 .msg files from proto_library descriptors.""" + proto_info = ctx.attr.proto_library[ProtoInfo] + descriptor_set = proto_info.direct_descriptor_set + label_name = ctx.label.name + + # Generate Python protobuf code for proto2ros to import. + py_pb2_dir = ctx.actions.declare_directory( + "{}_py_pb2".format(label_name), + ) + + proto_gen_args = ctx.actions.args() + proto_gen_args.add("--python_out", py_pb2_dir.path) + + # Add proto paths for all transitive proto roots + for proto_root in proto_info.transitive_proto_path.to_list(): + if proto_root: + proto_gen_args.add("--proto_path", proto_root) + + # Add the exec root as a fallback proto path + proto_gen_args.add("--proto_path", ".") + + # Generate Python code for ALL transitive sources so proto2ros can import them + proto_gen_args.add_all(proto_info.transitive_sources) + + ctx.actions.run( + inputs = proto_info.transitive_sources, + outputs = [py_pb2_dir], + executable = ctx.executable._protoc, + arguments = [proto_gen_args], + mnemonic = "GenProtoPython", + progress_message = "Generating Python protobuf code for {}".format(label_name), + ) + + # Declare output files. + output_prefix = "{}_proto2ros_output".format(label_name) + + msg_files = [] + for msg_name in ctx.attr.msg_names: + msg_file = ctx.actions.declare_file( + "{}/msg/{}.msg".format(output_prefix, msg_name), + ) + msg_files.append(msg_file) + + # Derive the output directory path from declared file paths. + output_dir_path = msg_files[0].path.rsplit("/msg/", 1)[0] + + args = ctx.actions.args() + args.add("--output-directory", output_dir_path) + args.add(label_name) + args.add(descriptor_set.path) + + ctx.actions.run( + inputs = [descriptor_set, py_pb2_dir], + outputs = msg_files, + executable = ctx.executable._proto2ros_generate, + arguments = [args], + env = {"PYTHONPATH": py_pb2_dir.path}, + mnemonic = "Proto2RosGenerate", + progress_message = "Generating ROS messages from proto for {}".format(label_name), + ) + + return [DefaultInfo(files = depset(msg_files))] + +proto2ros_message = rule( + implementation = _proto2ros_message_impl, + attrs = { + "proto_library": attr.label( + mandatory = True, + providers = [ProtoInfo], + doc = "The proto_library target to convert to ROS messages.", + ), + "msg_names": attr.string_list( + mandatory = True, + doc = "List of message names that will be generated (e.g., ['ChatterMessage']).", + ), + "_proto2ros_generate": attr.label( + default = Label("@proto2ros//:generate"), + executable = True, + cfg = "exec", + ), + "_protoc": attr.label( + default = Label("@com_google_protobuf//src/google/protobuf/compiler:protoc"), + executable = True, + cfg = "exec", + ), + }, + doc = "Generates ROS 2 .msg files from a proto_library target.", +)