From e7f98fd2e45286f15fe02ab0f995198f3120e17d Mon Sep 17 00:00:00 2001 From: Harshit Wandhare Date: Wed, 9 Sep 2026 12:54:20 -0500 Subject: [PATCH 1/2] examples: pin bsr-quickstart to one buffa release and check it in CI The example pinned the BSR plugin at v0.5.2 but compiled against the in-tree runtime through path dependencies, and its checked-in `src/gen/example.v1.rs` matched neither. On main it failed with four errors, the last of them a missing `merge_view_field` and a `to_owned_message` that is now infallible. The generated file had been hand-patched four times since it was generated (#184, #284, #409, #381) rather than regenerated, so it tracked the in-tree API by hand while still claiming to be published-plugin output. The example sits in the workspace `exclude` list, so nothing ever compiled it and the drift was invisible. Pinning the runtime back to the plugin's version alone does not fix it: against crates.io 0.5.2 the same file fails with three different errors, because it is no longer that plugin's output. Bumping only the plugin pin does not work either, since #381 now requires an `unsafe_impl_view_lifetime_parametric!` marker that no published plugin emits yet, so published v0.9.2 output does not compile against main. So the plugin pin moves to v0.9.2, `src/gen/` is regenerated from it, and the two crates move to the matching crates.io release. Both halves now come from one buffa version, which is also what a downstream BSR user has. A `cargo check` step in `lint-and-test` covers the example, and it can only go red if one pin moves without the other. Verified: cargo check, cargo run and clippy --all-targets -D warnings all clean. --- .../unreleased/fixed-20260909-125500.yaml | 4 + .github/workflows/ci.yml | 9 + examples/bsr-quickstart/Cargo.lock | 153 +++-- examples/bsr-quickstart/Cargo.toml | 25 +- examples/bsr-quickstart/buf.gen.yaml | 9 +- examples/bsr-quickstart/src/gen/example.v1.rs | 555 +++++++----------- 6 files changed, 346 insertions(+), 409 deletions(-) create mode 100644 .changes/unreleased/fixed-20260909-125500.yaml diff --git a/.changes/unreleased/fixed-20260909-125500.yaml b/.changes/unreleased/fixed-20260909-125500.yaml new file mode 100644 index 00000000..a5199d35 --- /dev/null +++ b/.changes/unreleased/fixed-20260909-125500.yaml @@ -0,0 +1,4 @@ +kind: Fixed +body: |- + **`examples/bsr-quickstart` compiles again, and CI checks it** (#427). Its checked-in generated code had been hand-patched across four releases while the plugin pin stayed at v0.5.2, so it matched neither the pinned plugin nor the in-tree runtime. The example now pins the BSR plugin and the `buffa` / `buffa-types` crates to the same release, and `cargo check` runs against it in CI. +time: 2026-09-09T12:55:00.000000000-07:00 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6021872..edf0b392 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,6 +93,15 @@ jobs: - name: Shared descriptor pool compile run: cargo test -p buffa-codegen --test shared_pool_compile -- --ignored + # `examples/bsr-quickstart` is outside the workspace, so nothing else + # compiles it. Its `src/gen/` is checked-in output from the BSR plugin + # pinned in that directory's `buf.gen.yaml`, and it drifted across four + # releases before anyone noticed (#427). The example pins the plugin and + # the buffa crates to the same release, so this only goes red if one of + # the two pins moves without the other. + - name: Check bsr-quickstart example + run: cargo check --manifest-path examples/bsr-quickstart/Cargo.toml + # ── Markdown lint ──────────────────────────────────────────────────────── # Same pinned markdownlint-cli version as `task lint-md`. Node is # preinstalled on ubuntu-latest so no setup-node step is needed. diff --git a/examples/bsr-quickstart/Cargo.lock b/examples/bsr-quickstart/Cargo.lock index 9e688c8f..8fcd8367 100644 --- a/examples/bsr-quickstart/Cargo.lock +++ b/examples/bsr-quickstart/Cargo.lock @@ -9,37 +9,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "borsh" -version = "1.6.1" +name = "bitflags" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "bytes", - "cfg_aliases", -] +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "buffa" -version = "0.7.1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92f2f5df67a9d5ccfc65237bfc954c56328aee40a04a9b92381ecba370be246" dependencies = [ "base64", "bytes", - "compact_str", - "ecow", + "foldhash", "hashbrown", "once_cell", + "rustversion", "serde", "serde_json", - "smol_str", + "smoothutf8", "thiserror", ] [[package]] name = "buffa-types" -version = "0.7.1" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15b05c8418f1e200e406e5b19df97400dffa833e2450e4392c506896cbc97ac" dependencies = [ "buffa", "bytes", + "jiff", "serde", "serde_json", "thiserror", @@ -52,48 +53,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] -name = "castaway" -version = "0.2.4" +name = "defmt" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +checksum = "e2953bfe4f93bbd20cc71198842756f77d161884c99ebbabc41d80231ded88d1" dependencies = [ - "rustversion", + "bitflags", + "defmt-macros", ] [[package]] -name = "cfg-if" -version = "1.0.4" +name = "defmt-macros" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "compact_str" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +checksum = "bad9c72e7ca2137e0dc3813245a0d282fd6daad32fd800af018306a9169b5fe8" dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "serde", - "static_assertions", + "defmt-parser", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "ecow" -version = "0.2.6" +name = "defmt-parser" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" +checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "serde", + "thiserror", ] [[package]] @@ -128,6 +115,48 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" +dependencies = [ + "defmt", + "jiff-core", + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + +[[package]] +name = "jiff-static" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + [[package]] name = "memchr" version = "2.8.0" @@ -140,6 +169,21 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -164,12 +208,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "serde" version = "1.0.228" @@ -214,20 +252,19 @@ dependencies = [ ] [[package]] -name = "smol_str" -version = "0.3.2" +name = "simdutf8" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9676b89cd56310a87b93dec47b11af744f34d5fc9f367b829474eec0a891350d" -dependencies = [ - "borsh", - "serde", -] +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" [[package]] -name = "static_assertions" -version = "1.1.0" +name = "smoothutf8" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "6b4ec95892483d6d94284caccfddb9b77111a4dcac33ed25d624248def0fa5f1" +dependencies = [ + "simdutf8", +] [[package]] name = "syn" diff --git a/examples/bsr-quickstart/Cargo.toml b/examples/bsr-quickstart/Cargo.toml index 400b4a50..78be34d6 100644 --- a/examples/bsr-quickstart/Cargo.toml +++ b/examples/bsr-quickstart/Cargo.toml @@ -10,18 +10,19 @@ plugin with `file_per_package=true` and a hand-written `mod.rs`. See """ [dependencies] -# Path dependencies so the example builds against the in-tree workspace -# (consistent with the other `examples/*`). The checked-in `src/gen/` is -# generated by the *published* BSR plugin pinned in `buf.gen.yaml`, so this -# example also acts as a forward-compat canary: if an in-tree runtime change -# ever breaks code generated by the published plugin, this example fails to -# build. In a real downstream project, use crates.io versions instead: -# buffa = { version = "0.9", features = ["json"] } -# buffa-types = { version = "0.9", features = ["json"] } -# serde = { version = "1", features = ["derive"] } -# serde_json = "1" -buffa = { path = "../../buffa", features = ["json"] } -buffa-types = { path = "../../buffa-types", features = ["json"] } +# Pinned to the crates.io releases matching the BSR plugin in `buf.gen.yaml`, +# so the checked-in `src/gen/` and the runtime it compiles against always come +# from one buffa version. That is also what a downstream BSR user has. +# +# The path dependencies this replaces made the example a forward-compat canary +# against the in-tree workspace. The canary could not hold, because the +# published plugin lags `main` by design: each in-tree change to generated +# output broke the example until someone hand-patched `src/gen/`, which is how +# it drifted across four releases without CI noticing (#427). +# +# Bump these together with the plugin pin in `buf.gen.yaml`. +buffa = { version = "=0.9.2", features = ["json"] } +buffa-types = { version = "=0.9.2", features = ["json"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/examples/bsr-quickstart/buf.gen.yaml b/examples/bsr-quickstart/buf.gen.yaml index 98d4038e..d9c230c4 100644 --- a/examples/bsr-quickstart/buf.gen.yaml +++ b/examples/bsr-quickstart/buf.gen.yaml @@ -10,12 +10,13 @@ # drop `file_per_package=true`). # # The plugin reference is pinned to a specific version so the generated -# output is reproducible and a plugin update can't silently change it. Match -# the pin to the `buffa` runtime crate version in `Cargo.toml`. Bump it (and -# `task gen-bsr-quickstart-example`) when a new BSR plugin release ships. +# output is reproducible and a plugin update can't silently change it. The pin +# and the `buffa` / `buffa-types` versions in `Cargo.toml` must name the same +# release, so bump both together when a new BSR plugin ships and then re-run +# `task gen-bsr-quickstart-example`. version: v2 plugins: - - remote: buf.build/anthropics/buffa:v0.5.2 + - remote: buf.build/anthropics/buffa:v0.9.2 out: src/gen opt: - file_per_package=true diff --git a/examples/bsr-quickstart/src/gen/example.v1.rs b/examples/bsr-quickstart/src/gen/example.v1.rs index 8e3e6f2a..47f4b17e 100644 --- a/examples/bsr-quickstart/src/gen/example.v1.rs +++ b/examples/bsr-quickstart/src/gen/example.v1.rs @@ -161,7 +161,10 @@ pub struct Greeting { rename = "at", skip_serializing_if = "::buffa::json_helpers::skip_if::is_unset_message_field" )] - pub at: ::buffa::MessageField<::buffa_types::google::protobuf::Timestamp>, + pub at: ::buffa::MessageField< + ::buffa_types::google::protobuf::Timestamp, + ::buffa::Inline<::buffa_types::google::protobuf::Timestamp>, + >, /// The mood of the greeting. /// /// Field 3: `mood` @@ -204,12 +207,7 @@ impl Greeting { /// Format: `type.googleapis.com/` pub const TYPE_URL: &'static str = "type.googleapis.com/example.v1.Greeting"; } -impl ::buffa::DefaultInstance for Greeting { - fn default_instance() -> &'static Self { - static VALUE: ::buffa::__private::OnceBox = ::buffa::__private::OnceBox::new(); - VALUE.get_or_init(|| ::buffa::alloc::boxed::Box::new(Self::default())) - } -} +::buffa::impl_default_instance!(Greeting); impl ::buffa::MessageName for Greeting { const PACKAGE: &'static str = "example.v1"; const NAME: &'static str = "Greeting"; @@ -219,46 +217,48 @@ impl ::buffa::MessageName for Greeting { impl ::buffa::Message for Greeting { /// Returns the total encoded size in bytes. /// - /// The result is a `u32`; the protobuf specification requires all - /// messages to fit within 2 GiB (2,147,483,647 bytes), so a - /// compliant message will never overflow this type. + /// Accumulates in `u64` (which cannot overflow for in-memory + /// data) and saturates to `u32` at return, so a message whose + /// encoded size exceeds the 2 GiB protobuf limit yields a value + /// above [`::buffa::MAX_MESSAGE_BYTES`] that the encode entry + /// points reject, never a silently wrapped size. #[allow(clippy::let_and_return)] fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { #[allow(unused_imports)] use ::buffa::Enumeration as _; - let mut size = 0u32; + let mut size = 0u64; if !self.text.is_empty() { - size += 1u32 + ::buffa::types::string_encoded_len(&self.text) as u32; + size += 1u64 + ::buffa::types::string_encoded_len(&self.text) as u64; } if self.at.is_set() { let __slot = __cache.reserve(); let inner_size = self.at.compute_size(__cache); __cache.set(__slot, inner_size); size - += 1u32 + ::buffa::encoding::varint_len(inner_size as u64) as u32 - + inner_size; + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; } { let val = self.mood.to_i32(); if val != 0 { - size += 1u32 + ::buffa::types::int32_encoded_len(val) as u32; + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; } } if let ::core::option::Option::Some(ref v) = self.recipient { match v { __buffa::oneof::greeting::Recipient::Name(x) => { - size += 1u32 + ::buffa::types::string_encoded_len(x) as u32; + size += 1u64 + ::buffa::types::string_encoded_len(x) as u64; } __buffa::oneof::greeting::Recipient::Everyone(_x) => { - size += 1u32 + ::buffa::types::BOOL_ENCODED_LEN as u32; + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; } } } for v in &self.tags { - size += 2u32 + ::buffa::types::string_encoded_len(v) as u32; + size += 2u64 + ::buffa::types::string_encoded_len(v) as u64; } - size += self.__buffa_unknown_fields.encoded_len() as u32; - size + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) } fn write_to( &self, @@ -268,57 +268,34 @@ impl ::buffa::Message for Greeting { #[allow(unused_imports)] use ::buffa::Enumeration as _; if !self.text.is_empty() { - ::buffa::encoding::Tag::new( - 1u32, - ::buffa::encoding::WireType::LengthDelimited, - ) - .encode(buf); - ::buffa::types::encode_string(&self.text, buf); + ::buffa::types::put_string_field(1u32, &self.text, buf); } if self.at.is_set() { - ::buffa::encoding::Tag::new( - 2u32, - ::buffa::encoding::WireType::LengthDelimited, - ) - .encode(buf); - ::buffa::encoding::encode_varint(__cache.consume_next() as u64, buf); + ::buffa::types::put_len_delimited_header( + 2u32, + u64::from(__cache.consume_next()), + buf, + ); self.at.write_to(__cache, buf); } { let val = self.mood.to_i32(); if val != 0 { - ::buffa::encoding::Tag::new(3u32, ::buffa::encoding::WireType::Varint) - .encode(buf); - ::buffa::types::encode_int32(val, buf); + ::buffa::types::put_int32_field(3u32, val, buf); } } if let ::core::option::Option::Some(ref v) = self.recipient { match v { __buffa::oneof::greeting::Recipient::Name(x) => { - ::buffa::encoding::Tag::new( - 10u32, - ::buffa::encoding::WireType::LengthDelimited, - ) - .encode(buf); - ::buffa::types::encode_string(x, buf); + ::buffa::types::put_string_field(10u32, x, buf); } __buffa::oneof::greeting::Recipient::Everyone(x) => { - ::buffa::encoding::Tag::new( - 11u32, - ::buffa::encoding::WireType::Varint, - ) - .encode(buf); - ::buffa::types::encode_bool(*x, buf); + ::buffa::types::put_bool_field(11u32, *x, buf); } } } for v in &self.tags { - ::buffa::encoding::Tag::new( - 20u32, - ::buffa::encoding::WireType::LengthDelimited, - ) - .encode(buf); - ::buffa::types::encode_string(v, buf); + ::buffa::types::put_string_field(20u32, v, buf); } self.__buffa_unknown_fields.write_to(buf); } @@ -334,23 +311,17 @@ impl ::buffa::Message for Greeting { use ::buffa::Enumeration as _; match tag.field_number() { 1u32 => { - if tag.wire_type() != ::buffa::encoding::WireType::LengthDelimited { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 1u32, - expected: 2u8, - actual: tag.wire_type() as u8, - }); - } + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; ::buffa::types::merge_string(&mut self.text, buf)?; } 2u32 => { - if tag.wire_type() != ::buffa::encoding::WireType::LengthDelimited { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 2u32, - expected: 2u8, - actual: tag.wire_type() as u8, - }); - } + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; ::buffa::Message::merge_length_delimited( self.at.get_or_insert_default(), buf, @@ -358,23 +329,17 @@ impl ::buffa::Message for Greeting { )?; } 3u32 => { - if tag.wire_type() != ::buffa::encoding::WireType::Varint { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 3u32, - expected: 0u8, - actual: tag.wire_type() as u8, - }); - } + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; self.mood = ::buffa::EnumValue::from(::buffa::types::decode_int32(buf)?); } 10u32 => { - if tag.wire_type() != ::buffa::encoding::WireType::LengthDelimited { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 10u32, - expected: 2u8, - actual: tag.wire_type() as u8, - }); - } + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; self.recipient = ::core::option::Option::Some( __buffa::oneof::greeting::Recipient::Name( ::buffa::types::decode_string(buf)?, @@ -382,13 +347,10 @@ impl ::buffa::Message for Greeting { ); } 11u32 => { - if tag.wire_type() != ::buffa::encoding::WireType::Varint { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 11u32, - expected: 0u8, - actual: tag.wire_type() as u8, - }); - } + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; self.recipient = ::core::option::Option::Some( __buffa::oneof::greeting::Recipient::Everyone( ::buffa::types::decode_bool(buf)?, @@ -396,14 +358,15 @@ impl ::buffa::Message for Greeting { ); } 20u32 => { - if tag.wire_type() != ::buffa::encoding::WireType::LengthDelimited { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 20u32, - expected: 2u8, - actual: tag.wire_type() as u8, - }); - } - self.tags.push(::buffa::types::decode_string(buf)?); + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __elem = ::buffa::types::decode_string(buf)?; + ctx.register_element_memory( + ::buffa::__private::element_footprint(&__elem), + )?; + self.tags.push(__elem); } _ => { self.__buffa_unknown_fields @@ -430,18 +393,21 @@ impl ::buffa::ExtensionSet for Greeting { &mut self.__buffa_unknown_fields } } -impl<'de> serde::Deserialize<'de> for Greeting { - fn deserialize>( +impl<'de> ::serde::Deserialize<'de> for Greeting { + fn deserialize>( d: D, ) -> ::core::result::Result { struct _V; - impl<'de> serde::de::Visitor<'de> for _V { + impl<'de> ::serde::de::Visitor<'de> for _V { type Value = Greeting; - fn expecting(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + fn expecting( + &self, + f: &mut ::core::fmt::Formatter<'_>, + ) -> ::core::fmt::Result { f.write_str("struct Greeting") } #[allow(clippy::field_reassign_with_default)] - fn visit_map>( + fn visit_map>( self, mut map: A, ) -> ::core::result::Result { @@ -449,7 +415,10 @@ impl<'de> serde::Deserialize<'de> for Greeting { ::buffa::alloc::string::String, > = None; let mut __f_at: ::core::option::Option< - ::buffa::MessageField<::buffa_types::google::protobuf::Timestamp>, + ::buffa::MessageField< + ::buffa_types::google::protobuf::Timestamp, + ::buffa::Inline<::buffa_types::google::protobuf::Timestamp>, + >, > = None; let mut __f_mood: ::core::option::Option<::buffa::EnumValue> = None; let mut __f_tags: ::core::option::Option< @@ -463,9 +432,9 @@ impl<'de> serde::Deserialize<'de> for Greeting { "text" => { __f_text = Some({ struct _S; - impl<'de> serde::de::DeserializeSeed<'de> for _S { + impl<'de> ::serde::de::DeserializeSeed<'de> for _S { type Value = ::buffa::alloc::string::String; - fn deserialize>( + fn deserialize>( self, d: D, ) -> ::core::result::Result< @@ -484,6 +453,7 @@ impl<'de> serde::Deserialize<'de> for Greeting { .next_value::< ::buffa::MessageField< ::buffa_types::google::protobuf::Timestamp, + ::buffa::Inline<::buffa_types::google::protobuf::Timestamp>, >, >()?, ); @@ -491,9 +461,9 @@ impl<'de> serde::Deserialize<'de> for Greeting { "mood" => { __f_mood = Some({ struct _S; - impl<'de> serde::de::DeserializeSeed<'de> for _S { + impl<'de> ::serde::de::DeserializeSeed<'de> for _S { type Value = ::buffa::EnumValue; - fn deserialize>( + fn deserialize>( self, d: D, ) -> ::core::result::Result< @@ -509,11 +479,11 @@ impl<'de> serde::Deserialize<'de> for Greeting { "tags" => { __f_tags = Some({ struct _S; - impl<'de> serde::de::DeserializeSeed<'de> for _S { + impl<'de> ::serde::de::DeserializeSeed<'de> for _S { type Value = ::buffa::alloc::vec::Vec< ::buffa::alloc::string::String, >; - fn deserialize>( + fn deserialize>( self, d: D, ) -> ::core::result::Result< @@ -540,7 +510,7 @@ impl<'de> serde::Deserialize<'de> for Greeting { if let Some(v) = v { if __oneof_recipient.is_some() { return Err( - serde::de::Error::custom( + ::serde::de::Error::custom( "multiple oneof fields set for 'recipient'", ), ); @@ -560,7 +530,7 @@ impl<'de> serde::Deserialize<'de> for Greeting { if let Some(v) = v { if __oneof_recipient.is_some() { return Err( - serde::de::Error::custom( + ::serde::de::Error::custom( "multiple oneof fields set for 'recipient'", ), ); @@ -571,7 +541,7 @@ impl<'de> serde::Deserialize<'de> for Greeting { } } _ => { - map.next_value::()?; + map.next_value::<::serde::de::IgnoredAny>()?; } } } @@ -666,151 +636,6 @@ pub mod __buffa { >, pub __buffa_unknown_fields: ::buffa::UnknownFieldsView<'a>, } - impl<'a> GreetingView<'a> { - /// Decode from `buf` under the limits carried by `ctx` (recursion - /// depth and the shared unknown-field allowance). - /// - /// Called by [`::buffa::MessageView::decode_view`] with a fresh - /// default context and by generated sub-message decode arms with - /// `ctx.descend()?`. - /// - /// **Not part of the public API.** Named with a leading underscore to - /// signal that it is for generated-code use only. - #[doc(hidden)] - pub fn _decode_ctx( - buf: &'a [u8], - ctx: ::buffa::DecodeContext<'_>, - ) -> ::core::result::Result { - let mut view = Self::default(); - view._merge_into_view(buf, ctx)?; - ::core::result::Result::Ok(view) - } - /// Merge fields from `buf` into this view (proto merge semantics). - /// - /// Repeated fields append; singular fields last-wins; singular - /// MESSAGE fields merge recursively. Used by sub-message decode - /// arms when the same field appears multiple times on the wire. - /// - /// **Not part of the public API.** - #[doc(hidden)] - pub fn _merge_into_view( - &mut self, - buf: &'a [u8], - ctx: ::buffa::DecodeContext<'_>, - ) -> ::core::result::Result<(), ::buffa::DecodeError> { - let _ = ctx; - #[allow(unused_variables)] - let view = self; - let mut cur: &'a [u8] = buf; - while !cur.is_empty() { - let before_tag = cur; - let tag = ::buffa::encoding::Tag::decode(&mut cur)?; - match tag.field_number() { - 1u32 => { - if tag.wire_type() - != ::buffa::encoding::WireType::LengthDelimited - { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 1u32, - expected: 2u8, - actual: tag.wire_type() as u8, - }); - } - view.text = ::buffa::types::borrow_str(&mut cur)?; - } - 2u32 => { - if tag.wire_type() - != ::buffa::encoding::WireType::LengthDelimited - { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 2u32, - expected: 2u8, - actual: tag.wire_type() as u8, - }); - } - let __sub_ctx = ctx.descend()?; - let sub = ::buffa::types::borrow_bytes(&mut cur)?; - match view.at.as_mut() { - Some(existing) => existing._merge_into_view(sub, __sub_ctx)?, - None => { - view.at = ::buffa::MessageFieldView::set( - ::buffa_types::google::protobuf::__buffa::view::TimestampView::_decode_ctx( - sub, - __sub_ctx, - )?, - ); - } - } - } - 3u32 => { - if tag.wire_type() != ::buffa::encoding::WireType::Varint { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 3u32, - expected: 0u8, - actual: tag.wire_type() as u8, - }); - } - view.mood = ::buffa::EnumValue::from( - ::buffa::types::decode_int32(&mut cur)?, - ); - } - 20u32 => { - if tag.wire_type() - != ::buffa::encoding::WireType::LengthDelimited - { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 20u32, - expected: 2u8, - actual: tag.wire_type() as u8, - }); - } - view.tags.push(::buffa::types::borrow_str(&mut cur)?); - } - 10u32 => { - if tag.wire_type() - != ::buffa::encoding::WireType::LengthDelimited - { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 10u32, - expected: 2u8, - actual: tag.wire_type() as u8, - }); - } - view.recipient = Some( - super::super::__buffa::view::oneof::greeting::Recipient::Name( - ::buffa::types::borrow_str(&mut cur)?, - ), - ); - } - 11u32 => { - if tag.wire_type() != ::buffa::encoding::WireType::Varint { - return ::core::result::Result::Err(::buffa::DecodeError::WireTypeMismatch { - field_number: 11u32, - expected: 0u8, - actual: tag.wire_type() as u8, - }); - } - view.recipient = Some( - super::super::__buffa::view::oneof::greeting::Recipient::Everyone( - ::buffa::types::decode_bool(&mut cur)?, - ), - ); - } - _ => { - ::buffa::encoding::skip_field_depth( - tag, - &mut cur, - ctx.depth(), - )?; - let span_len = before_tag.len() - cur.len(); - view.__buffa_unknown_fields - .push_record(before_tag, span_len, ctx)?; - } - } - } - ::core::result::Result::Ok(()) - } - } impl<'a> ::buffa::MessageView<'a> for GreetingView<'a> { type Owned = super::super::Greeting; fn decode_view( @@ -819,16 +644,116 @@ pub mod __buffa { let __limit = ::core::cell::Cell::new( ::buffa::DEFAULT_UNKNOWN_FIELD_LIMIT, ); - Self::_decode_ctx( + let __elem = ::core::cell::Cell::new( + ::buffa::DEFAULT_ELEMENT_MEMORY_LIMIT, + ); + ::decode_view_ctx( buf, - ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit), + ::buffa::DecodeContext::new(::buffa::RECURSION_LIMIT, &__limit) + .with_element_memory(&__elem), ) } fn decode_view_with_ctx( buf: &'a [u8], ctx: ::buffa::DecodeContext<'_>, ) -> ::core::result::Result { - Self::_decode_ctx(buf, ctx) + ::decode_view_ctx(buf, ctx) + } + #[inline] + fn merge_view_field( + &mut self, + tag: ::buffa::encoding::Tag, + cur: &'a [u8], + before_tag: &'a [u8], + ctx: ::buffa::DecodeContext<'_>, + ) -> ::core::result::Result<&'a [u8], ::buffa::DecodeError> { + let _ = ctx; + #[allow(unused_variables)] + let view = self; + let mut cur = cur; + match tag.field_number() { + 1u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.text = ::buffa::types::borrow_str(&mut cur)?; + } + 2u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __sub_ctx = ctx.descend()?; + let sub = ::buffa::types::borrow_bytes(&mut cur)?; + match view.at.as_mut() { + Some(existing) => { + ::buffa::MessageView::merge_into_view( + existing, + sub, + __sub_ctx, + )? + } + None => { + view.at = ::buffa::MessageFieldView::set( + <::buffa_types::google::protobuf::__buffa::view::TimestampView as ::buffa::MessageView>::decode_view_ctx( + sub, + __sub_ctx, + )?, + ); + } + } + } + 3u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.mood = ::buffa::EnumValue::from( + ::buffa::types::decode_int32(&mut cur)?, + ); + } + 20u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + let __elem = ::buffa::types::borrow_str(&mut cur)?; + ctx.register_element_memory( + ::buffa::__private::element_footprint(&__elem), + )?; + view.tags.push(__elem); + } + 10u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::LengthDelimited, + )?; + view.recipient = Some( + super::super::__buffa::view::oneof::greeting::Recipient::Name( + ::buffa::types::borrow_str(&mut cur)?, + ), + ); + } + 11u32 => { + ::buffa::encoding::check_wire_type( + tag, + ::buffa::encoding::WireType::Varint, + )?; + view.recipient = Some( + super::super::__buffa::view::oneof::greeting::Recipient::Everyone( + ::buffa::types::decode_bool(&mut cur)?, + ), + ); + } + _ => { + ::buffa::encoding::skip_field_depth(tag, &mut cur, ctx.depth())?; + let span_len = before_tag.len() - cur.len(); + view.__buffa_unknown_fields + .push_record(before_tag, span_len, ctx)?; + } + } + ::core::result::Result::Ok(cur) } fn to_owned_message( &self, @@ -849,6 +774,7 @@ pub mod __buffa { Some(v) => { ::buffa::MessageField::< ::buffa_types::google::protobuf::Timestamp, + ::buffa::Inline<::buffa_types::google::protobuf::Timestamp>, >::some(v.to_owned_from_source(__buffa_src)?) } None => ::buffa::MessageField::none(), @@ -887,22 +813,22 @@ pub mod __buffa { fn compute_size(&self, __cache: &mut ::buffa::SizeCache) -> u32 { #[allow(unused_imports)] use ::buffa::Enumeration as _; - let mut size = 0u32; + let mut size = 0u64; if !self.text.is_empty() { - size += 1u32 + ::buffa::types::string_encoded_len(&self.text) as u32; + size += 1u64 + ::buffa::types::string_encoded_len(&self.text) as u64; } if self.at.is_set() { let __slot = __cache.reserve(); let inner_size = self.at.compute_size(__cache); __cache.set(__slot, inner_size); size - += 1u32 + ::buffa::encoding::varint_len(inner_size as u64) as u32 - + inner_size; + += 1u64 + ::buffa::encoding::varint_len(inner_size as u64) as u64 + + inner_size as u64; } { let val = self.mood.to_i32(); if val != 0 { - size += 1u32 + ::buffa::types::int32_encoded_len(val) as u32; + size += 1u64 + ::buffa::types::int32_encoded_len(val) as u64; } } if let ::core::option::Option::Some(ref v) = self.recipient { @@ -910,20 +836,20 @@ pub mod __buffa { super::super::__buffa::view::oneof::greeting::Recipient::Name( x, ) => { - size += 1u32 + ::buffa::types::string_encoded_len(x) as u32; + size += 1u64 + ::buffa::types::string_encoded_len(x) as u64; } super::super::__buffa::view::oneof::greeting::Recipient::Everyone( _x, ) => { - size += 1u32 + ::buffa::types::BOOL_ENCODED_LEN as u32; + size += 1u64 + ::buffa::types::BOOL_ENCODED_LEN as u64; } } } for v in &self.tags { - size += 2u32 + ::buffa::types::string_encoded_len(v) as u32; + size += 2u64 + ::buffa::types::string_encoded_len(v) as u64; } - size += self.__buffa_unknown_fields.encoded_len() as u32; - size + size += self.__buffa_unknown_fields.encoded_len() as u64; + ::buffa::saturate_size(size) } #[allow(clippy::needless_borrow)] fn write_to( @@ -934,31 +860,20 @@ pub mod __buffa { #[allow(unused_imports)] use ::buffa::Enumeration as _; if !self.text.is_empty() { - ::buffa::encoding::Tag::new( - 1u32, - ::buffa::encoding::WireType::LengthDelimited, - ) - .encode(buf); - ::buffa::types::encode_string(&self.text, buf); + ::buffa::types::put_string_field(1u32, &self.text, buf); } if self.at.is_set() { - ::buffa::encoding::Tag::new( - 2u32, - ::buffa::encoding::WireType::LengthDelimited, - ) - .encode(buf); - ::buffa::encoding::encode_varint(__cache.consume_next() as u64, buf); + ::buffa::types::put_len_delimited_header( + 2u32, + u64::from(__cache.consume_next()), + buf, + ); self.at.write_to(__cache, buf); } { let val = self.mood.to_i32(); if val != 0 { - ::buffa::encoding::Tag::new( - 3u32, - ::buffa::encoding::WireType::Varint, - ) - .encode(buf); - ::buffa::types::encode_int32(val, buf); + ::buffa::types::put_int32_field(3u32, val, buf); } } if let ::core::option::Option::Some(ref v) = self.recipient { @@ -966,32 +881,17 @@ pub mod __buffa { super::super::__buffa::view::oneof::greeting::Recipient::Name( x, ) => { - ::buffa::encoding::Tag::new( - 10u32, - ::buffa::encoding::WireType::LengthDelimited, - ) - .encode(buf); - ::buffa::types::encode_string(x, buf); + ::buffa::types::put_string_field(10u32, x, buf); } super::super::__buffa::view::oneof::greeting::Recipient::Everyone( x, ) => { - ::buffa::encoding::Tag::new( - 11u32, - ::buffa::encoding::WireType::Varint, - ) - .encode(buf); - ::buffa::types::encode_bool(*x, buf); + ::buffa::types::put_bool_field(11u32, *x, buf); } } } for v in &self.tags { - ::buffa::encoding::Tag::new( - 20u32, - ::buffa::encoding::WireType::LengthDelimited, - ) - .encode(buf); - ::buffa::types::encode_string(v, buf); + ::buffa::types::put_string_field(20u32, v, buf); } self.__buffa_unknown_fields.write_to(buf); } @@ -1051,25 +951,8 @@ pub mod __buffa { const FULL_NAME: &'static str = "example.v1.Greeting"; const TYPE_URL: &'static str = "type.googleapis.com/example.v1.Greeting"; } - impl<'v> ::buffa::DefaultViewInstance for GreetingView<'v> { - fn default_view_instance<'a>() -> &'a Self - where - Self: 'a, - { - static VALUE: ::buffa::__private::OnceBox> = ::buffa::__private::OnceBox::new(); - VALUE - .get_or_init(|| ::buffa::alloc::boxed::Box::new( - >::default(), - )) - } - } - impl ::buffa::ViewReborrow for GreetingView<'static> { - type Reborrowed<'b> = GreetingView<'b>; - fn reborrow<'b>(this: &'b Self) -> &'b Self::Reborrowed<'b> { - this - } - } - ::buffa::unsafe_impl_view_lifetime_parametric!(GreetingView); + ::buffa::impl_default_view_instance!(GreetingView); + ::buffa::impl_view_reborrow!(GreetingView); /** Self-contained, `'static` owned view of a `Greeting` message. Wraps [`::buffa::OwnedView`]`<`[`GreetingView`]`<'static>>`: the decoded view and the [`::buffa::bytes::Bytes`] buffer it borrows from travel together, so the handle is `'static` and `Send + Sync` — suitable for async handlers, spawned tasks, and anywhere a `'static` bound is required. @@ -1115,7 +998,9 @@ pub mod __buffa { /// /// # Errors /// - /// Returns [`::buffa::DecodeError`] if the re-encoded bytes are + /// Returns [`::buffa::DecodeError::MessageTooLarge`] if the + /// message's encoded size exceeds the 2 GiB protobuf limit, or + /// another [`::buffa::DecodeError`] if the re-encoded bytes are /// somehow invalid (should not happen for well-formed messages). pub fn from_owned( msg: &super::super::Greeting, @@ -1131,13 +1016,13 @@ pub mod __buffa { } /// Convert to the owned message type. /// - /// # Errors - /// - /// Returns an error if re-materializing preserved unknown fields - /// fails (e.g. the unknown-field limit is exceeded). - pub fn to_owned_message( - &self, - ) -> ::core::result::Result { + /// Infallible: this type's constructors wire-decode their + /// buffer, and a view produced by wire decoding always + /// converts. Delegates to [`::buffa::OwnedView::to_owned_message`], + /// whose contract also governs handles converted from a raw + /// [`::buffa::OwnedView`]. + #[must_use] + pub fn to_owned_message(&self) -> super::super::Greeting { self.0.to_owned_message() } /// The underlying bytes buffer. @@ -1249,12 +1134,12 @@ pub mod __buffa { Everyone(bool), } impl ::buffa::Oneof for Recipient {} - impl serde::Serialize for Recipient { - fn serialize( + impl ::serde::Serialize for Recipient { + fn serialize( &self, s: S, ) -> ::core::result::Result { - use serde::ser::SerializeMap; + use ::serde::ser::SerializeMap; let mut map = s.serialize_map(Some(1))?; match self { Self::Name(v) => { From f00018d99a61305ab0bcf1141504a294e190840f Mon Sep 17 00:00:00 2001 From: Iain McGinniss <309153+iainmcgin@users.noreply.github.com> Date: Fri, 11 Sep 2026 15:26:55 -0700 Subject: [PATCH 2/2] examples: document the bsr-quickstart bump procedure and lock its CI check Run the CI check with `--locked` so a new upstream release of one of the example's floating transitive deps cannot turn `lint-and-test` red, and so a stale `Cargo.lock` after a pin bump fails in CI rather than for the next local `--locked` build. State the "bump the plugin pin and the crate versions together" rule where someone doing the bump will read it: the `gen-bsr-quickstart-example` task description, and a note in CONTRIBUTING.md's checked-in generated code section, since the bump follows publication of the BSR plugin rather than any codegen change. Correct the Taskfile comment that said every example uses path dependencies, and trim the Cargo.toml, buf.gen.yaml and ci.yml comments to the steady-state reasoning. --- .changes/unreleased/fixed-20260909-125500.yaml | 2 +- .github/workflows/ci.yml | 14 +++++++------- CONTRIBUTING.md | 2 ++ Taskfile.yml | 12 +++++++----- examples/bsr-quickstart/Cargo.toml | 15 ++++----------- examples/bsr-quickstart/buf.gen.yaml | 7 +++---- 6 files changed, 24 insertions(+), 28 deletions(-) diff --git a/.changes/unreleased/fixed-20260909-125500.yaml b/.changes/unreleased/fixed-20260909-125500.yaml index a5199d35..8c32b920 100644 --- a/.changes/unreleased/fixed-20260909-125500.yaml +++ b/.changes/unreleased/fixed-20260909-125500.yaml @@ -1,4 +1,4 @@ kind: Fixed body: |- - **`examples/bsr-quickstart` compiles again, and CI checks it** (#427). Its checked-in generated code had been hand-patched across four releases while the plugin pin stayed at v0.5.2, so it matched neither the pinned plugin nor the in-tree runtime. The example now pins the BSR plugin and the `buffa` / `buffa-types` crates to the same release, and `cargo check` runs against it in CI. + **`examples/bsr-quickstart` builds again** (#427). Its checked-in generated code had been hand-patched since it was generated from the v0.5.2 plugin, so it matched neither that plugin nor the current runtime. It is now regenerated from the `buf.build/anthropics/buffa:v0.9.2` plugin and depends on `buffa` / `buffa-types` 0.9.2 from crates.io, and CI builds it. time: 2026-09-09T12:55:00.000000000-07:00 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edf0b392..1e0648eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,14 +93,14 @@ jobs: - name: Shared descriptor pool compile run: cargo test -p buffa-codegen --test shared_pool_compile -- --ignored - # `examples/bsr-quickstart` is outside the workspace, so nothing else - # compiles it. Its `src/gen/` is checked-in output from the BSR plugin - # pinned in that directory's `buf.gen.yaml`, and it drifted across four - # releases before anyone noticed (#427). The example pins the plugin and - # the buffa crates to the same release, so this only goes red if one of - # the two pins moves without the other. + # `examples/bsr-quickstart` is outside the workspace, and its `src/gen/` + # is checked-in output from the published BSR plugin, not in-tree + # codegen. Its runtime deps are pinned to the plugin's release (see its + # `Cargo.toml`), so this fails if the pins diverge or `src/gen/` is + # hand-edited. `--locked` keeps new upstream releases of its transitive + # deps from changing the result. (#427) - name: Check bsr-quickstart example - run: cargo check --manifest-path examples/bsr-quickstart/Cargo.toml + run: cargo check --locked --manifest-path examples/bsr-quickstart/Cargo.toml # ── Markdown lint ──────────────────────────────────────────────────────── # Same pinned markdownlint-cli version as `task lint-md`. Node is diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ffec0459..612856be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,6 +134,8 @@ Three sets of generated code are checked into the repo and **must be regenerated 3. **Logging example** (`examples/logging/src/gen/`): Regenerate with `task gen-logging-example` (requires `buf` on PATH). +The `bsr-quickstart` example's `examples/bsr-quickstart/src/gen/` is the exception: it holds output from the *published* BSR plugin, so codegen changes do not regenerate it. Once the `buf.build/anthropics/buffa` plugin for a new release is published, bump the plugin pin in its `buf.gen.yaml` and the `buffa` / `buffa-types` versions in its `Cargo.toml` together, then run `task gen-bsr-quickstart-example`. + CI (`check-generated-code` job) will fail if checked-in generated code is stale. ## Cross-Target Checks diff --git a/Taskfile.yml b/Taskfile.yml index 5d7901cb..2cc81f63 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -667,17 +667,19 @@ tasks: consumer-facing remote-plugin workflow, so it deliberately uses the published plugin rather than the in-tree `protoc-gen-buffa`. The plugin version is pinned in `buf.gen.yaml` for reproducible - output — bump the pin and re-run this task when a new BSR plugin - release ships, not after every codegen change. `src/gen/mod.rs` is - hand-written and not touched by buf. + output, and the example's `Cargo.toml` pins `buffa` / `buffa-types` + to the same release. When a new BSR plugin release ships, bump all + three together and re-run this task — not after every codegen + change. `src/gen/mod.rs` is hand-written and not touched by buf. dir: examples/bsr-quickstart cmds: - rm -f src/gen/example.v1.rs - buf generate # The examples are independent cargo projects (own Cargo.toml, own target/), - # not workspace members — they declare path deps on the workspace crates to - # mirror a downstream consumer's setup. + # not workspace members. Most declare path deps on the workspace crates to + # mirror a downstream consumer's setup; bsr-quickstart instead depends on the + # crates.io release matching its BSR plugin pin. build-examples: desc: Build all example binaries. diff --git a/examples/bsr-quickstart/Cargo.toml b/examples/bsr-quickstart/Cargo.toml index 78be34d6..e0cba0fb 100644 --- a/examples/bsr-quickstart/Cargo.toml +++ b/examples/bsr-quickstart/Cargo.toml @@ -10,17 +10,10 @@ plugin with `file_per_package=true` and a hand-written `mod.rs`. See """ [dependencies] -# Pinned to the crates.io releases matching the BSR plugin in `buf.gen.yaml`, -# so the checked-in `src/gen/` and the runtime it compiles against always come -# from one buffa version. That is also what a downstream BSR user has. -# -# The path dependencies this replaces made the example a forward-compat canary -# against the in-tree workspace. The canary could not hold, because the -# published plugin lags `main` by design: each in-tree change to generated -# output broke the example until someone hand-patched `src/gen/`, which is how -# it drifted across four releases without CI noticing (#427). -# -# Bump these together with the plugin pin in `buf.gen.yaml`. +# crates.io releases, not path dependencies: `src/gen/` is output from the +# published plugin pinned in `buf.gen.yaml`, which trails `main`, so it only +# compiles against the runtime from that same release. Keep these versions +# equal to that pin. In your own project, `version = "0.9"` is enough. buffa = { version = "=0.9.2", features = ["json"] } buffa-types = { version = "=0.9.2", features = ["json"] } serde = { version = "1", features = ["derive"] } diff --git a/examples/bsr-quickstart/buf.gen.yaml b/examples/bsr-quickstart/buf.gen.yaml index d9c230c4..63a711f5 100644 --- a/examples/bsr-quickstart/buf.gen.yaml +++ b/examples/bsr-quickstart/buf.gen.yaml @@ -9,10 +9,9 @@ # `protoc-gen-buffa-packaging` locally and add it as a second plugin (and # drop `file_per_package=true`). # -# The plugin reference is pinned to a specific version so the generated -# output is reproducible and a plugin update can't silently change it. The pin -# and the `buffa` / `buffa-types` versions in `Cargo.toml` must name the same -# release, so bump both together when a new BSR plugin ships and then re-run +# The plugin reference is pinned so the generated output is reproducible and a +# plugin update can't silently change it. `Cargo.toml` pins `buffa` / +# `buffa-types` to the same release: bump all three together, then run # `task gen-bsr-quickstart-example`. version: v2 plugins: