From d0d472aaa2edc93755f35644d4cfe827c213d663 Mon Sep 17 00:00:00 2001 From: Magne Sjaastad Date: Fri, 11 Sep 2026 11:40:58 +0200 Subject: [PATCH] #14723 Python: Pin grpcio-tools to keep protobuf 6 gencode loadable grpcio-tools 1.82.0+ (including its 1.82.0 pre-releases) bundles a protoc that emits protobuf 7.35.x gencode, which no protobuf 6 runtime can load. Pin grpcio-tools below that (excluding pre-releases via the '.dev0' trick, since a plain '<1.82' bound already let a 1.82.0rc release slip through once) and add a CI check that verifies the generated *_pb2.py files stay within the protobuf floor declared in pyproject.toml. Only the load-bearing bounds are kept: protobuf's '>=6.33.5' floor (must match the emitted gencode) and grpcio-tools' '<1.82.0.dev0' ceiling. Non-load-bearing bounds (protobuf's '<8', grpcio-tools' '>=1.81') were dropped since they don't change dependency resolution or protect against anything the above doesn't already cover. --- .github/workflows/ResInsightWithCache.yml | 7 ++ .../Python/check_gencode_compatibility.py | 77 +++++++++++++++++++ GrpcInterface/Python/pyproject.toml | 20 ++++- 3 files changed, 102 insertions(+), 2 deletions(-) create mode 100644 GrpcInterface/Python/check_gencode_compatibility.py diff --git a/.github/workflows/ResInsightWithCache.yml b/.github/workflows/ResInsightWithCache.yml index df957d047c..9f2d73d531 100644 --- a/.github/workflows/ResInsightWithCache.yml +++ b/.github/workflows/ResInsightWithCache.yml @@ -324,6 +324,13 @@ jobs: ls cmakebuild/${{ matrix.config.ri-unit-test-path }} cmakebuild/${{ matrix.config.ri-unit-test-path }} + - name: (Python) Verify protobuf gencode compatibility + if: matrix.config.build-python-module + shell: bash + run: | + cd GrpcInterface/Python + ${{ steps.python-path.outputs.PYTHON_EXECUTABLE }} check_gencode_compatibility.py + - name: (Python) Build Python module if: matrix.config.build-python-module shell: bash diff --git a/GrpcInterface/Python/check_gencode_compatibility.py b/GrpcInterface/Python/check_gencode_compatibility.py new file mode 100644 index 0000000000..7e38766700 --- /dev/null +++ b/GrpcInterface/Python/check_gencode_compatibility.py @@ -0,0 +1,77 @@ +"""Check that the generated protobuf code can be loaded by every supported runtime. + +The protobuf runtime refuses to load gencode newer than itself, so the gencode +version emitted by grpcio-tools must not exceed the lower bound declared for +protobuf in pyproject.toml. Nothing ties those two together: a grpcio-tools +upgrade can move the gencode a full major version without any edit here, and +the resulting package still installs cleanly and only fails at import time. +""" + +import re +import sys +import tomllib +from pathlib import Path + +from packaging.requirements import Requirement +from packaging.version import Version + +HERE = Path(__file__).parent +GENERATED = HERE / "rips" / "generated" +GENCODE_PATTERN = re.compile(r"^# Protobuf Python Version: (\S+)", re.MULTILINE) + + +def declared_protobuf_floor() -> Version: + """Lowest protobuf runtime a user of this package can end up with.""" + with (HERE / "pyproject.toml").open("rb") as f: + dependencies = tomllib.load(f)["project"]["dependencies"] + + requirements = [Requirement(d) for d in dependencies] + protobuf = next((r for r in requirements if r.name == "protobuf"), None) + if protobuf is None: + sys.exit("protobuf is not declared in [project].dependencies") + + floors = [ + Version(s.version) for s in protobuf.specifier if s.operator in (">=", "==") + ] + if not floors: + sys.exit( + f"protobuf dependency '{protobuf}' has no lower bound to check against" + ) + + return max(floors) + + +def gencode_versions() -> dict[str, Version]: + versions = {} + for path in sorted(GENERATED.glob("*_pb2.py")): + match = GENCODE_PATTERN.search(path.read_text(encoding="utf-8")) + if match is None: + sys.exit(f"{path.name} has no gencode version header") + versions[path.name] = Version(match.group(1)) + return versions + + +def main() -> None: + floor = declared_protobuf_floor() + versions = gencode_versions() + if not versions: + sys.exit(f"no generated *_pb2.py files in {GENERATED}, nothing was verified") + + too_new = {name: v for name, v in versions.items() if v > floor} + if too_new: + listing = "\n".join(f" {name}: gencode {v}" for name, v in too_new.items()) + sys.exit( + f"Generated code is newer than the oldest supported protobuf runtime ({floor}).\n" + f"{listing}\n" + "Pin grpcio-tools to a release emitting older gencode, or raise the protobuf " + "lower bound in pyproject.toml. Note that raising it drops support for runtimes " + "below the new bound." + ) + + print( + f"{len(versions)} generated files, gencode {max(versions.values())}, runtime floor {floor}: OK" + ) + + +if __name__ == "__main__": + main() diff --git a/GrpcInterface/Python/pyproject.toml b/GrpcInterface/Python/pyproject.toml index 9c73bb3cf6..9a26a9c861 100644 --- a/GrpcInterface/Python/pyproject.toml +++ b/GrpcInterface/Python/pyproject.toml @@ -13,13 +13,29 @@ requires-python = ">=3.11" authors = [{name = "Ceetron Solutions", email = "info@ceetronsolutions.com"}] dependencies = [ "grpcio", - "protobuf", + # Floor must match the gencode version emitted by the pinned grpcio-tools + # below: the protobuf runtime refuses to load gencode newer than itself. + # No upper bound: protobuf runtimes stay backward compatible with older + # gencode, so nothing here caps how new a runtime a user can install. + "protobuf>=6.33.5", "typing_extensions", ] [project.optional-dependencies] dev = [ - "grpcio-tools", + # 1.82.x bundles a protoc that emits protobuf 7.35.x gencode, which no + # protobuf 6 runtime can load - including its 1.82.0 pre-releases, which + # is why the bound goes below ".dev0" rather than just "<1.82": a plain + # "<1.82" bound already let a 1.82.0 release candidate slip through in + # CI once (pre-release versions sort below their final release). + # No lower bound: the resolver already prefers the newest version + # satisfying the upper bound, so ">=1.81" wasn't changing what gets + # installed, only guarding against a future resolver picking something + # much older if 1.81.x ever became unavailable. + # Generating 6.x gencode keeps rips importable on both 6.x and 7.x + # runtimes, so it stays installable next to packages that cap protobuf + # below 7 (ortools and friends). + "grpcio-tools<1.82.0.dev0", "pytest", "mypy", "types-protobuf",