thru-grpc-client 0.1.21

Thru GRPC Client
use std::{
    env,
    error::Error,
    fs,
    path::{Path, PathBuf},
    process::Command,
};

use prost::Message;
use prost_types::FileDescriptorSet;
use tonic_prost_build as tonic_build;
use walkdir::WalkDir;

fn main() -> Result<(), Box<dyn Error>> {
    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
    let workspace_root = manifest_dir
        .parent()
        .and_then(Path::parent)
        .ok_or("failed to determine workspace root")?;
    let proto_root_local = manifest_dir.join("proto");
    let proto_root = if proto_root_local.exists() {
        proto_root_local
    } else {
        workspace_root.join("proto")
    };

    println!("cargo:rerun-if-env-changed=PROTOC");
    println!("cargo:rerun-if-env-changed=PROTOC_INCLUDE");
    println!("cargo:rerun-if-env-changed=BUF_CACHE_DIR");
    println!(
        "cargo:rerun-if-changed={}",
        manifest_dir.join("build.rs").display()
    );
    let buf_config_dir = if manifest_dir.join("buf.yaml").exists() {
        manifest_dir.clone()
    } else {
        workspace_root.to_path_buf()
    };
    println!(
        "cargo:rerun-if-changed={}",
        buf_config_dir.join("buf.yaml").display()
    );
    println!(
        "cargo:rerun-if-changed={}",
        buf_config_dir.join("buf.lock").display()
    );

    for entry in WalkDir::new(&proto_root).follow_links(true) {
        let entry = entry?;
        if entry.file_type().is_file()
            && entry.path().extension().and_then(|ext| ext.to_str()) == Some("proto")
        {
            println!("cargo:rerun-if-changed={}", entry.path().display());
        }
    }

    let out_dir = PathBuf::from(env::var("OUT_DIR")?);
    let descriptor_path = out_dir.join("thru_descriptor.bin");

    generate_descriptor(&buf_config_dir, &descriptor_path)?;

    let descriptor_bytes = fs::read(&descriptor_path)?;
    let mut descriptor = FileDescriptorSet::decode(descriptor_bytes.as_slice())?;
    for file in &mut descriptor.file {
        if matches!(file.syntax.as_deref(), Some("editions")) {
            file.syntax = Some("proto3".to_string());
        }
    }

    tonic_build::configure()
        .build_client(true)
        .build_server(false)
        .compile_fds(descriptor)?;

    Ok(())
}

fn generate_descriptor(workspace_root: &Path, output: &Path) -> Result<(), Box<dyn Error>> {
    if let Some(parent) = output.parent() {
        fs::create_dir_all(parent)?;
    }

    let status = Command::new("buf")
        .current_dir(workspace_root)
        .arg("build")
        .arg("--output")
        .arg(output)
        .status();

    match status {
        Ok(status) if status.success() => Ok(()),
        Ok(status) => Err(format!(
            "buf build failed with status {status:?}. Ensure buf is installed and dependencies are fetched with `buf dep update`."
        )
        .into()),
        Err(err) => {
            if err.kind() == std::io::ErrorKind::NotFound {
                Err(format!(
                    "buf is not installed. Please install buf from https://buf.build/docs/cli/installation/"
                ).into())
            } else {
                Err(format!("failed to execute buf build: {err}").into())
            }
        }
    }
}