Skip to content

Commit 2b19b6c

Browse files
committed
Add feature to prost-build to allow runtime specification of protoc
Opening this as a way of opening the discussion about an issue that I was running into. If you would prefer a different approach, I'm happy to discuss that. Within multi-language environments, such as bazel, cargo isn't always able to drive the build process. However, prost and prost-build provides an amazing foundation of which to build off. The general idea of this commit was to open up prost-build to having tools built off of it, while still preserving previous behavior. Currently, if PROTOC or PROTO_INCLUDE are not set at build-time, then the build fails. This commit introduces feature, runtime-protoc. This hides env! invocations when enabled, allowing compilation, under the assumption that the protoc location will be provided at runtime. This wouldn't be my first preference, but I was prioritizing the build-break if the environment flags were not specified. I would generally have preferred grabbing those with option_env! macros and then trying to gracefully handle the error at runtime protoc has not been provided at build time or runtime similar, since this is already covered by tests. That would allow removal of the feature flag. It would also make the code a little cleaner, since there's not a great way to do conditionals with cfg (see rust-lang/rust#71679).
1 parent 2de785a commit 2b19b6c

File tree

2 files changed

+72
-4
lines changed

2 files changed

+72
-4
lines changed

prost-build/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ readme = "README.md"
99
description = "A Protocol Buffers implementation for the Rust Language."
1010
edition = "2018"
1111

12+
[features]
13+
# Modify prost-build to be built and run as an executable outside of the normal
14+
# build.rs environment. Requires explicitly setting the protoc path at runtime.
15+
runtime-protoc = []
16+
1217
[dependencies]
1318
bytes = "0.5"
1419
heck = "0.3"

prost-build/src/lib.rs

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,8 @@ pub struct Config {
189189
strip_enum_prefix: bool,
190190
out_dir: Option<PathBuf>,
191191
extern_paths: Vec<(String, String)>,
192+
#[cfg(feature = "runtime-protoc")]
193+
protoc: Option<PathBuf>,
192194
}
193195

194196
impl Config {
@@ -484,6 +486,19 @@ impl Config {
484486
self
485487
}
486488

489+
/// Specifies the path of the protoc executable to be used if using `runtime-protoc`.
490+
///
491+
/// If not set, then `compile_protos` will result in an error when the `runtime-protoc` feature
492+
/// is enabled, otherwise, `PROTOC` is expected to be set at build time.
493+
#[cfg(feature = "runtime-protoc")]
494+
pub fn protoc<P>(&mut self, path: P) -> &mut Self
495+
where
496+
P: Into<PathBuf>,
497+
{
498+
self.protoc = Some(path.into());
499+
self
500+
}
501+
487502
/// Compile `.proto` files into Rust files during a Cargo build with additional code generator
488503
/// configuration options.
489504
///
@@ -523,7 +538,7 @@ impl Config {
523538
let tmp = tempfile::Builder::new().prefix("prost-build").tempdir()?;
524539
let descriptor_set = tmp.path().join("prost-descriptor-set");
525540

526-
let mut cmd = Command::new(protoc());
541+
let mut cmd = Command::new(self.get_protoc()?);
527542
cmd.arg("--include_imports")
528543
.arg("--include_source_info")
529544
.arg("-o")
@@ -535,7 +550,9 @@ impl Config {
535550

536551
// Set the protoc include after the user includes in case the user wants to
537552
// override one of the built-in .protos.
538-
cmd.arg("-I").arg(protoc_include());
553+
if let Some(protoc_include_dir) = get_protoc_include() {
554+
cmd.arg("-I").arg(protoc_include_dir);
555+
}
539556

540557
for proto in protos {
541558
cmd.arg(proto.as_ref());
@@ -613,6 +630,23 @@ impl Config {
613630
.map(to_snake)
614631
.collect()
615632
}
633+
634+
/// Provides a path to a protoc executable, if available. Provides a means of hiding the
635+
/// runtime-protoc feature usage from the rest of the logic.
636+
fn get_protoc(&self) -> Result<PathBuf> {
637+
#[cfg(not(feature = "runtime-protoc"))]
638+
return Ok(protoc().to_path_buf());
639+
640+
#[cfg(feature = "runtime-protoc")]
641+
match &self.protoc {
642+
Some(x) => Ok(x.clone()),
643+
None => Err(Error::new(
644+
ErrorKind::Other,
645+
"Protoc was not specified. Provide it using Config::protoc when building the \
646+
config or by disabling the runtime-protoc feature",
647+
)),
648+
}
649+
}
616650
}
617651

618652
impl default::Default for Config {
@@ -626,6 +660,8 @@ impl default::Default for Config {
626660
strip_enum_prefix: true,
627661
out_dir: None,
628662
extern_paths: Vec::new(),
663+
#[cfg(feature = "runtime-protoc")]
664+
protoc: None,
629665
}
630666
}
631667
}
@@ -680,15 +716,28 @@ where
680716
}
681717

682718
/// Returns the path to the `protoc` binary.
719+
#[cfg(not(feature = "runtime-protoc"))]
683720
pub fn protoc() -> &'static Path {
684721
Path::new(env!("PROTOC"))
685722
}
686723

687724
/// Returns the path to the Protobuf include directory.
725+
#[cfg(not(feature = "runtime-protoc"))]
688726
pub fn protoc_include() -> &'static Path {
689727
Path::new(env!("PROTOC_INCLUDE"))
690728
}
691729

730+
/// Returns the path to the protobuf include directory if the configuration allows it.
731+
fn get_protoc_include() -> Option<&'static Path> {
732+
#[cfg(not(feature = "runtime-protoc"))]
733+
return Some(protoc_include());
734+
735+
// We expect, with runtime-protoc enabled, that all include directories will be passed at
736+
// runtime.
737+
#[cfg(feature = "runtime-protoc")]
738+
None
739+
}
740+
692741
#[cfg(test)]
693742
mod tests {
694743
use super::*;
@@ -759,10 +808,24 @@ mod tests {
759808
}
760809
}
761810

811+
// Test helper to make sure tests run under both the default and under runtime-protoc, by still
812+
// taking advantage of the fact PROTOC is still set by the build.rs file. This is slightly at
813+
// odds with the intended use-case, but seemed like a better option than recreating the build.rs
814+
// file logic here to lookup the appopriate protoc executable in third-party for the platform.
815+
fn config_new() -> Config {
816+
#![allow(unused_mut)]
817+
let mut config = Config::new();
818+
819+
#[cfg(feature = "runtime-protoc")]
820+
config.protoc(env!("PROTOC"));
821+
822+
config
823+
}
824+
762825
#[test]
763826
fn smoke_test() {
764827
let _ = env_logger::try_init();
765-
Config::new()
828+
config_new()
766829
.service_generator(Box::new(ServiceTraitGenerator))
767830
.compile_protos(&["src/smoke_test.proto"], &["src"])
768831
.unwrap();
@@ -775,7 +838,7 @@ mod tests {
775838
let state = Rc::new(RefCell::new(MockState::default()));
776839
let gen = MockServiceGenerator::new(Rc::clone(&state));
777840

778-
Config::new()
841+
config_new()
779842
.service_generator(Box::new(gen))
780843
.compile_protos(&["src/hello.proto", "src/goodbye.proto"], &["src"])
781844
.unwrap();

0 commit comments

Comments
 (0)