Skip to content

Commit 80efeba

Browse files
committed
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.
1 parent 5a56c62 commit 80efeba

File tree

1 file changed

+48
-3
lines changed

1 file changed

+48
-3
lines changed

connectrpc-build/src/lib.rs

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
//! call [`Config::use_buf`]. To avoid both, precompile a `FileDescriptorSet`
3232
//! once and ship it alongside your source via [`Config::descriptor_set`].
3333
34-
use std::io::Write;
3534
use std::path::{Path, PathBuf};
3635
use std::process::Command;
3736

@@ -251,7 +250,7 @@ impl Config {
251250
if let Some(parent) = path.parent() {
252251
std::fs::create_dir_all(parent)?;
253252
}
254-
std::fs::File::create(&path)?.write_all(file.content.as_bytes())?;
253+
write_if_changed(&path, file.content.as_bytes())?;
255254
let pkg = name_to_package.get(&file.name).cloned().unwrap_or_default();
256255
entries.push((file.name.clone(), pkg));
257256
}
@@ -260,7 +259,7 @@ impl Config {
260259
if let Some(ref include_name) = self.include_file {
261260
let include_src = generate_include_file(&entries, relative_includes);
262261
let include_path = out_dir.join(include_name);
263-
std::fs::File::create(&include_path)?.write_all(include_src.as_bytes())?;
262+
write_if_changed(&include_path, include_src.as_bytes())?;
264263
}
265264

266265
// 6. Cargo re-run triggers.
@@ -281,6 +280,19 @@ impl Default for Config {
281280
}
282281
}
283282

283+
/// Write `content` to `path` only if the file doesn't already exist with
284+
/// identical content. Cargo's rebuild decision for `include!`-ed files is
285+
/// mtime-based, so an unconditional write here would cascade into
286+
/// recompiling every downstream crate whenever any `.proto` is touched.
287+
fn write_if_changed(path: &Path, content: &[u8]) -> std::io::Result<()> {
288+
if let Ok(existing) = std::fs::read(path)
289+
&& existing == content
290+
{
291+
return Ok(());
292+
}
293+
std::fs::write(path, content)
294+
}
295+
284296
/// Run `protoc` and return the serialized `FileDescriptorSet`.
285297
fn run_protoc(files: &[PathBuf], includes: &[PathBuf]) -> Result<Vec<u8>> {
286298
let protoc = std::env::var("PROTOC").unwrap_or_else(|_| "protoc".to_string());
@@ -678,4 +690,37 @@ mod tests {
678690
vec!["my/pkg/svc.proto".to_string(), "top.proto".to_string()]
679691
);
680692
}
693+
694+
#[test]
695+
fn write_if_changed_creates_new_file() {
696+
let dir = tempfile::tempdir().unwrap();
697+
let path = dir.path().join("new.rs");
698+
write_if_changed(&path, b"hello").unwrap();
699+
assert_eq!(std::fs::read(&path).unwrap(), b"hello");
700+
}
701+
702+
#[test]
703+
fn write_if_changed_skips_identical_content() {
704+
let dir = tempfile::tempdir().unwrap();
705+
let path = dir.path().join("same.rs");
706+
std::fs::write(&path, b"content").unwrap();
707+
let mtime_before = std::fs::metadata(&path).unwrap().modified().unwrap();
708+
709+
// Sleep briefly so a write would produce a distinguishable mtime.
710+
std::thread::sleep(std::time::Duration::from_millis(50));
711+
712+
write_if_changed(&path, b"content").unwrap();
713+
let mtime_after = std::fs::metadata(&path).unwrap().modified().unwrap();
714+
assert_eq!(mtime_before, mtime_after);
715+
}
716+
717+
#[test]
718+
fn write_if_changed_overwrites_different_content() {
719+
let dir = tempfile::tempdir().unwrap();
720+
let path = dir.path().join("changed.rs");
721+
std::fs::write(&path, b"old").unwrap();
722+
723+
write_if_changed(&path, b"new").unwrap();
724+
assert_eq!(std::fs::read(&path).unwrap(), b"new");
725+
}
681726
}

0 commit comments

Comments
 (0)