1use std::{
4 ffi::OsString,
5 path::{Path, PathBuf},
6 process::Command,
7};
8
9use buffa::Message as _;
10use buffa_descriptor::generated::descriptor::{DescriptorProto, FileDescriptorSet};
11
12const DEFAULT_FDS_NAME: &str = "file_descriptor_set.bin";
14
15#[derive(Debug, Clone, Default)]
18enum DescriptorSource {
19 #[default]
22 Protoc,
23 Buf,
25 Precompiled(PathBuf),
27}
28
29#[derive(Debug, Default)]
57pub struct Builder {
58 descriptor_source: DescriptorSource,
59 files: Vec<PathBuf>,
60 includes: Vec<PathBuf>,
61 out_dir: Option<PathBuf>,
62 file_descriptor_set_path: Option<PathBuf>,
63 descriptor_pool_expr: Option<String>,
64 file_descriptor_set_bytes_expr: Option<String>,
65 include_file: Option<String>,
66
67 type_attributes: Vec<(String, String)>,
69 field_attributes: Vec<(String, String)>,
70 message_attributes: Vec<(String, String)>,
71 enum_attributes: Vec<(String, String)>,
72 extern_paths: Vec<(String, String)>,
73 bytes_paths: Vec<String>,
74 generate_views: Option<bool>,
75 generate_json: Option<bool>,
76 generate_text: Option<bool>,
77 generate_arbitrary: Option<bool>,
78 preserve_unknown_fields: Option<bool>,
79 strict_utf8_mapping: Option<bool>,
80 allow_message_set: Option<bool>,
81 generate_view_reflection: Option<bool>,
82 view_reflection_include_file: Option<String>,
83 view_reflection_root_path: Option<String>,
84}
85
86#[derive(Debug, thiserror::Error)]
88#[non_exhaustive]
89pub enum Error {
90 #[error(
93 "buffa-reflect-build: descriptor binding not configured — call `.descriptor_pool(..)` or \
94 `.file_descriptor_set_bytes(..)`"
95 )]
96 MissingDescriptorBinding,
97
98 #[error(
100 "buffa-reflect-build: cannot set both `descriptor_pool` and `file_descriptor_set_bytes` — \
101 pick one"
102 )]
103 ConflictingDescriptorBindings,
104
105 #[error(
108 "buffa-reflect-build: OUT_DIR is not set and no out_dir() was configured (run from \
109 build.rs or call .out_dir())"
110 )]
111 MissingOutDir,
112
113 #[error("buffa-reflect-build: failed to invoke {tool}: {source}")]
115 DescriptorTool {
116 tool: &'static str,
118 #[source]
120 source: std::io::Error,
121 },
122
123 #[error("buffa-reflect-build: {tool} exited with status {status}: {stderr}")]
125 DescriptorToolFailed {
126 tool: &'static str,
128 status: std::process::ExitStatus,
130 stderr: String,
132 },
133
134 #[error("buffa-reflect-build: failed to decode FileDescriptorSet: {0}")]
136 DecodeFileDescriptorSet(#[source] buffa::DecodeError),
137
138 #[error(
143 "buffa-reflect-build: .includes(..) is not supported in `use_buf` mode (buf reads import \
144 paths from buf.yaml); drop the includes() call or switch to the protoc source"
145 )]
146 BufWithIncludes,
147
148 #[error("buffa-reflect-build: buffa-build error: {0}")]
156 BuffaBuild(String),
157
158 #[error("buffa-reflect-build: io error: {0}")]
160 Io(#[from] std::io::Error),
161}
162
163impl Builder {
164 #[must_use]
166 pub fn new() -> Self {
167 Self::default()
168 }
169
170 #[must_use]
174 pub fn descriptor_pool(mut self, expr: impl Into<String>) -> Self {
175 self.descriptor_pool_expr = Some(expr.into());
176 self
177 }
178
179 #[must_use]
184 pub fn file_descriptor_set_bytes(mut self, expr: impl Into<String>) -> Self {
185 self.file_descriptor_set_bytes_expr = Some(expr.into());
186 self
187 }
188
189 #[must_use]
192 pub fn file_descriptor_set_path(mut self, path: impl Into<PathBuf>) -> Self {
193 self.file_descriptor_set_path = Some(path.into());
194 self
195 }
196
197 #[must_use]
200 pub fn files(mut self, files: &[impl AsRef<Path>]) -> Self {
201 self.files
202 .extend(files.iter().map(|f| f.as_ref().to_path_buf()));
203 self
204 }
205
206 #[must_use]
208 pub fn includes(mut self, includes: &[impl AsRef<Path>]) -> Self {
209 self.includes
210 .extend(includes.iter().map(|i| i.as_ref().to_path_buf()));
211 self
212 }
213
214 #[must_use]
216 pub fn use_buf(mut self) -> Self {
217 self.descriptor_source = DescriptorSource::Buf;
218 self
219 }
220
221 #[must_use]
225 pub fn descriptor_set(mut self, path: impl Into<PathBuf>) -> Self {
226 self.descriptor_source = DescriptorSource::Precompiled(path.into());
227 self
228 }
229
230 #[must_use]
232 pub fn out_dir(mut self, dir: impl Into<PathBuf>) -> Self {
233 self.out_dir = Some(dir.into());
234 self
235 }
236
237 #[must_use]
239 pub fn type_attribute(mut self, path: impl Into<String>, attr: impl Into<String>) -> Self {
240 self.type_attributes.push((path.into(), attr.into()));
241 self
242 }
243
244 #[must_use]
246 pub fn field_attribute(mut self, path: impl Into<String>, attr: impl Into<String>) -> Self {
247 self.field_attributes.push((path.into(), attr.into()));
248 self
249 }
250
251 #[must_use]
253 pub fn message_attribute(mut self, path: impl Into<String>, attr: impl Into<String>) -> Self {
254 self.message_attributes.push((path.into(), attr.into()));
255 self
256 }
257
258 #[must_use]
260 pub fn enum_attribute(mut self, path: impl Into<String>, attr: impl Into<String>) -> Self {
261 self.enum_attributes.push((path.into(), attr.into()));
262 self
263 }
264
265 #[must_use]
268 pub fn extern_path(
269 mut self,
270 proto_path: impl Into<String>,
271 rust_path: impl Into<String>,
272 ) -> Self {
273 self.extern_paths
274 .push((proto_path.into(), rust_path.into()));
275 self
276 }
277
278 #[must_use]
281 pub fn use_bytes_type_in(mut self, paths: &[impl AsRef<str>]) -> Self {
282 self.bytes_paths
283 .extend(paths.iter().map(|p| p.as_ref().to_string()));
284 self
285 }
286
287 #[must_use]
290 pub fn generate_views(mut self, enabled: bool) -> Self {
291 self.generate_views = Some(enabled);
292 self
293 }
294
295 #[must_use]
297 pub fn generate_json(mut self, enabled: bool) -> Self {
298 self.generate_json = Some(enabled);
299 self
300 }
301
302 #[must_use]
304 pub fn generate_text(mut self, enabled: bool) -> Self {
305 self.generate_text = Some(enabled);
306 self
307 }
308
309 #[must_use]
311 pub fn generate_arbitrary(mut self, enabled: bool) -> Self {
312 self.generate_arbitrary = Some(enabled);
313 self
314 }
315
316 #[must_use]
318 pub fn preserve_unknown_fields(mut self, enabled: bool) -> Self {
319 self.preserve_unknown_fields = Some(enabled);
320 self
321 }
322
323 #[must_use]
326 pub fn strict_utf8_mapping(mut self, enabled: bool) -> Self {
327 self.strict_utf8_mapping = Some(enabled);
328 self
329 }
330
331 #[must_use]
333 pub fn allow_message_set(mut self, enabled: bool) -> Self {
334 self.allow_message_set = Some(enabled);
335 self
336 }
337
338 #[must_use]
340 pub fn include_file(mut self, name: impl Into<String>) -> Self {
341 self.include_file = Some(name.into());
342 self
343 }
344
345 #[must_use]
355 pub fn generate_view_reflection(mut self, enabled: bool) -> Self {
356 self.generate_view_reflection = Some(enabled);
357 self
358 }
359
360 #[must_use]
363 pub fn view_reflection_include_file(mut self, name: impl Into<String>) -> Self {
364 self.view_reflection_include_file = Some(name.into());
365 self
366 }
367
368 #[must_use]
376 pub fn view_reflection_root_path(mut self, path: impl Into<String>) -> Self {
377 self.view_reflection_root_path = Some(path.into());
378 self
379 }
380
381 pub fn compile(self) -> Result<(), Error> {
389 match (
390 self.descriptor_pool_expr.is_some(),
391 self.file_descriptor_set_bytes_expr.is_some(),
392 ) {
393 (false, false) => return Err(Error::MissingDescriptorBinding),
394 (true, true) => return Err(Error::ConflictingDescriptorBindings),
395 _ => {}
396 }
397
398 if matches!(self.descriptor_source, DescriptorSource::Buf) && !self.includes.is_empty() {
399 return Err(Error::BufWithIncludes);
400 }
401
402 let out_dir = self
403 .out_dir
404 .clone()
405 .or_else(|| std::env::var_os("OUT_DIR").map(PathBuf::from))
406 .ok_or(Error::MissingOutDir)?;
407 std::fs::create_dir_all(&out_dir)?;
408
409 let fds_path = self
410 .file_descriptor_set_path
411 .clone()
412 .unwrap_or_else(|| out_dir.join(DEFAULT_FDS_NAME));
413 if let Some(parent) = fds_path.parent() {
414 std::fs::create_dir_all(parent)?;
415 }
416
417 produce_descriptor_set(
418 &self.descriptor_source,
419 &self.files,
420 &self.includes,
421 &fds_path,
422 )?;
423
424 let fds_bytes = std::fs::read(&fds_path)?;
425 let fds = FileDescriptorSet::decode_from_slice(&fds_bytes)
426 .map_err(Error::DecodeFileDescriptorSet)?;
427
428 let mut cfg = buffa_build::Config::new();
430 cfg = self.apply_reflection_attributes(cfg, &fds);
431 cfg = self.apply_user_passthroughs(cfg);
432
433 cfg = cfg.descriptor_set(&fds_path);
437 cfg = cfg.files(&proto_relative_files(
438 &self.descriptor_source,
439 &self.files,
440 &self.includes,
441 &fds,
442 ));
443 cfg = cfg.out_dir(&out_dir);
444 if let Some(name) = &self.include_file {
445 cfg = cfg.include_file(name);
446 }
447
448 cfg.compile()
449 .map_err(|e| Error::BuffaBuild(e.to_string()))?;
450
451 let want_view_refl = self
452 .generate_view_reflection
453 .unwrap_or(self.file_descriptor_set_bytes_expr.is_some());
454 if want_view_refl {
455 emit_view_reflection_file(&self, &fds, &out_dir)?;
456 }
457
458 emit_cargo_directives(&self.descriptor_source, &self.files, &fds_path);
462
463 Ok(())
464 }
465
466 fn apply_reflection_attributes(
467 &self,
468 mut cfg: buffa_build::Config,
469 fds: &FileDescriptorSet,
470 ) -> buffa_build::Config {
471 let binding_attr = if let Some(expr) = &self.descriptor_pool_expr {
472 format!(r#"#[buffa_reflect(descriptor_pool = "{expr}")]"#)
473 } else if let Some(expr) = &self.file_descriptor_set_bytes_expr {
474 format!(r#"#[buffa_reflect(file_descriptor_set_bytes = "{expr}")]"#)
475 } else {
476 unreachable!("descriptor binding presence enforced above")
477 };
478
479 cfg = cfg.message_attribute(".", "#[derive(::buffa_reflect::ReflectMessage)]");
483 cfg = cfg.message_attribute(".", binding_attr);
484
485 for full_name in collect_message_full_names(fds) {
489 cfg = cfg.message_attribute(
490 format!(".{full_name}"),
491 format!(r#"#[buffa_reflect(message_name = "{full_name}")]"#),
492 );
493 }
494 cfg
495 }
496
497 fn apply_user_passthroughs(&self, mut cfg: buffa_build::Config) -> buffa_build::Config {
498 for (path, attr) in &self.type_attributes {
499 cfg = cfg.type_attribute(path, attr);
500 }
501 for (path, attr) in &self.field_attributes {
502 cfg = cfg.field_attribute(path, attr);
503 }
504 for (path, attr) in &self.message_attributes {
505 cfg = cfg.message_attribute(path, attr);
506 }
507 for (path, attr) in &self.enum_attributes {
508 cfg = cfg.enum_attribute(path, attr);
509 }
510 for (proto, rust) in &self.extern_paths {
511 cfg = cfg.extern_path(proto.clone(), rust.clone());
512 }
513 if !self.bytes_paths.is_empty() {
514 cfg = cfg.use_bytes_type_in(&self.bytes_paths);
515 }
516 if let Some(b) = self.generate_views {
517 cfg = cfg.generate_views(b);
518 }
519 if let Some(b) = self.generate_json {
520 cfg = cfg.generate_json(b);
521 }
522 if let Some(b) = self.generate_text {
523 cfg = cfg.generate_text(b);
524 }
525 if let Some(b) = self.generate_arbitrary {
526 cfg = cfg.generate_arbitrary(b);
527 }
528 if let Some(b) = self.preserve_unknown_fields {
529 cfg = cfg.preserve_unknown_fields(b);
530 }
531 if let Some(b) = self.strict_utf8_mapping {
532 cfg = cfg.strict_utf8_mapping(b);
533 }
534 if let Some(b) = self.allow_message_set {
535 cfg = cfg.allow_message_set(b);
536 }
537 cfg
538 }
539}
540
541fn produce_descriptor_set(
542 source: &DescriptorSource,
543 files: &[PathBuf],
544 includes: &[PathBuf],
545 fds_path: &Path,
546) -> Result<(), Error> {
547 match source {
548 DescriptorSource::Protoc => invoke_protoc(files, includes, fds_path),
549 DescriptorSource::Buf => invoke_buf(fds_path),
550 DescriptorSource::Precompiled(src) => {
551 std::fs::copy(src, fds_path).map(|_| ()).map_err(Error::Io)
552 }
553 }
554}
555
556fn invoke_protoc(files: &[PathBuf], includes: &[PathBuf], out: &Path) -> Result<(), Error> {
557 let protoc: OsString = std::env::var_os("PROTOC").unwrap_or_else(|| "protoc".into());
558 let mut cmd = Command::new(&protoc);
559 cmd.arg("--include_imports");
560 cmd.arg("--include_source_info");
561 {
562 let mut arg = OsString::from("--descriptor_set_out=");
563 arg.push(out);
564 cmd.arg(arg);
565 }
566 for include in includes {
567 let mut arg = OsString::from("--proto_path=");
568 arg.push(include);
569 cmd.arg(arg);
570 }
571 for file in files {
572 cmd.arg(file);
573 }
574 let output = cmd.output().map_err(|source| Error::DescriptorTool {
575 tool: "protoc",
576 source,
577 })?;
578 if !output.status.success() {
579 return Err(Error::DescriptorToolFailed {
580 tool: "protoc",
581 status: output.status,
582 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
583 });
584 }
585 Ok(())
586}
587
588fn invoke_buf(out: &Path) -> Result<(), Error> {
589 let mut cmd = Command::new("buf");
590 cmd.arg("build")
591 .arg("--as-file-descriptor-set")
592 .arg("-o")
593 .arg(out);
594 let output = cmd.output().map_err(|source| Error::DescriptorTool {
595 tool: "buf",
596 source,
597 })?;
598 if !output.status.success() {
599 return Err(Error::DescriptorToolFailed {
600 tool: "buf",
601 status: output.status,
602 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
603 });
604 }
605 Ok(())
606}
607
608fn proto_relative_files(
609 source: &DescriptorSource,
610 files: &[PathBuf],
611 includes: &[PathBuf],
612 fds: &FileDescriptorSet,
613) -> Vec<String> {
614 match source {
615 DescriptorSource::Buf | DescriptorSource::Precompiled(_) => {
616 if files.is_empty() {
620 fds.file.iter().filter_map(|f| f.name.clone()).collect()
621 } else {
622 files
623 .iter()
624 .map(|p| p.to_string_lossy().into_owned())
625 .collect()
626 }
627 }
628 DescriptorSource::Protoc => files
629 .iter()
630 .map(|f| proto_relative_name(f, includes))
631 .filter(|s| !s.is_empty())
632 .collect(),
633 }
634}
635
636fn proto_relative_name(file: &Path, includes: &[PathBuf]) -> String {
639 let mut best: Option<&Path> = None;
641 for include in includes {
642 if let Ok(rel) = file.strip_prefix(include) {
643 match best {
644 Some(prev) if prev.as_os_str().len() <= rel.as_os_str().len() => {}
645 _ => best = Some(rel),
646 }
647 }
648 }
649 best.unwrap_or(file).to_string_lossy().into_owned()
650}
651
652fn emit_view_reflection_file(
655 builder: &Builder,
656 fds: &FileDescriptorSet,
657 out_dir: &Path,
658) -> Result<(), Error> {
659 let file_name = builder
660 .view_reflection_include_file
661 .as_deref()
662 .unwrap_or("_reflect_views.rs");
663 let view_root = builder
664 .view_reflection_root_path
665 .as_deref()
666 .unwrap_or("crate::__buffa::view");
667 let bytes_expr = builder
668 .file_descriptor_set_bytes_expr
669 .as_deref()
670 .ok_or_else(|| {
671 Error::BuffaBuild(
672 "view-reflection emit requires file_descriptor_set_bytes(\"...\") binding".into(),
673 )
674 })?;
675
676 let mut out = String::new();
677 out.push_str(&format!(
678 "// @generated by buffa-reflect-build {} for view reflection. DO NOT EDIT.\n// View \
679 module convention pinned to buffa-codegen as of buffa 0.4.x.\n// If buffa changes its \
680 __buffa::view module layout, regenerate.\n",
681 env!("CARGO_PKG_VERSION"),
682 ));
683
684 for entry in collect_view_entries(fds) {
685 let view_path = format!(
686 "{view_root}::{}{}View<'__a>",
687 entry.module_path, entry.struct_name
688 );
689 let fqn = entry.full_name;
690 out.push_str(&format!(
691 r#"
692impl<'__a> ::buffa_reflect::ReflectMessageView<'__a> for {view_path} {{
693 fn descriptor(&self) -> ::buffa_reflect::MessageDescriptor {{
694 static __INIT: ::std::sync::OnceLock<::buffa_reflect::DescriptorPool> =
695 ::std::sync::OnceLock::new();
696 let pool = __INIT.get_or_init(|| {{
697 ::buffa_reflect::DescriptorPool::decode({bytes_expr})
698 .expect("buffa-reflect: invalid FileDescriptorSet")
699 }});
700 pool.get_message_by_name("{fqn}")
701 .expect(concat!("descriptor for `", "{fqn}", "` not found"))
702 }}
703}}
704"#,
705 ));
706 }
707
708 let path = out_dir.join(file_name);
709 std::fs::write(&path, out)?;
710 Ok(())
711}
712
713struct ViewEntry {
715 full_name: String,
717 module_path: String,
720 struct_name: String,
722}
723
724fn collect_view_entries(fds: &FileDescriptorSet) -> Vec<ViewEntry> {
725 let mut out = Vec::new();
726 for file in &fds.file {
727 let pkg = file.package.as_deref().unwrap_or("");
728 for msg in &file.message_type {
729 collect_view_entries_inner(msg, pkg, "", &mut out);
730 }
731 }
732 out
733}
734
735fn collect_view_entries_inner(
736 msg: &DescriptorProto,
737 proto_scope: &str,
738 rust_scope: &str,
739 out: &mut Vec<ViewEntry>,
740) {
741 let Some(name) = msg.name.as_deref() else {
742 return;
743 };
744 let full_name = if proto_scope.is_empty() {
745 name.to_string()
746 } else {
747 format!("{proto_scope}.{name}")
748 };
749 let is_map_entry = msg
750 .options
751 .as_option()
752 .and_then(|o| o.map_entry)
753 .unwrap_or(false);
754 if !is_map_entry {
755 out.push(ViewEntry {
756 full_name: full_name.clone(),
757 module_path: rust_scope.to_string(),
758 struct_name: name.to_string(),
759 });
760 }
761 if !msg.nested_type.is_empty() {
762 let child_rust_scope = format!("{rust_scope}{}::", to_snake_case(name));
763 for nested in &msg.nested_type {
764 collect_view_entries_inner(nested, &full_name, &child_rust_scope, out);
765 }
766 }
767}
768
769fn to_snake_case(s: &str) -> String {
774 let mut result = String::with_capacity(s.len() + 4);
775 let chars: Vec<char> = s.chars().collect();
776 for (i, &c) in chars.iter().enumerate() {
777 if c.is_uppercase() && i > 0 {
778 let prev = chars[i - 1];
779 let next_is_lower = chars.get(i + 1).is_some_and(|n| n.is_lowercase());
780 if prev.is_lowercase() || (prev.is_uppercase() && next_is_lower) {
781 result.push('_');
782 }
783 }
784 result.push(c.to_lowercase().next().unwrap());
785 }
786 result
787}
788
789fn collect_message_full_names(fds: &FileDescriptorSet) -> Vec<String> {
793 let mut out = Vec::new();
794 for file in &fds.file {
795 let pkg = file.package.as_deref().unwrap_or("");
796 for msg in &file.message_type {
797 collect_messages(msg, pkg, &mut out);
798 }
799 }
800 out
801}
802
803fn collect_messages(msg: &DescriptorProto, scope: &str, out: &mut Vec<String>) {
804 let Some(name) = msg.name.as_deref() else {
805 return;
806 };
807 let full_name = if scope.is_empty() {
808 name.to_string()
809 } else {
810 format!("{scope}.{name}")
811 };
812 out.push(full_name.clone());
813 for nested in &msg.nested_type {
814 collect_messages(nested, &full_name, out);
815 }
816}
817
818fn emit_cargo_directives(source: &DescriptorSource, files: &[PathBuf], fds_path: &Path) {
819 println!("cargo:rerun-if-changed={}", fds_path.display());
820 match source {
821 DescriptorSource::Protoc => {
822 println!("cargo:rerun-if-env-changed=PROTOC");
823 for f in files {
824 println!("cargo:rerun-if-changed={}", f.display());
825 }
826 }
827 DescriptorSource::Buf => {
828 println!("cargo:rerun-if-changed=buf.yaml");
829 if Path::new("buf.lock").exists() {
830 println!("cargo:rerun-if-changed=buf.lock");
831 }
832 }
833 DescriptorSource::Precompiled(p) => {
834 println!("cargo:rerun-if-changed={}", p.display());
835 }
836 }
837}
838
839#[cfg(test)]
840mod tests {
841 use buffa_descriptor::generated::descriptor::FileDescriptorProto;
842
843 use super::*;
844
845 #[test]
846 fn test_collect_messages_walks_nested_and_map_entries() {
847 let labels_entry = DescriptorProto {
848 name: Some("LabelsEntry".to_string()),
849 ..Default::default()
850 };
851 let nested_msg = DescriptorProto {
852 name: Some("Profile".to_string()),
853 ..Default::default()
854 };
855 let user = DescriptorProto {
856 name: Some("User".to_string()),
857 nested_type: vec![nested_msg, labels_entry],
858 ..Default::default()
859 };
860 let file = FileDescriptorProto {
861 name: Some("acme/v1/user.proto".to_string()),
862 package: Some("acme.v1".to_string()),
863 message_type: vec![user],
864 ..Default::default()
865 };
866 let fds = FileDescriptorSet {
867 file: vec![file],
868 ..Default::default()
869 };
870
871 let names = collect_message_full_names(&fds);
872 assert_eq!(
873 names,
874 vec![
875 "acme.v1.User".to_string(),
876 "acme.v1.User.Profile".to_string(),
877 "acme.v1.User.LabelsEntry".to_string(),
878 ]
879 );
880 }
881
882 #[test]
883 fn test_proto_relative_name_strips_longest_include() {
884 let got = proto_relative_name(
885 Path::new("proto/vendor/ext.proto"),
886 &[PathBuf::from("proto/"), PathBuf::from("proto/vendor/")],
887 );
888 assert_eq!(got, "ext.proto");
889 }
890
891 #[test]
892 fn test_compile_errors_when_binding_missing() {
893 let err = Builder::new()
894 .files(&["foo.proto"])
895 .out_dir(std::env::temp_dir())
896 .compile()
897 .unwrap_err();
898 assert!(matches!(err, Error::MissingDescriptorBinding));
899 }
900
901 #[test]
902 fn test_compile_errors_when_both_bindings_set() {
903 let err = Builder::new()
904 .descriptor_pool("crate::POOL")
905 .file_descriptor_set_bytes("crate::BYTES")
906 .out_dir(std::env::temp_dir())
907 .compile()
908 .unwrap_err();
909 assert!(matches!(err, Error::ConflictingDescriptorBindings));
910 }
911}