generate_pb.sh 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. #!/usr/bin/env bash
  2. # Regenerate the vendored Python protobuf + ConnectRPC bindings under
  3. # src/chattolib/_pb.
  4. #
  5. # Fetches the .proto sources for a pinned Chatto ref (default: a tag derived
  6. # from the version recorded in pyproject.toml) and runs protoc with the
  7. # built-in Python generator and the connect-python plugin.
  8. #
  9. # ALWAYS pin to a released tag. Fetching from `main` risks shipping
  10. # unreleased wire changes that the deployed Chatto server does not speak
  11. # — chattolib 0.4.19 shipped that way and broke realtime for every
  12. # downstream client. Override the pin explicitly if you know what you are
  13. # doing:
  14. #
  15. # CHATTO_REF=v0.4.19 ./scripts/generate_pb.sh
  16. # CHATTO_REF=main ./scripts/generate_pb.sh # unreleased
  17. #
  18. # Requirements:
  19. # - protoc on PATH
  20. # - protoc-gen-chattolib on PATH (bundled with chattolib itself — run
  21. # `pip install -e .` and it is on PATH; no separate install needed)
  22. # - curl on PATH
  23. #
  24. # Run from the repo root: ./scripts/generate_pb.sh
  25. set -euo pipefail
  26. if ! command -v protoc >/dev/null; then
  27. echo "protoc not found on PATH" >&2
  28. exit 1
  29. fi
  30. repo_root=$(cd "$(dirname "$0")/.." && pwd)
  31. cd "$repo_root"
  32. # Default the Chatto ref to a tag derived from `version` in pyproject.toml.
  33. # Strip any post-release suffix — those are chattolib-only fixes against the
  34. # same server release. Pre-releases keep their kind and number, because
  35. # Chatto tags them with SemVer dashes (PEP 440 0.5.0b6 -> v0.5.0-beta.6)
  36. # rather than the plain `v<base>` tag a final release uses. Candidates are
  37. # probed in preference order and the first existing tag wins. Callers can
  38. # override with CHATTO_REF.
  39. default_version=$(sed -nE 's/^version *= *\"([^\"]+)\".*/\1/p' pyproject.toml | head -1)
  40. default_base=${default_version%%.post*}
  41. default_base=${default_base%%.dev*}
  42. default_base=${default_base%%a*}
  43. default_base=${default_base%%b*}
  44. default_base=${default_base%%rc*}
  45. candidates="v${default_base}"
  46. # Map the PEP 440 pre-release onto the SemVer tag Chatto actually publishes.
  47. case ${default_version%%.post*} in
  48. *a[0-9]*) candidates="$candidates v${default_base}-alpha.${default_version##*a}" ;;
  49. *b[0-9]*) candidates="$candidates v${default_base}-beta.${default_version##*b}" ;;
  50. *rc[0-9]*) candidates="$candidates v${default_base}-rc.${default_version##*rc}" ;;
  51. esac
  52. # The REST /tags/{name} endpoint 404s on lightweight tags, so probe with
  53. # ls-remote instead: it sees annotated and lightweight tags alike, costs no
  54. # API quota, and does exact ref-name matching (no prefix false-positives).
  55. # Note: ls-remote exits 0 even when nothing matches, so test the output.
  56. tag_exists() {
  57. [ -n "$(git ls-remote --tags "https://github.com/chattocorp/chatto" "refs/tags/$1" 2>/dev/null)" ]
  58. }
  59. chatto_ref=${CHATTO_REF:-}
  60. if [ -z "$chatto_ref" ]; then
  61. for candidate in $candidates; do
  62. if tag_exists "$candidate"; then
  63. chatto_ref=$candidate
  64. break
  65. fi
  66. done
  67. if [ -z "$chatto_ref" ]; then
  68. echo "Could not resolve an existing chattocorp/chatto tag for version '$default_version' (tried: $candidates). Set CHATTO_REF explicitly." >&2
  69. exit 1
  70. fi
  71. fi
  72. echo "Fetching proto sources from chattocorp/chatto@${chatto_ref}"
  73. mkdir -p proto/chatto/{api,admin,auth,discovery,realtime}/v1 proto/buf/validate
  74. chatto_base="https://raw.githubusercontent.com/chattocorp/chatto/${chatto_ref}/proto/chatto"
  75. fetch() {
  76. local rel=$1
  77. curl -sfL "$chatto_base/${rel#chatto/}" -o "proto/$rel"
  78. echo " $rel"
  79. }
  80. echo "Fetching proto sources ..."
  81. # chatto.api.v1
  82. for f in \
  83. account.proto attachments.proto asset_uploads.proto common.proto \
  84. external_identities.proto link_previews.proto member_directory.proto \
  85. messages.proto message_types.proto \
  86. notifications.proto pagination.proto permissions.proto presence.proto \
  87. push_notifications.proto reactions.proto read_state.proto \
  88. room_directory.proto rooms.proto room_timeline.proto roles.proto \
  89. server.proto server_state.proto threads.proto user_service.proto \
  90. users.proto user_status.proto viewer.proto voice_calls.proto
  91. do
  92. fetch "chatto/api/v1/$f"
  93. done
  94. # chatto.admin.v1
  95. for f in \
  96. diagnostics.proto event_log.proto members.proto permissions.proto \
  97. roles.proto room_layout.proto server.proto
  98. do
  99. fetch "chatto/admin/v1/$f"
  100. done
  101. # chatto.auth.v1
  102. fetch chatto/auth/v1/external_identity_auth.proto
  103. # chatto.discovery.v1
  104. fetch chatto/discovery/v1/server.proto
  105. # chatto.realtime.v1
  106. fetch chatto/realtime/v1/realtime.proto
  107. fetch chatto/realtime/v1/events.proto
  108. # buf.validate — sourced from bufbuild/protovalidate
  109. curl -sfL \
  110. "https://raw.githubusercontent.com/bufbuild/protovalidate/main/proto/protovalidate/buf/validate/validate.proto" \
  111. -o proto/buf/validate/validate.proto
  112. echo " buf/validate/validate.proto"
  113. echo "Regenerating Python bindings ..."
  114. rm -rf src/chattolib/_pb/chatto src/chattolib/_pb/buf
  115. mkdir -p src/chattolib/_pb
  116. # Collect every input .proto so protoc's plugins visit each service.
  117. mapfile -t proto_files < <(find proto -type f -name '*.proto' | sort)
  118. protoc --proto_path=proto \
  119. --python_out=src/chattolib/_pb \
  120. "${proto_files[@]}"
  121. # Generate the Connect client stubs with chattolib's own plugin. The
  122. # realtime proto has no services, so it yields only *_pb2.py (above).
  123. # The plugin is a standalone script (scripts/protoc-gen-chattolib) loaded by
  124. # file path, so it runs even before the stubs it generates exist. We expose it
  125. # on PATH via a temp bin dir for the duration of this protoc invocation.
  126. if [ -f "$repo_root/scripts/protoc-gen-chattolib" ]; then
  127. _plugin_bin=$(mktemp -d)
  128. ln -s "$repo_root/scripts/protoc-gen-chattolib" "$_plugin_bin/protoc-gen-chattolib"
  129. PATH="$_plugin_bin:$PATH" protoc --proto_path=proto \
  130. --chattolib_out=src/chattolib/_pb \
  131. "${proto_files[@]}"
  132. rm -rf "$_plugin_bin"
  133. else
  134. echo "protoc-gen-chattolib not found; skipped connect stubs" >&2
  135. fi
  136. # Add __init__.py at every level so the generated modules form a package.
  137. find src/chattolib/_pb \( -path '*/chatto*' -o -path '*/buf*' \) -type d \
  138. | while read -r dir; do touch "$dir/__init__.py"; done
  139. echo "Done."