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"); + } }