From 80efebaa46eb5eed698d29cae25bf10ed57b67f0 Mon Sep 17 00:00:00 2001 From: Iain McGinniss <309153+iainmcgin@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:53:12 +0000 Subject: [PATCH] Use write-if-changed pattern in connectrpc-build Skip writing output files when content is already identical, preserving mtime so Cargo doesn't spuriously recompile downstream crates. Cargo's rebuild decision for include!-ed files is mtime-based (rustc dep-info lists the file, Cargo compares mtime vs fingerprint). Before this change, touching any single .proto file re-ran the build script and unconditionally rewrote every output .rs, bumping all their mtimes and cascading into a full recompile even when N-1 of N files were byte-identical. Mirrors anthropics/buffa#17 and prost-build's write_file_if_changed. --- connectrpc-build/src/lib.rs | 51 ++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/connectrpc-build/src/lib.rs b/connectrpc-build/src/lib.rs index 8807f39..6f221e0 100644 --- a/connectrpc-build/src/lib.rs +++ b/connectrpc-build/src/lib.rs @@ -31,7 +31,6 @@ //! call [`Config::use_buf`]. To avoid both, precompile a `FileDescriptorSet` //! once and ship it alongside your source via [`Config::descriptor_set`]. -use std::io::Write; use std::path::{Path, PathBuf}; use std::process::Command; @@ -251,7 +250,7 @@ impl Config { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } - std::fs::File::create(&path)?.write_all(file.content.as_bytes())?; + write_if_changed(&path, file.content.as_bytes())?; let pkg = name_to_package.get(&file.name).cloned().unwrap_or_default(); entries.push((file.name.clone(), pkg)); } @@ -260,7 +259,7 @@ impl Config { if let Some(ref include_name) = self.include_file { let include_src = generate_include_file(&entries, relative_includes); let include_path = out_dir.join(include_name); - std::fs::File::create(&include_path)?.write_all(include_src.as_bytes())?; + write_if_changed(&include_path, include_src.as_bytes())?; } // 6. Cargo re-run triggers. @@ -281,6 +280,19 @@ impl Default for Config { } } +/// Write `content` to `path` only if the file doesn't already exist with +/// identical content. Cargo's rebuild decision for `include!`-ed files is +/// mtime-based, so an unconditional write here would cascade into +/// recompiling every downstream crate whenever any `.proto` is touched. +fn write_if_changed(path: &Path, content: &[u8]) -> std::io::Result<()> { + if let Ok(existing) = std::fs::read(path) + && existing == content + { + return Ok(()); + } + std::fs::write(path, content) +} + /// Run `protoc` and return the serialized `FileDescriptorSet`. fn run_protoc(files: &[PathBuf], includes: &[PathBuf]) -> Result> { let protoc = std::env::var("PROTOC").unwrap_or_else(|_| "protoc".to_string()); @@ -678,4 +690,37 @@ mod tests { vec!["my/pkg/svc.proto".to_string(), "top.proto".to_string()] ); } + + #[test] + fn write_if_changed_creates_new_file() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("new.rs"); + write_if_changed(&path, b"hello").unwrap(); + assert_eq!(std::fs::read(&path).unwrap(), b"hello"); + } + + #[test] + fn write_if_changed_skips_identical_content() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("same.rs"); + std::fs::write(&path, b"content").unwrap(); + let mtime_before = std::fs::metadata(&path).unwrap().modified().unwrap(); + + // Sleep briefly so a write would produce a distinguishable mtime. + std::thread::sleep(std::time::Duration::from_millis(50)); + + write_if_changed(&path, b"content").unwrap(); + let mtime_after = std::fs::metadata(&path).unwrap().modified().unwrap(); + assert_eq!(mtime_before, mtime_after); + } + + #[test] + fn write_if_changed_overwrites_different_content() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("changed.rs"); + std::fs::write(&path, b"old").unwrap(); + + write_if_changed(&path, b"new").unwrap(); + assert_eq!(std::fs::read(&path).unwrap(), b"new"); + } }