#!/usr/bin/env python3 # Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. # See LICENSE.txt for license information. """ Protocol Buffer and gRPC code generator for the Mattermost Plugin SDK. This script generates Python code and type stubs from the .proto files in the server/public/pluginapi/grpc/proto directory. Usage: cd python-sdk python scripts/generate_protos.py Prerequisites: pip install grpcio-tools mypy-protobuf Generated files: - src/mattermost_plugin/grpc/*_pb2.py (Protocol Buffer messages) - src/mattermost_plugin/grpc/*_pb2_grpc.py (gRPC service stubs) - src/mattermost_plugin/grpc/*_pb2.pyi (Type stubs for messages) - src/mattermost_plugin/grpc/*_pb2_grpc.pyi (Type stubs for services) """ import subprocess import sys from pathlib import Path def get_repo_root() -> Path: """Find the repository root by looking for .git directory.""" current = Path(__file__).resolve().parent while current != current.parent: if (current / ".git").exists(): return current # Also check parent in case we're in python-sdk/scripts parent = current.parent if (parent / ".git").exists(): return parent current = parent raise RuntimeError("Could not find repository root (.git directory)") def main() -> int: """Generate Protocol Buffer and gRPC code from .proto files.""" # Determine paths script_dir = Path(__file__).resolve().parent sdk_root = script_dir.parent repo_root = get_repo_root() proto_dir = repo_root / "server" / "public" / "pluginapi" / "grpc" / "proto" output_dir = sdk_root / "src" / "mattermost_plugin" / "grpc" # Validate paths if not proto_dir.exists(): print(f"ERROR: Proto directory not found: {proto_dir}", file=sys.stderr) return 1 # Ensure output directory exists output_dir.mkdir(parents=True, exist_ok=True) # Find all .proto files proto_files = sorted(proto_dir.glob("*.proto")) if not proto_files: print(f"ERROR: No .proto files found in {proto_dir}", file=sys.stderr) return 1 print(f"Found {len(proto_files)} .proto files in {proto_dir}") for pf in proto_files: print(f" - {pf.name}") # Build the protoc command # We need to include the Google well-known types path cmd = [ sys.executable, "-m", "grpc_tools.protoc", f"-I{proto_dir}", f"--python_out={output_dir}", f"--grpc_python_out={output_dir}", f"--mypy_out={output_dir}", f"--mypy_grpc_out={output_dir}", ] + [str(pf) for pf in proto_files] print(f"\nRunning: {' '.join(cmd[:6])} [... {len(proto_files)} proto files]") try: result = subprocess.run(cmd, check=True, capture_output=True, text=True) if result.stdout: print(result.stdout) except subprocess.CalledProcessError as e: print(f"ERROR: protoc failed with exit code {e.returncode}", file=sys.stderr) if e.stderr: print(e.stderr, file=sys.stderr) return 1 except FileNotFoundError: print( "ERROR: grpcio-tools not found. Install with:\n" " pip install grpcio-tools mypy-protobuf", file=sys.stderr ) return 1 # Fix imports in generated files (convert absolute to relative imports) # Python protoc generates absolute imports like "import common_pb2" # but we need "from . import common_pb2" for package-relative imports fix_imports(output_dir) # Verify generated files generated = list(output_dir.glob("*_pb2*.py")) + list(output_dir.glob("*_pb2*.pyi")) print(f"\nGenerated {len(generated)} files in {output_dir}") # Check that we have the key files required_files = [ "api_pb2.py", "api_pb2_grpc.py", "common_pb2.py", ] missing = [f for f in required_files if not (output_dir / f).exists()] if missing: print(f"ERROR: Missing required files: {missing}", file=sys.stderr) return 1 print("\nCode generation completed successfully!") return 0 def fix_imports(output_dir: Path) -> None: """ Fix imports in generated Python files. The protoc compiler generates imports like: import common_pb2 as common__pb2 But for proper package imports, we need: from . import common_pb2 as common__pb2 """ import re # Pattern to match: import _pb2 as # or: from _pb2 import # We need to convert to relative imports import_pattern = re.compile( r'^import ((?:api|common|user|team|channel|post|file|hooks|api_\w+|hooks_\w+)_pb2(?:_grpc)?)' r'(?: as (\w+))?$', re.MULTILINE ) from_import_pattern = re.compile( r'^from ((?:api|common|user|team|channel|post|file|hooks|api_\w+|hooks_\w+)_pb2(?:_grpc)?)' r' import (.+)$', re.MULTILINE ) for py_file in output_dir.glob("*.py"): content = py_file.read_text() original = content # Replace "import foo_pb2 as bar" with "from . import foo_pb2 as bar" def replace_import(match: re.Match[str]) -> str: module = match.group(1) alias = match.group(2) if alias: return f"from . import {module} as {alias}" return f"from . import {module}" content = import_pattern.sub(replace_import, content) # Replace "from foo_pb2 import X" with "from .foo_pb2 import X" def replace_from_import(match: re.Match[str]) -> str: module = match.group(1) imports = match.group(2) return f"from .{module} import {imports}" content = from_import_pattern.sub(replace_from_import, content) if content != original: py_file.write_text(content) print(f" Fixed imports in {py_file.name}") if __name__ == "__main__": sys.exit(main())