generate-buf-config.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. #!/usr/bin/env python3
  2. # Copyright Materialize, Inc. and contributors. All rights reserved.
  3. #
  4. # Use of this software is governed by the Business Source License
  5. # included in the LICENSE file at the root of this repository.
  6. #
  7. # As of the Change Date specified in that file, in accordance with
  8. # the Business Source License, use of this software will be governed
  9. # by the Apache License, Version 2.0.
  10. import glob
  11. import os
  12. import re
  13. BUF_YAML_FILE_PATH = "./src/buf.yaml"
  14. BUF_YAML_TEMPLATE_FILE_PATH = f"{BUF_YAML_FILE_PATH}.template"
  15. SOURCE_DIR = "src/"
  16. PROTO_FILE_GLOB = f"{SOURCE_DIR}**/*.proto"
  17. GENERATION_COMMENT = "File generated by generate-buf-config.py - DO NOT EDIT"
  18. BUF_INSTRUCTION_PREFIX = "// buf breaking:"
  19. # matched examples: "ignore", "ignore (database-issues#1000 must be fixed)"
  20. BUF_IGNORE_COMMAND = re.compile(r"ignore( \((.*)\))?")
  21. class ProtoFile:
  22. def __init__(self, path: str):
  23. self.path = path
  24. self.ignore_reason: str | None = None
  25. def is_ignore(self) -> bool:
  26. return self.ignore_reason is not None
  27. def collect_proto_files() -> list[ProtoFile]:
  28. print(f"Working dir: {os.getcwd()}")
  29. proto_file_paths = glob.glob(PROTO_FILE_GLOB, recursive=True)
  30. return [ProtoFile(path) for path in proto_file_paths]
  31. def load_buf_instructions(files: list[ProtoFile]) -> None:
  32. for file in files:
  33. load_buf_instructions_for_file(file)
  34. def load_buf_instructions_for_file(file: ProtoFile) -> None:
  35. with open(file.path) as lines:
  36. for line in lines:
  37. if line.startswith(BUF_INSTRUCTION_PREFIX):
  38. handle_buf_instruction_in_proto_file(file, line)
  39. def handle_buf_instruction_in_proto_file(file: ProtoFile, line: str) -> None:
  40. command = line.removeprefix(BUF_INSTRUCTION_PREFIX).strip()
  41. match = BUF_IGNORE_COMMAND.search(command)
  42. if match:
  43. has_ignore_reason = len(match.groups()) == 2 and match.group(2)
  44. # groups are not zero-based because group 0 contains the whole match
  45. file.ignore_reason = (
  46. match.group(2) if has_ignore_reason else "no reason specified"
  47. )
  48. else:
  49. raise RuntimeError(f"Unsupported buf instruction in {file.path}: {line}")
  50. def generate_buf_ignore_section(ignored_files: list[ProtoFile]) -> str:
  51. ignore_entry_lines = []
  52. ignored_files = sorted(ignored_files, key=lambda file: file.path)
  53. for ignored_file in ignored_files:
  54. relative_path = ignored_file.path.removeprefix(SOURCE_DIR)
  55. ignore_entry_lines.append(f" # reason: {ignored_file.ignore_reason}")
  56. ignore_entry_lines.append(f" - {relative_path}")
  57. if len(ignore_entry_lines) == 0:
  58. ignore_entry_lines.append(" # none")
  59. return "\n".join(ignore_entry_lines).strip()
  60. def write_buf_configuration(
  61. template_path: str, target_path: str, ignored_files: list[ProtoFile]
  62. ) -> None:
  63. with open(template_path) as input_file:
  64. content = input_file.read()
  65. content = content.replace("${generation-comment}", GENERATION_COMMENT)
  66. content = content.replace(
  67. "${ignore-entries}", generate_buf_ignore_section(ignored_files)
  68. )
  69. with open(target_path, "w") as output_file:
  70. output_file.write(content)
  71. def main() -> None:
  72. proto_files = collect_proto_files()
  73. print(f"Collected {len(proto_files)} proto files.")
  74. load_buf_instructions(proto_files)
  75. ignored_proto_files = [file for file in proto_files if file.is_ignore()]
  76. print(f"{len(ignored_proto_files)} proto files to be ignored from breaking check.")
  77. write_buf_configuration(
  78. BUF_YAML_TEMPLATE_FILE_PATH, BUF_YAML_FILE_PATH, ignored_proto_files
  79. )
  80. print(f"Written buf configuration to '{BUF_YAML_FILE_PATH}'.")
  81. if __name__ == "__main__":
  82. main()