Files
neuron-tai/scripts/generate_native_protocol.py
2026-07-13 16:00:49 +03:00

126 lines
4.4 KiB
Python

#!/usr/bin/env python3
"""Generate the Python Shard-protocol stubs from `shard_runtime.proto`.
The `.proto` file is the contract; the Python modules under
`meshnet_node/native_protocol/generated/` are build output that happens to be
committed. They are committed so that installing the node does not require a
protoc toolchain, and `--check` exists so a committed stub can never silently
drift from the schema it claims to implement.
Usage::
python scripts/generate_native_protocol.py # regenerate in place
python scripts/generate_native_protocol.py --check # fail if out of date
C++ stubs are *not* generated here. They are build artifacts produced by CMake
(`packages/node/native/CMakeLists.txt`) into the build tree, because a C++ build
already requires a toolchain and nothing is gained by committing them.
"""
from __future__ import annotations
import argparse
import pathlib
import shutil
import subprocess
import sys
import tempfile
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
PROTO_DIR = REPO_ROOT / "packages/node/native/proto"
PROTO_FILE = PROTO_DIR / "shard_runtime.proto"
OUT_DIR = REPO_ROOT / "packages/node/meshnet_node/native_protocol/generated"
# Regenerating with a different protoc emits different gencode headers, so the
# generator version is part of the contract and `--check` would catch a drift.
REQUIRED_GRPCIO_TOOLS = "1.82.1"
_HEADER = "# Generated by scripts/generate_native_protocol.py. Do not edit.\n"
def _generate(into: pathlib.Path) -> None:
"""Run protoc, writing generated modules into `into`."""
try:
from grpc_tools import protoc
except ImportError: # pragma: no cover - exercised only without the toolchain
sys.exit(
"grpc_tools is required to generate stubs:\n"
f" pip install grpcio-tools=={REQUIRED_GRPCIO_TOOLS}"
)
into.mkdir(parents=True, exist_ok=True)
# grpc_tools bundles protoc and the well-known types, so generation needs no
# system protoc and produces identical output on any machine.
well_known = pathlib.Path(protoc.__file__).parent / "_proto"
args = [
"protoc",
f"--proto_path={PROTO_DIR}",
f"--proto_path={well_known}",
f"--python_out={into}",
f"--pyi_out={into}",
f"--grpc_python_out={into}",
str(PROTO_FILE),
]
if protoc.main(args) != 0:
sys.exit("protoc failed")
# protoc emits `import shard_runtime_pb2` — a bare top-level import that only
# resolves if the generated directory happens to be on sys.path. Rewrite it
# to a relative import so the package works as an installed package.
grpc_module = into / "shard_runtime_pb2_grpc.py"
text = grpc_module.read_text()
text = text.replace(
"import shard_runtime_pb2 as shard__runtime__pb2",
"from . import shard_runtime_pb2 as shard__runtime__pb2",
)
grpc_module.write_text(text)
(into / "__init__.py").write_text(
_HEADER + '"""Generated protobuf/gRPC stubs for the native Shard protocol."""\n'
)
def _tracked_files(directory: pathlib.Path) -> dict[str, bytes]:
return {
path.name: path.read_bytes()
for path in sorted(directory.iterdir())
if path.is_file() and path.suffix in {".py", ".pyi"}
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--check",
action="store_true",
help="verify the committed stubs match the .proto instead of rewriting them",
)
args = parser.parse_args()
if args.check:
with tempfile.TemporaryDirectory() as tmp:
fresh = pathlib.Path(tmp) / "generated"
_generate(fresh)
if not OUT_DIR.is_dir():
print(f"generated stubs are missing: {OUT_DIR}", file=sys.stderr)
return 1
if _tracked_files(fresh) != _tracked_files(OUT_DIR):
print(
"generated stubs are out of date with shard_runtime.proto.\n"
"Run: python scripts/generate_native_protocol.py",
file=sys.stderr,
)
return 1
print("generated stubs are up to date")
return 0
if OUT_DIR.exists():
shutil.rmtree(OUT_DIR)
_generate(OUT_DIR)
print(f"wrote {OUT_DIR.relative_to(REPO_ROOT)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())