gen_dialect.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. """Regenerate motian_bridge/dialects/v20/VKFly.py from the dialect submodule.
  2. The generated module must never be hand-edited. Run this script after updating
  3. the `msg_definitions` submodule, then commit both the submodule pointer and the
  4. regenerated file.
  5. """
  6. import argparse
  7. import hashlib
  8. import logging
  9. import subprocess
  10. import sys
  11. import tempfile
  12. from pathlib import Path
  13. if __package__ in (None, ""):
  14. # Allow `python tools/gen_dialect.py` as well as `from tools import ...`.
  15. sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
  16. ROOT = Path(__file__).resolve().parent.parent
  17. OUTPUT = ROOT / "motian_bridge" / "dialects" / "v20" / "VKFly.py"
  18. SOURCE = ROOT / "msg_definitions" / "msg_definitions" / "VKFly.xml"
  19. INCLUDES = ("common.xml", "standard.xml", "minimal.xml")
  20. LOG = logging.getLogger("gen_dialect")
  21. def _sha256(path: Path) -> str:
  22. digest = hashlib.sha256()
  23. with open(path, "rb") as stream:
  24. for block in iter(lambda: stream.read(1 << 20), b""):
  25. digest.update(block)
  26. return digest.hexdigest()
  27. def check_sources() -> None:
  28. """Fail early with an actionable message instead of a mavgen traceback."""
  29. missing = [path for path in (SOURCE, *(SOURCE.parent / name for name in INCLUDES))
  30. if not path.is_file()]
  31. if missing:
  32. listing = "\n".join(f" - {path}" for path in missing)
  33. raise SystemExit(
  34. "MAVLink XML sources are missing:\n"
  35. f"{listing}\n"
  36. "Initialize the dialect submodule first:\n"
  37. " git submodule update --init --recursive"
  38. )
  39. def mavgen(extra_args: list[str], output: Path) -> None:
  40. command = [
  41. sys.executable, "-m", "pymavlink.tools.mavgen",
  42. "--lang", "Python",
  43. "--wire-protocol", "2.0",
  44. "--output", str(output),
  45. *extra_args,
  46. str(SOURCE),
  47. ]
  48. LOG.debug("running: %s", " ".join(command))
  49. subprocess.run(command, cwd=ROOT, check=True)
  50. def generate(dry_run: bool = False, check: bool = False, no_validate: bool = False) -> bool:
  51. """Generate the dialect. Returns True when the committed file was updated."""
  52. check_sources()
  53. previous = _sha256(OUTPUT) if OUTPUT.is_file() else None
  54. with tempfile.TemporaryDirectory(prefix="mavgen-") as directory:
  55. candidate = Path(directory) / "VKFly.py"
  56. mavgen(["--no-validate"] if no_validate else [], candidate)
  57. generated = candidate.read_bytes()
  58. if check:
  59. if previous is None:
  60. raise SystemExit(f"{OUTPUT} does not exist; run: python tools/gen_dialect.py")
  61. if _sha256(OUTPUT) != hashlib.sha256(generated).hexdigest():
  62. raise SystemExit(
  63. f"{OUTPUT} is stale. Regenerate with: python tools/gen_dialect.py"
  64. )
  65. LOG.info("generated dialect matches the submodule XML")
  66. return False
  67. if previous == hashlib.sha256(generated).hexdigest():
  68. LOG.info("generated dialect already up to date")
  69. return False
  70. if dry_run:
  71. LOG.info("dry run: %s would be regenerated", OUTPUT)
  72. return True
  73. OUTPUT.parent.mkdir(parents=True, exist_ok=True)
  74. OUTPUT.write_bytes(generated)
  75. LOG.info("wrote %s (%d bytes)", OUTPUT, len(generated))
  76. return True
  77. def main() -> None:
  78. parser = argparse.ArgumentParser(description=__doc__)
  79. parser.add_argument("--check", action="store_true",
  80. help="verify the committed file matches the XML; do not write")
  81. parser.add_argument("--dry-run", action="store_true",
  82. help="report whether regeneration is needed; do not write")
  83. parser.add_argument("--no-validate", action="store_true",
  84. help="pass --no-validate to mavgen (skips upstream enum warnings)")
  85. parser.add_argument("--quiet", action="store_true")
  86. args = parser.parse_args()
  87. logging.basicConfig(
  88. level=logging.WARNING if args.quiet else logging.INFO,
  89. format="%(levelname)s %(name)s: %(message)s",
  90. )
  91. try:
  92. generate(dry_run=args.dry_run, check=args.check, no_validate=args.no_validate)
  93. except subprocess.CalledProcessError as exc:
  94. raise SystemExit(f"mavgen failed with exit code {exc.returncode}")
  95. if __name__ == "__main__":
  96. main()