From 1e1648388ee7b8f27befadeec8f096bebf060dfb Mon Sep 17 00:00:00 2001 From: Nathaniel Ford Date: Tue, 1 Sep 2026 18:13:11 +0000 Subject: [PATCH 1/2] build: improve in-tree protobuf codegen caching and deletion recovery Avoid rebuilding in-tree protobuf files when source protos and generated markers are unchanged. Detect missing generated directories and proto timestamp changes automatically while preserving fast-path (~0.2s) builds. Remove redundant CARGO_MANIFEST_DIR usages. --- examples/build.rs | 76 +++++++++++++++++++++++++++--------------- grpc-protobuf/build.rs | 34 +++++++++++++++---- 2 files changed, 76 insertions(+), 34 deletions(-) diff --git a/examples/build.rs b/examples/build.rs index 9629ad9d1..275e3451a 100644 --- a/examples/build.rs +++ b/examples/build.rs @@ -23,8 +23,10 @@ */ use std::env; -use std::path::PathBuf; +use std::fs; +use std::path::{Path, PathBuf}; +#[allow(clippy::too_many_lines)] fn main() { println!("cargo:rerun-if-changed=build.rs"); @@ -82,41 +84,61 @@ fn main() { let grpc_helloworld = env::var_os("CARGO_FEATURE_GRPC_HELLOWORLD").is_some(); let grpc_routeguide = env::var_os("CARGO_FEATURE_GRPC_ROUTEGUIDE").is_some(); - if (grpc_helloworld || grpc_routeguide) && env::var_os("GRPC_RUST_REGENERATE_PROTO").is_some() { - let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); - - let generated_dir = manifest_dir.join("generated"); - if generated_dir.exists() { - std::fs::remove_dir_all(&generated_dir) - .expect("All files in generated/ directory should be deletable"); + if grpc_helloworld || grpc_routeguide { + let hw_proto = Path::new("proto/helloworld/helloworld.proto"); + let rg_proto = Path::new("proto/routeguide/route_guide.proto"); + let hw_gen = Path::new("generated/helloworld/generated.rs"); + let rg_gen = Path::new("generated/routeguide/generated.rs"); + + println!("cargo:rerun-if-changed={}", hw_proto.display()); + println!("cargo:rerun-if-changed={}", rg_proto.display()); + println!("cargo:rerun-if-changed={}", hw_gen.display()); + println!("cargo:rerun-if-changed={}", rg_gen.display()); + + let force_regenerate = env::var_os("GRPC_RUST_REGENERATE_PROTO").is_some(); + let generated_missing = !hw_gen.exists() || !rg_gen.exists(); + let proto_newer = match ( + fs::metadata(hw_proto).and_then(|m| m.modified()), + fs::metadata(rg_proto).and_then(|m| m.modified()), + fs::metadata(hw_gen).and_then(|m| m.modified()), + fs::metadata(rg_gen).and_then(|m| m.modified()), + ) { + (Ok(hw_p), Ok(rg_p), Ok(hw_g), Ok(rg_g)) => hw_p > hw_g || rg_p > rg_g, + _ => true, + }; + + if force_regenerate || generated_missing || proto_newer { + let generated_dir = Path::new("generated"); + if generated_dir.exists() { + let _ = fs::remove_dir_all(generated_dir); + } + + grpc_protobuf_build::CodeGen::new() + .output_dir(generated_dir.join("helloworld")) + .input("helloworld.proto") + .include("proto/helloworld") + .client_only() + .compile() + .unwrap(); + + grpc_protobuf_build::CodeGen::new() + .output_dir(generated_dir.join("routeguide")) + .input("route_guide.proto") + .include("proto/routeguide") + .client_only() + .compile() + .unwrap(); } - - grpc_protobuf_build::CodeGen::new() - .output_dir(generated_dir.join("helloworld")) - .input("helloworld.proto") - .include(manifest_dir.join("proto/helloworld")) - .client_only() - .compile() - .unwrap(); - - grpc_protobuf_build::CodeGen::new() - .output_dir(generated_dir.join("routeguide")) - .input("route_guide.proto") - .include(manifest_dir.join("proto/routeguide")) - .client_only() - .compile() - .unwrap(); } if env::var_os("CARGO_FEATURE_GRPC_GCP").is_some() { - let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); let dependencies = protobuf_well_known_types::get_dependency("protobuf_well_known_types") .into_iter() - .map(|d| d.into()) + .map(std::convert::Into::into) .collect(); grpc_protobuf_build::CodeGen::new() - .include(manifest_dir.join("proto/googleapis")) + .include("proto/googleapis") .inputs([ "google/pubsub/v1/pubsub.proto", "google/pubsub/v1/schema.proto", diff --git a/grpc-protobuf/build.rs b/grpc-protobuf/build.rs index 61f60f12f..7d7514292 100644 --- a/grpc-protobuf/build.rs +++ b/grpc-protobuf/build.rs @@ -23,22 +23,42 @@ */ use std::env; -use std::path::PathBuf; +use std::fs; +use std::path::Path; fn main() { - println!("cargo:rerun-if-changed=build.rs"); + let proto_path = Path::new("third_party/googleapis/google/rpc/status.proto"); + let generated_dir = Path::new("generated"); + let gen_marker = Path::new("generated/google/rpc/generated.rs"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed={}", proto_path.display()); + println!("cargo:rerun-if-changed={}", gen_marker.display()); println!("cargo:rerun-if-env-changed=GRPC_RUST_REGENERATE_PROTO"); - if env::var_os("GRPC_RUST_REGENERATE_PROTO").is_some() { - let manifest_dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()); + + let force_regenerate = env::var_os("GRPC_RUST_REGENERATE_PROTO").is_some(); + let generated_missing = !generated_dir.exists() || !gen_marker.exists(); + let proto_newer = match (fs::metadata(proto_path), fs::metadata(gen_marker)) { + (Ok(p_meta), Ok(g_meta)) => match (p_meta.modified(), g_meta.modified()) { + (Ok(p_time), Ok(g_time)) => p_time > g_time, + _ => true, + }, + _ => true, + }; + + if force_regenerate || generated_missing || proto_newer { + if generated_dir.exists() { + let _ = fs::remove_dir_all(generated_dir); + } + let dependencies = protobuf_well_known_types::get_dependency("protobuf_well_known_types") .into_iter() - .map(|d| d.into()) + .map(std::convert::Into::into) .collect(); grpc_protobuf_build::CodeGen::new() - .output_dir(manifest_dir.join("generated")) - .include(manifest_dir.join("third_party/googleapis")) + .output_dir(generated_dir) + .include("third_party/googleapis") .inputs(["google/rpc/status.proto"]) .dependencies(dependencies) .client_only() From 9922cf62ebab0809ec06281d5414aceb8ab121fc Mon Sep 17 00:00:00 2001 From: Nathaniel Ford Date: Tue, 1 Sep 2026 21:21:49 +0000 Subject: [PATCH 2/2] build: add protoc availability fallback and SKIP_GRPC_RUST_PROTO_CODEGEN flag * Check for protoc availability in build scripts and fall back to checked-in code with a warning if missing. * Add SKIP_GRPC_RUST_PROTO_CODEGEN environment variable detection to unconditionally skip codegen during CI checks. * Set SKIP_GRPC_RUST_PROTO_CODEGEN: 1 in semver CI workflow job. --- .github/workflows/CI.yml | 2 + examples/build.rs | 112 +++++++++++++----- .../helloworld/helloworld_grpc.pb.rs | 1 + .../routeguide/route_guide_grpc.pb.rs | 1 + grpc-protobuf/build.rs | 80 ++++++++++--- 5 files changed, 150 insertions(+), 46 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index f3554dc36..1ceb84983 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -291,6 +291,8 @@ jobs: semver: runs-on: ubuntu-latest + env: + SKIP_GRPC_RUST_PROTO_CODEGEN: 1 steps: - uses: actions/checkout@v6 - uses: obi1kenobi/cargo-semver-checks-action@v2 diff --git a/examples/build.rs b/examples/build.rs index 275e3451a..089d65065 100644 --- a/examples/build.rs +++ b/examples/build.rs @@ -81,6 +81,7 @@ fn main() { .unwrap(); println!("cargo:rerun-if-env-changed=GRPC_RUST_REGENERATE_PROTO"); + println!("cargo:rerun-if-env-changed=SKIP_GRPC_RUST_PROTO_CODEGEN"); let grpc_helloworld = env::var_os("CARGO_FEATURE_GRPC_HELLOWORLD").is_some(); let grpc_routeguide = env::var_os("CARGO_FEATURE_GRPC_ROUTEGUIDE").is_some(); @@ -95,39 +96,63 @@ fn main() { println!("cargo:rerun-if-changed={}", hw_gen.display()); println!("cargo:rerun-if-changed={}", rg_gen.display()); + let skip_codegen = env::var_os("SKIP_GRPC_RUST_PROTO_CODEGEN").is_some(); let force_regenerate = env::var_os("GRPC_RUST_REGENERATE_PROTO").is_some(); - let generated_missing = !hw_gen.exists() || !rg_gen.exists(); - let proto_newer = match ( - fs::metadata(hw_proto).and_then(|m| m.modified()), - fs::metadata(rg_proto).and_then(|m| m.modified()), - fs::metadata(hw_gen).and_then(|m| m.modified()), - fs::metadata(rg_gen).and_then(|m| m.modified()), - ) { - (Ok(hw_p), Ok(rg_p), Ok(hw_g), Ok(rg_g)) => hw_p > hw_g || rg_p > rg_g, - _ => true, - }; - - if force_regenerate || generated_missing || proto_newer { - let generated_dir = Path::new("generated"); - if generated_dir.exists() { - let _ = fs::remove_dir_all(generated_dir); - } - grpc_protobuf_build::CodeGen::new() - .output_dir(generated_dir.join("helloworld")) - .input("helloworld.proto") - .include("proto/helloworld") - .client_only() - .compile() - .unwrap(); - - grpc_protobuf_build::CodeGen::new() - .output_dir(generated_dir.join("routeguide")) - .input("route_guide.proto") - .include("proto/routeguide") - .client_only() - .compile() - .unwrap(); + if skip_codegen { + if force_regenerate { + println!( + "cargo:warning=Both SKIP_GRPC_RUST_PROTO_CODEGEN and GRPC_RUST_REGENERATE_PROTO are set. Skipping code generation." + ); + } + assert!( + hw_gen.exists() && rg_gen.exists(), + "SKIP_GRPC_RUST_PROTO_CODEGEN is set, but generated files are missing in generated/" + ); + } else { + let generated_missing = !hw_gen.exists() || !rg_gen.exists(); + let proto_newer = match ( + fs::metadata(hw_proto).and_then(|m| m.modified()), + fs::metadata(rg_proto).and_then(|m| m.modified()), + fs::metadata(hw_gen).and_then(|m| m.modified()), + fs::metadata(rg_gen).and_then(|m| m.modified()), + ) { + (Ok(hw_p), Ok(rg_p), Ok(hw_g), Ok(rg_g)) => hw_p > hw_g || rg_p > rg_g, + _ => true, + }; + + if force_regenerate || generated_missing || proto_newer { + if has_protoc() { + let generated_dir = Path::new("generated"); + if generated_dir.exists() { + let _ = fs::remove_dir_all(generated_dir); + } + + grpc_protobuf_build::CodeGen::new() + .output_dir(generated_dir.join("helloworld")) + .input("helloworld.proto") + .include("proto/helloworld") + .client_only() + .compile() + .unwrap(); + + grpc_protobuf_build::CodeGen::new() + .output_dir(generated_dir.join("routeguide")) + .input("route_guide.proto") + .include("proto/routeguide") + .client_only() + .compile() + .unwrap(); + } else if generated_missing { + panic!( + "Cannot generate protobuf code: protoc is not available and generated files are missing in generated/" + ); + } else { + println!( + "cargo:warning=protoc not found; skipping proto regeneration and using checked-in files." + ); + } + } } } @@ -156,6 +181,31 @@ fn main() { } } +fn has_protoc() -> bool { + #[cfg(feature = "protoc-gen-rust-grpc")] + if protoc_gen_rust_grpc::protoc().is_file() { + return true; + } + if env::var_os("GRPC_RUST_PROTOC_DIR").is_some_and(|dir| { + !dir.is_empty() + && (Path::new(&dir).join("protoc").is_file() + || Path::new(&dir).join("protoc.exe").is_file()) + }) { + return true; + } + if env::var_os("PROTOC").is_some_and(|p| !p.is_empty() && Path::new(&p).is_file()) { + return true; + } + if let Some(path_var) = env::var_os("PATH") { + for dir in env::split_paths(&path_var) { + if dir.join("protoc").is_file() || dir.join("protoc.exe").is_file() { + return true; + } + } + } + false +} + // Manually define the json.helloworld.Greeter service which used a custom JsonCodec to use json // serialization instead of protobuf for sending messages on the wire. // This will result in generated client and server code which relies on its request, response and diff --git a/examples/generated/helloworld/helloworld_grpc.pb.rs b/examples/generated/helloworld/helloworld_grpc.pb.rs index 82e1996d4..df0bc71a4 100644 --- a/examples/generated/helloworld/helloworld_grpc.pb.rs +++ b/examples/generated/helloworld/helloworld_grpc.pb.rs @@ -1,5 +1,6 @@ /// Generated client implementations. pub mod greeter_client { + #![allow(unused_imports, dead_code, missing_docs, clippy::wildcard_imports)] use grpc::client::*; use grpc_protobuf::*; use grpc_protobuf::client::*; diff --git a/examples/generated/routeguide/route_guide_grpc.pb.rs b/examples/generated/routeguide/route_guide_grpc.pb.rs index 56cf27759..2749403cf 100644 --- a/examples/generated/routeguide/route_guide_grpc.pb.rs +++ b/examples/generated/routeguide/route_guide_grpc.pb.rs @@ -1,5 +1,6 @@ /// Generated client implementations. pub mod route_guide_client { + #![allow(unused_imports, dead_code, missing_docs, clippy::wildcard_imports)] use grpc::client::*; use grpc_protobuf::*; use grpc_protobuf::client::*; diff --git a/grpc-protobuf/build.rs b/grpc-protobuf/build.rs index 7d7514292..d34f54c91 100644 --- a/grpc-protobuf/build.rs +++ b/grpc-protobuf/build.rs @@ -35,8 +35,25 @@ fn main() { println!("cargo:rerun-if-changed={}", proto_path.display()); println!("cargo:rerun-if-changed={}", gen_marker.display()); println!("cargo:rerun-if-env-changed=GRPC_RUST_REGENERATE_PROTO"); + println!("cargo:rerun-if-env-changed=SKIP_GRPC_RUST_PROTO_CODEGEN"); + let skip_codegen = env::var_os("SKIP_GRPC_RUST_PROTO_CODEGEN").is_some(); let force_regenerate = env::var_os("GRPC_RUST_REGENERATE_PROTO").is_some(); + + if skip_codegen { + if force_regenerate { + println!( + "cargo:warning=Both SKIP_GRPC_RUST_PROTO_CODEGEN and GRPC_RUST_REGENERATE_PROTO are set. Skipping code generation." + ); + } + assert!( + gen_marker.exists(), + "SKIP_GRPC_RUST_PROTO_CODEGEN is set, but generated files are missing at {}", + gen_marker.display() + ); + return; + } + let generated_missing = !generated_dir.exists() || !gen_marker.exists(); let proto_newer = match (fs::metadata(proto_path), fs::metadata(gen_marker)) { (Ok(p_meta), Ok(g_meta)) => match (p_meta.modified(), g_meta.modified()) { @@ -47,22 +64,55 @@ fn main() { }; if force_regenerate || generated_missing || proto_newer { - if generated_dir.exists() { - let _ = fs::remove_dir_all(generated_dir); - } + if has_protoc() { + if generated_dir.exists() { + let _ = fs::remove_dir_all(generated_dir); + } + + let dependencies = + protobuf_well_known_types::get_dependency("protobuf_well_known_types") + .into_iter() + .map(std::convert::Into::into) + .collect(); - let dependencies = protobuf_well_known_types::get_dependency("protobuf_well_known_types") - .into_iter() - .map(std::convert::Into::into) - .collect(); + grpc_protobuf_build::CodeGen::new() + .output_dir(generated_dir) + .include("third_party/googleapis") + .inputs(["google/rpc/status.proto"]) + .dependencies(dependencies) + .client_only() + .compile() + .unwrap(); + } else if generated_missing { + panic!( + "Cannot generate protobuf code: protoc is not available and generated files are missing at {}", + generated_dir.display() + ); + } else { + println!( + "cargo:warning=protoc not found; skipping proto regeneration and using checked-in files." + ); + } + } +} - grpc_protobuf_build::CodeGen::new() - .output_dir(generated_dir) - .include("third_party/googleapis") - .inputs(["google/rpc/status.proto"]) - .dependencies(dependencies) - .client_only() - .compile() - .unwrap(); +fn has_protoc() -> bool { + if env::var_os("GRPC_RUST_PROTOC_DIR").is_some_and(|dir| { + !dir.is_empty() + && (Path::new(&dir).join("protoc").is_file() + || Path::new(&dir).join("protoc.exe").is_file()) + }) { + return true; + } + if env::var_os("PROTOC").is_some_and(|p| !p.is_empty() && Path::new(&p).is_file()) { + return true; + } + if let Some(path_var) = env::var_os("PATH") { + for dir in env::split_paths(&path_var) { + if dir.join("protoc").is_file() || dir.join("protoc.exe").is_file() { + return true; + } + } } + false }