use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum BuildError {
Io(std::io::Error),
ProtoBuild(Box<dyn std::error::Error>),
NoProtoFiles(PathBuf),
InvalidProtoDir(PathBuf),
}
impl std::fmt::Display for BuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BuildError::Io(e) => write!(f, "IO error: {}", e),
BuildError::ProtoBuild(e) => write!(f, "Proto compilation error: {}", e),
BuildError::NoProtoFiles(dir) => {
write!(f, "No .proto files found in directory: {}", dir.display())
}
BuildError::InvalidProtoDir(dir) => write!(
f,
"Proto directory does not exist or is not a directory: {}",
dir.display()
),
}
}
}
impl std::error::Error for BuildError {}
impl From<std::io::Error> for BuildError {
fn from(e: std::io::Error) -> Self {
BuildError::Io(e)
}
}
pub type BuildResult<T> = Result<T, BuildError>;
pub fn compile_protos_from_dir<P: AsRef<Path>>(proto_dir: P) -> BuildResult<()> {
let proto_dir = proto_dir.as_ref();
if !proto_dir.exists() || !proto_dir.is_dir() {
return Err(BuildError::InvalidProtoDir(proto_dir.to_path_buf()));
}
let proto_files: Vec<PathBuf> = discover_proto_files(proto_dir)?;
if proto_files.is_empty() {
return Err(BuildError::NoProtoFiles(proto_dir.to_path_buf()));
}
let out_dir = std::env::var("OUT_DIR").map_err(|e| {
BuildError::ProtoBuild(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("OUT_DIR not set: {}", e),
)))
})?;
let pkg_name = std::env::var("CARGO_PKG_NAME")
.map_err(|e| {
BuildError::ProtoBuild(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("CARGO_PKG_NAME not set: {}", e),
)))
})?
.replace('-', "_");
let descriptor_path = format!("{}/{}_descriptor.bin", out_dir, pkg_name);
println!(
"cargo:warning=Compiling {} proto files from {}",
proto_files.len(),
proto_dir.display()
);
for proto_file in &proto_files {
println!("cargo:warning= - {}", proto_file.display());
}
compile_protos_with_descriptor(&proto_files, proto_dir, &descriptor_path)?;
println!("cargo:warning=Generated descriptor: {}", descriptor_path);
Ok(())
}
pub fn compile_service_protos() -> BuildResult<()> {
let proto_dir = std::env::var("ACTON_PROTO_DIR").unwrap_or_else(|_| "proto".to_string());
println!("cargo:warning=Using proto directory: {}", proto_dir);
compile_protos_from_dir(proto_dir)
}
fn discover_proto_files(dir: &Path) -> BuildResult<Vec<PathBuf>> {
let mut proto_files = Vec::new();
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file() {
if let Some(ext) = path.extension() {
if ext == "proto" {
proto_files.push(path);
}
}
} else if path.is_dir() {
proto_files.extend(discover_proto_files(&path)?);
}
}
proto_files.sort();
Ok(proto_files)
}
fn compile_protos_with_descriptor(
proto_files: &[PathBuf],
proto_include_dir: &Path,
descriptor_path: &str,
) -> BuildResult<()> {
let proto_paths: Vec<&str> = proto_files
.iter()
.map(|p| p.to_str().expect("Invalid UTF-8 in proto path"))
.collect();
let include_dirs = vec![proto_include_dir
.to_str()
.expect("Invalid UTF-8 in include path")];
#[cfg(feature = "grpc")]
{
tonic_prost_build::configure()
.file_descriptor_set_path(descriptor_path)
.compile_protos(&proto_paths, &include_dirs)
.map_err(|e| BuildError::ProtoBuild(Box::new(e)))?;
Ok(())
}
#[cfg(not(feature = "grpc"))]
{
let _ = (proto_paths, include_dirs, descriptor_path);
Err(BuildError::ProtoBuild(Box::new(std::io::Error::other(
"grpc feature not enabled",
))))
}
}
pub fn compile_specific_protos<P: AsRef<Path>>(
proto_files: &[P],
include_dirs: &[P],
descriptor_name: &str,
) -> BuildResult<()> {
let out_dir = std::env::var("OUT_DIR").map_err(|e| {
BuildError::ProtoBuild(Box::new(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("OUT_DIR not set: {}", e),
)))
})?;
let descriptor_path = format!("{}/{}", out_dir, descriptor_name);
let proto_paths: Vec<&str> = proto_files
.iter()
.map(|p| p.as_ref().to_str().expect("Invalid UTF-8 in proto path"))
.collect();
let include_paths: Vec<&str> = include_dirs
.iter()
.map(|p| p.as_ref().to_str().expect("Invalid UTF-8 in include path"))
.collect();
compile_protos_with_descriptor(
&proto_paths.iter().map(PathBuf::from).collect::<Vec<_>>(),
Path::new(include_paths[0]),
&descriptor_path,
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_discover_proto_files() {
let _result: BuildResult<Vec<PathBuf>> = discover_proto_files(Path::new("nonexistent"));
}
}