126 lines
4.4 KiB
Python
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())
|