pw_rpc: Nanopb RPC protoc plugin
This change adds a protoc plugin which generates service code for pw_rpc
using nanopb as the protobuf implementation. Only the plugin code is
added here; it is not yet integrated into the build system.
Change-Id: Ia24bb5d130bf54c7afb41af2bca7f346e14bed9b
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/12941
Commit-Queue: Alexei Frolov <frolv@google.com>
Reviewed-by: Wyatt Hepler <hepler@google.com>
diff --git a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
index b9760fa..4588753 100644
--- a/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
+++ b/pw_protobuf/py/pw_protobuf/codegen_pwpb.py
@@ -21,6 +21,7 @@
import google.protobuf.descriptor_pb2 as descriptor_pb2
+from pw_protobuf.output_file import OutputFile
from pw_protobuf.proto_tree import ProtoMessageField, ProtoNode
from pw_protobuf.proto_tree import build_node_tree
@@ -500,66 +501,6 @@
}
-class OutputFile:
- """A buffer to which data is written.
-
- Example:
-
- ```
- output = Output("hello.c")
- output.write_line('int main(void) {')
- with output.indent():
- output.write_line('printf("Hello, world");')
- output.write_line('return 0;')
- output.write_line('}')
-
- print(output.content())
- ```
-
- Produces:
- ```
- int main(void) {
- printf("Hello, world");
- return 0;
- }
- ```
- """
-
- INDENT_WIDTH = 2
-
- def __init__(self, filename: str):
- self._filename: str = filename
- self._content: List[str] = []
- self._indentation: int = 0
-
- def write_line(self, line: str = '') -> None:
- if line:
- self._content.append(' ' * self._indentation)
- self._content.append(line)
- self._content.append('\n')
-
- def indent(self) -> 'OutputFile._IndentationContext':
- """Increases the indentation level of the output."""
- return self._IndentationContext(self)
-
- def name(self) -> str:
- return self._filename
-
- def content(self) -> str:
- return ''.join(self._content)
-
- class _IndentationContext:
- """Context that increases the output's indentation when it is active."""
- def __init__(self, output: 'OutputFile'):
- self._output = output
-
- def __enter__(self):
- self._output._indentation += OutputFile.INDENT_WIDTH
-
- def __exit__(self, typ, value, traceback):
- self._output._indentation -= OutputFile.INDENT_WIDTH
-
-
def generate_code_for_message(message: ProtoNode, root: ProtoNode,
output: OutputFile) -> None:
"""Creates a C++ class for a protobuf message."""
diff --git a/pw_protobuf/py/pw_protobuf/output_file.py b/pw_protobuf/py/pw_protobuf/output_file.py
new file mode 100644
index 0000000..04bd0ee
--- /dev/null
+++ b/pw_protobuf/py/pw_protobuf/output_file.py
@@ -0,0 +1,80 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Defines a class used to write code to an output buffer."""
+
+from typing import List
+
+
+class OutputFile:
+ """A buffer to which data is written.
+
+ Example:
+
+ ```
+ output = Output("hello.c")
+ output.write_line('int main(void) {')
+ with output.indent():
+ output.write_line('printf("Hello, world");')
+ output.write_line('return 0;')
+ output.write_line('}')
+
+ print(output.content())
+ ```
+
+ Produces:
+ ```
+ int main(void) {
+ printf("Hello, world");
+ return 0;
+ }
+ ```
+ """
+
+ INDENT_WIDTH = 2
+
+ def __init__(self, filename: str):
+ self._filename: str = filename
+ self._content: List[str] = []
+ self._indentation: int = 0
+
+ def write_line(self, line: str = '') -> None:
+ if line:
+ self._content.append(' ' * self._indentation)
+ self._content.append(line)
+ self._content.append('\n')
+
+ def indent(
+ self,
+ amount: int = INDENT_WIDTH,
+ ) -> 'OutputFile._IndentationContext':
+ """Increases the indentation level of the output."""
+ return self._IndentationContext(self, amount)
+
+ def name(self) -> str:
+ return self._filename
+
+ def content(self) -> str:
+ return ''.join(self._content)
+
+ class _IndentationContext:
+ """Context that increases the output's indentation when it is active."""
+ def __init__(self, output: 'OutputFile', amount: int):
+ self._output = output
+ self._amount: int = amount
+
+ def __enter__(self):
+ self._output._indentation += self._amount
+
+ def __exit__(self, typ, value, traceback):
+ self._output._indentation -= self._amount
diff --git a/pw_protobuf/py/pw_protobuf/proto_tree.py b/pw_protobuf/py/pw_protobuf/proto_tree.py
index 7b9e361..9313689 100644
--- a/pw_protobuf/py/pw_protobuf/proto_tree.py
+++ b/pw_protobuf/py/pw_protobuf/proto_tree.py
@@ -70,6 +70,11 @@
return '::'.join(
self._attr_hierarchy(lambda node: node.cpp_name(), root))
+ def nanopb_name(self) -> str:
+ """Full nanopb-style name of the node."""
+ name = '_'.join(self._attr_hierarchy(lambda node: node.name(), None))
+ return name.lstrip('_')
+
def common_ancestor(self, other: 'ProtoNode') -> Optional['ProtoNode']:
"""Finds the earliest common ancestor of this node and other."""
@@ -329,6 +334,15 @@
def name(self) -> str:
return self._name
+ def type(self) -> Type:
+ return self._type
+
+ def request_type(self) -> ProtoNode:
+ return self._request_type
+
+ def response_type(self) -> ProtoNode:
+ return self._response_type
+
def _add_enum_fields(enum_node: ProtoNode, proto_enum) -> None:
"""Adds fields from a protobuf enum descriptor to an enum node."""
diff --git a/pw_rpc/BUILD b/pw_rpc/BUILD
index b2f3f2c..36e8c8b 100644
--- a/pw_rpc/BUILD
+++ b/pw_rpc/BUILD
@@ -90,9 +90,13 @@
filegroup(
name = "nanopb",
srcs = [
+ "nanopb/codegen_manual_test.cc",
"nanopb/method.cc",
"nanopb/method_test.cc",
"nanopb/public_overrides/pw_rpc/internal/method.h",
+ "nanopb/test.pb.c",
+ "nanopb/test.pb.h",
+ "nanopb/test_rpc.pb.h",
],
)
diff --git a/pw_rpc/pw_rpc_test_protos/test.proto b/pw_rpc/pw_rpc_test_protos/test.proto
index 48e8cc5..3a67f91 100644
--- a/pw_rpc/pw_rpc_test_protos/test.proto
+++ b/pw_rpc/pw_rpc_test_protos/test.proto
@@ -23,4 +23,13 @@
int32 value = 1;
}
+message TestStreamResponse {
+ bytes chunk = 1;
+}
+
message Empty {}
+
+service TestService {
+ rpc TestRpc(TestRequest) returns (TestResponse) {}
+ rpc TestStreamRpc(Empty) returns (stream TestStreamResponse) {}
+}
diff --git a/pw_rpc/py/codegen_test.py b/pw_rpc/py/codegen_test.py
new file mode 100644
index 0000000..d72b6e5
--- /dev/null
+++ b/pw_rpc/py/codegen_test.py
@@ -0,0 +1,138 @@
+#!/usr/bin/env python3
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""Tests the generated pw_rpc code."""
+
+from pathlib import Path
+import os
+import subprocess
+import tempfile
+import unittest
+
+TEST_PROTO_FILE = b"""\
+syntax = "proto3";
+
+package pw.rpc.test;
+
+message TestRequest {
+ float integer = 1;
+}
+
+message TestResponse {
+ int32 value = 1;
+}
+
+message TestStreamResponse {
+ bytes chunk = 1;
+}
+
+message Empty {}
+
+service TestService {
+ rpc TestRpc(TestRequest) returns (TestResponse) {}
+ rpc TestStreamRpc(Empty) returns (stream TestStreamResponse) {}
+}
+"""
+
+EXPECTED_NANOPB_CODE = """\
+#pragma once
+
+#include <cstddef>
+#include <cstdint>
+
+#include "pw_rpc/internal/service.h"
+#include "test.pb.h"
+
+
+namespace pw::rpc::test {
+
+class TestService : public ::pw::rpc::internal::Service {
+ public:
+ using ServerContext = ::pw::rpc::ServerContext;
+ template <typename T> using ServerWriter = ::pw::rpc::ServerWriter<T>;
+
+ constexpr TestService() : ::pw::rpc::internal::Service(kServiceId, kMethods) {}
+
+ TestService(const TestService&) = delete;
+ TestService& operator=(const TestService&) = delete;
+
+ static ::pw::Status TestRpc(
+ ServerContext& ctx,
+ const pw_rpc_test_TestRequest& request,
+ pw_rpc_test_TestResponse& response);
+
+ static ::pw::Status TestStreamRpc(
+ ServerContext& ctx,
+ const pw_rpc_test_Empty& request,
+ ServerWriter<pw_rpc_test_TestStreamResponse>& writer);
+
+ private:
+ // 65599 hash of "TestService".
+ static constexpr uint32_t kServiceId = 0x105b6ac8;
+ static constexpr char* kServiceName = "TestService";
+
+ static constexpr std::array<::pw::rpc::internal::Method, 2> kMethods = {
+ ::pw::rpc::internal::Method::Unary<TestRpc>(
+ 0xbc924054, // 65599 hash of "TestRpc"
+ pw_rpc_test_TestRequest_fields,
+ pw_rpc_test_TestResponse_fields),
+ ::pw::rpc::internal::Method::ServerStreaming<TestStreamRpc>(
+ 0xd97a28fa, // 65599 hash of "TestStreamRpc"
+ pw_rpc_test_Empty_fields,
+ pw_rpc_test_TestStreamResponse_fields),
+ };
+};
+
+} // namespace pw::rpc::test
+"""
+
+
+class TestNanopbCodegen(unittest.TestCase):
+ """Test case for nanopb code generation."""
+ def setUp(self):
+ self._output_dir = tempfile.TemporaryDirectory()
+
+ def tearDown(self):
+ self._output_dir.cleanup()
+
+ def test_nanopb_codegen(self):
+ root = Path(os.getenv('PW_ROOT'))
+ proto_dir = root / 'pw_rpc' / 'pw_rpc_test_protos'
+ proto_file = proto_dir / 'test.proto'
+
+ venv_bin = 'Scripts' if os.name == 'nt' else 'bin'
+ plugin = root / '.python3-env' / venv_bin / 'pw_rpc_codegen'
+
+ command = (
+ 'protoc',
+ f'-I{proto_dir}',
+ proto_file,
+ '--plugin',
+ f'protoc-gen-custom={plugin}',
+ '--custom_out',
+ self._output_dir.name,
+ )
+
+ subprocess.run(command)
+
+ generated_files = os.listdir(self._output_dir.name)
+ self.assertEqual(len(generated_files), 1)
+ self.assertEqual(generated_files[0], 'test_rpc.pb.h')
+
+ # Read the generated file, ignoring its preamble.
+ generated_code = Path(self._output_dir.name,
+ generated_files[0]).read_text()
+ generated_code = generated_code[generated_code.index('#pragma'):]
+
+ self.assertEqual(generated_code, EXPECTED_NANOPB_CODE)
diff --git a/pw_rpc/py/pw_rpc/codegen_nanopb.py b/pw_rpc/py/pw_rpc/codegen_nanopb.py
new file mode 100644
index 0000000..27b2573
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/codegen_nanopb.py
@@ -0,0 +1,200 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""This module generates the code for nanopb-based pw_rpc services."""
+
+from datetime import datetime
+import os
+from typing import Iterable
+
+from pw_protobuf.output_file import OutputFile
+from pw_protobuf.proto_tree import ProtoNode, ProtoServiceMethod
+from pw_protobuf.proto_tree import build_node_tree
+import pw_rpc.ids
+
+PLUGIN_NAME = 'pw_rpc_codegen'
+PLUGIN_VERSION = '0.1.0'
+
+PROTO_H_EXTENSION = '.pb.h'
+PROTO_CC_EXTENSION = '.pb.cc'
+NANOPB_H_EXTENSION = '.pb.h'
+
+RPC_NAMESPACE = '::pw::rpc'
+
+
+def _proto_filename_to_nanopb_header(proto_file: str) -> str:
+ """Returns the generated nanopb header name for a .proto file."""
+ return os.path.splitext(proto_file)[0] + NANOPB_H_EXTENSION
+
+
+def _proto_filename_to_generated_header(proto_file: str) -> str:
+ """Returns the generated C++ RPC header name for a .proto file."""
+ filename = os.path.splitext(proto_file)[0]
+ return f'{filename}_rpc{PROTO_H_EXTENSION}'
+
+
+def _generate_method_descriptor(method: ProtoServiceMethod,
+ output: OutputFile) -> None:
+ """Generates a nanopb method descriptor for an RPC method."""
+
+ method_class = f'{RPC_NAMESPACE}::internal::Method'
+
+ if method.type() == ProtoServiceMethod.Type.UNARY:
+ func = f'{method_class}::Unary<{method.name()}>'
+ elif method.type() == ProtoServiceMethod.Type.SERVER_STREAMING:
+ func = f'{method_class}::ServerStreaming<{method.name()}>'
+ else:
+ raise NotImplementedError(
+ 'Only unary and server streaming RPCs are currently supported')
+
+ method_id = pw_rpc.ids.calculate(method.name())
+ req_fields = f'{method.request_type().nanopb_name()}_fields'
+ res_fields = f'{method.response_type().nanopb_name()}_fields'
+
+ output.write_line(f'{func}(')
+ with output.indent(4):
+ output.write_line(
+ f'{hex(method_id)}, // 65599 hash of "{method.name()}"')
+ output.write_line(f'{req_fields},')
+ output.write_line(f'{res_fields}),')
+
+
+def _generate_code_for_method(method: ProtoServiceMethod,
+ output: OutputFile) -> None:
+ """Generates the function singature of a nanopb RPC method."""
+
+ req_type = method.request_type().nanopb_name()
+ res_type = method.response_type().nanopb_name()
+ signature = f'static ::pw::Status {method.name()}'
+
+ output.write_line()
+
+ if method.type() == ProtoServiceMethod.Type.UNARY:
+ output.write_line(f'{signature}(')
+ with output.indent(4):
+ output.write_line('ServerContext& ctx,')
+ output.write_line(f'const {req_type}& request,')
+ output.write_line(f'{res_type}& response);')
+ elif method.type() == ProtoServiceMethod.Type.SERVER_STREAMING:
+ output.write_line(f'{signature}(')
+ with output.indent(4):
+ output.write_line('ServerContext& ctx,')
+ output.write_line(f'const {req_type}& request,')
+ output.write_line(f'ServerWriter<{res_type}>& writer);')
+ else:
+ raise NotImplementedError(
+ 'Only unary and server streaming RPCs are currently supported')
+
+
+def _generate_code_for_service(service: ProtoNode, root: ProtoNode,
+ output: OutputFile) -> None:
+ """Generates a C++ derived class for a nanopb RPC service."""
+
+ base_class = f'{RPC_NAMESPACE}::internal::Service'
+ output.write_line(
+ f'\nclass {service.cpp_namespace(root)} : public {base_class} {{')
+ output.write_line(' public:')
+
+ with output.indent():
+ output.write_line(
+ f'using ServerContext = {RPC_NAMESPACE}::ServerContext;')
+ output.write_line('template <typename T> using ServerWriter = '
+ f'{RPC_NAMESPACE}::ServerWriter<T>;')
+ output.write_line()
+
+ output.write_line(f'constexpr {service.name()}()'
+ f' : {base_class}(kServiceId, kMethods) {{}}')
+
+ output.write_line()
+ output.write_line(
+ f'{service.name()}(const {service.name()}&) = delete;')
+ output.write_line(f'{service.name()}& operator='
+ f'(const {service.name()}&) = delete;')
+
+ for method in service.methods():
+ _generate_code_for_method(method, output)
+
+ service_name_hash = pw_rpc.ids.calculate(service.name())
+ output.write_line('\n private:')
+
+ with output.indent():
+ output.write_line(f'// 65599 hash of "{service.name()}".')
+ output.write_line(
+ f'static constexpr uint32_t kServiceId = {hex(service_name_hash)};'
+ )
+ output.write_line(
+ f'static constexpr char* kServiceName = "{service.name()}";')
+ output.write_line()
+
+ output.write_line(
+ f'static constexpr std::array<{RPC_NAMESPACE}::internal::Method,'
+ f' {len(service.methods())}> kMethods = {{')
+
+ with output.indent(4):
+ for method in service.methods():
+ _generate_method_descriptor(method, output)
+
+ output.write_line('};')
+
+ output.write_line('};')
+
+
+def generate_code_for_package(file_descriptor_proto, package: ProtoNode,
+ output: OutputFile) -> None:
+ """Generates code for a header file corresponding to a .proto file."""
+
+ assert package.type() == ProtoNode.Type.PACKAGE
+
+ output.write_line(f'// {os.path.basename(output.name())} automatically '
+ f'generated by {PLUGIN_NAME} {PLUGIN_VERSION}')
+ output.write_line(f'// on {datetime.now()}')
+ output.write_line('// clang-format off')
+ output.write_line('#pragma once\n')
+ output.write_line('#include <cstddef>')
+ output.write_line('#include <cstdint>\n')
+ output.write_line('#include "pw_rpc/internal/service.h"')
+
+ # Include the corresponding nanopb header file for this proto file, in which
+ # the file's messages and enums are generated.
+ nanopb_header = _proto_filename_to_nanopb_header(
+ file_descriptor_proto.name)
+ output.write_line(f'#include "{nanopb_header}"\n')
+
+ for imported_file in file_descriptor_proto.dependency:
+ generated_header = _proto_filename_to_nanopb_header(imported_file)
+ output.write_line(f'#include "{generated_header}"')
+
+ if package.cpp_namespace():
+ file_namespace = package.cpp_namespace()
+ if file_namespace.startswith('::'):
+ file_namespace = file_namespace[2:]
+
+ output.write_line(f'\nnamespace {file_namespace} {{')
+
+ for node in package:
+ if node.type() == ProtoNode.Type.SERVICE:
+ _generate_code_for_service(node, package, output)
+
+ if package.cpp_namespace():
+ output.write_line(f'\n}} // namespace {file_namespace}')
+
+
+def process_proto_file(proto_file) -> Iterable[OutputFile]:
+ """Generates code for a single .proto file."""
+
+ _, package_root = build_node_tree(proto_file)
+ output_filename = _proto_filename_to_generated_header(proto_file.name)
+ output_file = OutputFile(output_filename)
+ generate_code_for_package(proto_file, package_root, output_file)
+
+ return [output_file]
diff --git a/pw_rpc/py/pw_rpc/ids.py b/pw_rpc/py/pw_rpc/ids.py
new file mode 100644
index 0000000..fa56f02
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/ids.py
@@ -0,0 +1,34 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""This module defines the string to ID hash used in pw_rpc."""
+
+HASH_CONSTANT = 65599
+
+
+# This is the same hash function that is used in pw_tokenizer, with the maximum
+# length removed. It is chosen due to its simplicity. The tokenizer code is
+# duplicated here to avoid unnecessary dependencies between modules.
+def hash_65599(string: str) -> int:
+ hash_value = len(string)
+ coefficient = HASH_CONSTANT
+
+ for char in string:
+ hash_value = (hash_value + coefficient * ord(char)) % 2**32
+ coefficient = (coefficient * HASH_CONSTANT) % 2**32
+
+ return hash_value
+
+
+# Function that converts a name to an ID.
+calculate = hash_65599
diff --git a/pw_rpc/py/pw_rpc/plugin.py b/pw_rpc/py/pw_rpc/plugin.py
new file mode 100644
index 0000000..a7ea616
--- /dev/null
+++ b/pw_rpc/py/pw_rpc/plugin.py
@@ -0,0 +1,60 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_rpc compiler plugin.
+
+protoc plugin which generates C++ code for pw_rpc services using nanopb.
+"""
+
+import sys
+
+import google.protobuf.compiler.plugin_pb2 as plugin_pb2
+
+import pw_rpc.codegen_nanopb as codegen_nanopb
+
+
+def process_proto_request(req: plugin_pb2.CodeGeneratorRequest,
+ res: plugin_pb2.CodeGeneratorResponse) -> None:
+ """Handles a protoc CodeGeneratorRequest message.
+
+ Generates code for the files in the request and writes the output to the
+ specified CodeGeneratorResponse message.
+
+ Args:
+ req: A CodeGeneratorRequest for a proto compilation.
+ res: A CodeGeneratorResponse to populate with the plugin's output.
+ """
+ for proto_file in req.proto_file:
+ output_files = codegen_nanopb.process_proto_file(proto_file)
+ for output_file in output_files:
+ fd = res.file.add()
+ fd.name = output_file.name()
+ fd.content = output_file.content()
+
+
+def main() -> int:
+ """Protobuf compiler plugin entrypoint.
+
+ Reads a CodeGeneratorRequest proto from stdin and writes a
+ CodeGeneratorResponse to stdout.
+ """
+ data = sys.stdin.buffer.read()
+ request = plugin_pb2.CodeGeneratorRequest.FromString(data)
+ response = plugin_pb2.CodeGeneratorResponse()
+ process_proto_request(request, response)
+ sys.stdout.buffer.write(response.SerializeToString())
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/pw_rpc/py/setup.py b/pw_rpc/py/setup.py
new file mode 100644
index 0000000..07c6b2c
--- /dev/null
+++ b/pw_rpc/py/setup.py
@@ -0,0 +1,30 @@
+# Copyright 2020 The Pigweed Authors
+#
+# 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
+#
+# https://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.
+"""pw_protobuf"""
+
+import setuptools
+
+setuptools.setup(
+ name='pw_rpc',
+ version='0.0.1',
+ author='Pigweed Authors',
+ author_email='pigweed-developers@googlegroups.com',
+ description='On-device remote procedure calls',
+ packages=setuptools.find_packages(),
+ entry_points={'console_scripts': ['pw_rpc_codegen = pw_rpc.plugin:main']},
+ install_requires=[
+ 'protobuf',
+ 'pw_protobuf',
+ ],
+)