Merge pull request #463 from theunkn0wn1/feature/grpcio_tools_protoc

use grpcio-tools protoc instead of system protoc
diff --git a/extra/poetry/pyproject.toml b/extra/poetry/pyproject.toml
index cc1a5bd..5b33dfe 100644
--- a/extra/poetry/pyproject.toml
+++ b/extra/poetry/pyproject.toml
@@ -18,9 +18,13 @@
 [tool.poetry.dependencies]
 python = ">=2.7"
 protobuf = ">=3.6"
+grpcio-tools = {version = ">=1.26.0rc1", allow-prereleases = true, optional=true}
 
 [tool.poetry.dev-dependencies]
 
+[tool.poetry.extras]
+grpcio-tools = ["grpcio-tools"]
+
 [build-system]
 requires = ["poetry>=0.12"]
 build-backend = "poetry.masonry.api"
diff --git a/generator/proto/__init__.py b/generator/proto/__init__.py
index 93e7b33..29153d4 100644
--- a/generator/proto/__init__.py
+++ b/generator/proto/__init__.py
@@ -1,9 +1,12 @@
 '''This file automatically rebuilds the proto definitions for Python.'''
+from __future__ import absolute_import
 
-import os
 import os.path
 import sys
-import subprocess
+
+import pkg_resources
+
+from ._utils import has_grpcio_protoc, invoke_protoc
 
 dirname = os.path.dirname(__file__)
 protosrc = os.path.join(dirname, "nanopb.proto")
@@ -12,8 +15,22 @@
 if os.path.isfile(protosrc):
     src_date = os.path.getmtime(protosrc)
     if not os.path.isfile(protodst) or os.path.getmtime(protodst) < src_date:
-        cmd = ["protoc", "--python_out=.", "nanopb.proto"]
-        status = subprocess.call(cmd, cwd = dirname)
-        if status != 0:
-            sys.stderr.write("Failed to build nanopb_pb2.py: " + ' '.join(cmd) + "\n")
 
+        cmd = [
+            "protoc",
+            "--python_out={}".format(dirname),
+            protosrc,
+            "-I={}".format(dirname),
+        ]
+
+        if has_grpcio_protoc():
+            # grpcio-tools has an extra CLI argument
+            # from grpc.tools.protoc __main__ invocation.
+            _builtin_proto_include = pkg_resources.resource_filename('grpc_tools', '_proto')
+
+            cmd.append("-I={}".format(_builtin_proto_include))
+        try:
+            invoke_protoc(argv=cmd)
+        except:
+            sys.stderr.write("Failed to build nanopb_pb2.py: " + ' '.join(cmd) + "\n")
+            raise
diff --git a/generator/proto/_utils.py b/generator/proto/_utils.py
new file mode 100644
index 0000000..5cbc650
--- /dev/null
+++ b/generator/proto/_utils.py
@@ -0,0 +1,43 @@
+import subprocess
+
+
+def has_system_protoc():
+    # type: () -> bool
+    """ checks if a system-installed `protoc` executable exists """
+
+    try:
+        subprocess.check_call("protoc --version".split())
+    except FileNotFoundError:
+        return False
+    return True
+
+
+def has_grpcio_protoc():
+    # type: () -> bool
+    """ checks if grpcio-tools protoc is installed"""
+
+    try:
+        import grpc_tools.protoc
+    except ImportError:
+        return False
+    return True
+
+
+def invoke_protoc(argv):
+    # type: (list) -> typing.Any
+    """
+    Invoke protoc.
+
+    This routine will use grpcio-provided protoc if it exists,
+    using system-installed protoc as a fallback.
+
+    Args:
+        argv: protoc CLI invocation
+    """
+    if has_grpcio_protoc():
+        import grpc_tools.protoc as protoc
+        return protoc.main(argv)
+    if has_system_protoc():
+        return subprocess.check_call(argv)
+
+    raise FileNotFoundError("Neither grpc-tools nor system provided protoc could be found.")