Skip to content

Commit

Permalink
feat(build): Add direct fds compile support (#819)
Browse files Browse the repository at this point in the history
This commit adds two new compile functions that allow passing a
`FileDescriptorSet` and it will generate the Rust code. This allows
users to use libraries like `protox` directly and in an ergonomic way.
  • Loading branch information
LucioFranco committed Feb 17, 2023
1 parent b845e69 commit e895d4c
Showing 1 changed file with 112 additions and 66 deletions.
178 changes: 112 additions & 66 deletions prost-build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,92 @@ impl Config {
self
}

/// Compile a [`FileDescriptorSet`] into Rust files during a Cargo build with
/// additional code generator configuration options.
///
/// This method is like `compile_protos` function except it does not invoke `protoc`
/// and instead requires the user to supply a [`FileDescriptorSet`].
///
/// # Example `build.rs`
///
/// ```rust,no_run
/// # fn fds() -> FileDescriptorSet { todo!() }
/// fn main() -> std::io::Result<()> {
/// let file_descriptor_set = fds();
///
/// prost_build::Config::new()
/// .compile_fds(file_descriptor_set)?;
/// }
/// ```
pub fn compile_fds(&mut self, fds: FileDescriptorSet) -> Result<()> {
let mut target_is_env = false;
let target: PathBuf = self.out_dir.clone().map(Ok).unwrap_or_else(|| {
env::var_os("OUT_DIR")
.ok_or_else(|| {
Error::new(ErrorKind::Other, "OUT_DIR environment variable is not set")
})
.map(|val| {
target_is_env = true;
Into::into(val)
})
})?;

let requests = fds
.file
.into_iter()
.map(|descriptor| {
(
Module::from_protobuf_package_name(descriptor.package()),
descriptor,
)
})
.collect::<Vec<_>>();

let file_names = requests
.iter()
.map(|req| {
(
req.0.clone(),
req.0.to_file_name_or(&self.default_package_filename),
)
})
.collect::<HashMap<Module, String>>();

let modules = self.generate(requests)?;
for (module, content) in &modules {
let file_name = file_names
.get(module)
.expect("every module should have a filename");
let output_path = target.join(file_name);

let previous_content = fs::read(&output_path);

if previous_content
.map(|previous_content| previous_content == content.as_bytes())
.unwrap_or(false)
{
trace!("unchanged: {:?}", file_name);
} else {
trace!("writing: {:?}", file_name);
fs::write(output_path, content)?;
}
}

if let Some(ref include_file) = self.include_file {
trace!("Writing include file: {:?}", target.join(include_file));
let mut file = fs::File::create(target.join(include_file))?;
self.write_includes(
modules.keys().collect(),
&mut file,
0,
if target_is_env { None } else { Some(&target) },
)?;
file.flush()?;
}

Ok(())
}

/// Compile `.proto` files into Rust files during a Cargo build with additional code generator
/// configuration options.
///
Expand All @@ -903,18 +989,6 @@ impl Config {
protos: &[impl AsRef<Path>],
includes: &[impl AsRef<Path>],
) -> Result<()> {
let mut target_is_env = false;
let target: PathBuf = self.out_dir.clone().map(Ok).unwrap_or_else(|| {
env::var_os("OUT_DIR")
.ok_or_else(|| {
Error::new(ErrorKind::Other, "OUT_DIR environment variable is not set")
})
.map(|val| {
target_is_env = true;
Into::into(val)
})
})?;

// TODO: This should probably emit 'rerun-if-changed=PATH' directives for cargo, however
// according to [1] if any are output then those paths replace the default crate root,
// which is undesirable. Figure out how to do it in an additive way; perhaps gcc-rs has
Expand Down Expand Up @@ -1002,60 +1076,7 @@ impl Config {
)
})?;

let requests = file_descriptor_set
.file
.into_iter()
.map(|descriptor| {
(
Module::from_protobuf_package_name(descriptor.package()),
descriptor,
)
})
.collect::<Vec<_>>();

let file_names = requests
.iter()
.map(|req| {
(
req.0.clone(),
req.0.to_file_name_or(&self.default_package_filename),
)
})
.collect::<HashMap<Module, String>>();

let modules = self.generate(requests)?;
for (module, content) in &modules {
let file_name = file_names
.get(module)
.expect("every module should have a filename");
let output_path = target.join(file_name);

let previous_content = fs::read(&output_path);

if previous_content
.map(|previous_content| previous_content == content.as_bytes())
.unwrap_or(false)
{
trace!("unchanged: {:?}", file_name);
} else {
trace!("writing: {:?}", file_name);
fs::write(output_path, content)?;
}
}

if let Some(ref include_file) = self.include_file {
trace!("Writing include file: {:?}", target.join(include_file));
let mut file = fs::File::create(target.join(include_file))?;
self.write_includes(
modules.keys().collect(),
&mut file,
0,
if target_is_env { None } else { Some(&target) },
)?;
file.flush()?;
}

Ok(())
self.compile_fds(file_descriptor_set)
}

fn write_includes(
Expand Down Expand Up @@ -1360,6 +1381,31 @@ pub fn compile_protos(protos: &[impl AsRef<Path>], includes: &[impl AsRef<Path>]
Config::new().compile_protos(protos, includes)
}

/// Compile a [`FileDescriptorSet`] into Rust files during a Cargo build.
///
/// The generated `.rs` files are written to the Cargo `OUT_DIR` directory, suitable for use with
/// the [include!][1] macro. See the [Cargo `build.rs` code generation][2] example for more info.
///
/// This function should be called in a project's `build.rs`.
///
/// This function can be combined with a crate like [`protox`] which outputs a
/// [`FileDescriptorSet`] and is a pure Rust implementation of `protoc`.
///
/// [`protox`]: https://github.com/andrewhickman/protox
///
/// # Example
/// ```rust,no_run
/// # fn fds() -> FileDescriptorSet { todo!() }
/// fn main() -> std::io::Result<()> {
/// let file_descriptor_set = fds();
///
/// prost_build::compile_fds(file_descriptor_set)?;
/// }
/// ```
pub fn compile_fds(fds: FileDescriptorSet) -> Result<()> {
Config::new().compile_fds(fds)
}

/// Returns the path to the `protoc` binary.
pub fn protoc_from_env() -> PathBuf {
let os_specific_hint = if cfg!(target_os = "macos") {
Expand Down

0 comments on commit e895d4c

Please sign in to comment.