thru-grpc-client 0.2.21

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

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 mut last_status = None;
    for attempt in 1..=3 {
        let status = Command::new("buf")
            .current_dir(workspace_root)
            .arg("build")
            .arg("--output")
            .arg(output)
            .status();

        match status {
            Ok(status) if status.success() => return Ok(()),
            Ok(status) => {
                last_status = Some(status);
                if attempt < 3 {
                    eprintln!("buf build failed with status {status:?}; retrying");
                    thread::sleep(Duration::from_secs(2 * attempt));
                }
            }
            Err(err) => {
                if err.kind() == std::io::ErrorKind::NotFound {
                    return Err(
                        "buf is not installed. Please install buf from https://buf.build/docs/cli/installation/"
                            .into(),
                    );
                }
                return Err(format!("failed to execute buf build: {err}").into());
            }
        }
    }

    Err(format!(
        "buf build failed with status {:?}. Ensure buf is installed and dependencies are fetched with `buf dep update`.",
        last_status.ok_or("buf build did not report an exit status")?
    )
    .into())
}