use std::fs;
use std::process;
pub(crate) fn proto_add(file: &str) {
let path = std::path::Path::new(file);
if path.exists() {
eprintln!("Error: proto file '{}' already exists", file);
process::exit(1);
}
if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) {
fs::create_dir_all(parent).unwrap_or_else(|e| {
eprintln!("Failed to create directory '{}': {}", parent.display(), e);
process::exit(1);
});
}
let template = format!("syntax = \"proto3\";\n\npackage {};\n", proto_package(file));
fs::write(path, template).unwrap_or_else(|e| {
eprintln!("Failed to write '{}': {}", file, e);
process::exit(1);
});
println!("Created proto file: {}", file);
}
fn proto_package(file: &str) -> String {
let path = std::path::Path::new(file);
let mut parts: Vec<String> = Vec::new();
if let Some(parent) = path.parent() {
for component in parent.components() {
if let std::path::Component::Normal(seg) = component
&& let Some(seg) = seg.to_str()
{
let clean: String = seg
.chars()
.filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '_')
.collect();
if !clean.is_empty() {
parts.push(clean);
}
}
}
}
if parts.is_empty() {
"service".to_string()
} else {
parts.join(".")
}
}
pub(crate) fn proto_generate(file: &str, server: bool, output: Option<&str>) {
let proto_path = std::path::Path::new(file);
if !proto_path.exists() {
eprintln!("Error: proto file '{}' not found", file);
process::exit(1);
}
if !proto_path.starts_with("proto") {
eprintln!(
"Error: '{}' is outside the proto/ directory; tonic-build scans proto/ only",
file
);
process::exit(1);
}
let kind = if server { "server" } else { "client" };
let out_dir = match (server, output) {
(true, Some(out)) => out.to_string(),
(true, None) => "src/pb_server".to_string(),
(false, _) => "src/pb_client".to_string(),
};
let build_rs = format!(
r#"// Generated by `ecat proto {kind}`. Do not edit by hand.
fn main() -> Result<(), Box<dyn std::error::Error>> {{
let protos: Vec<String> = std::fs::read_dir("proto")?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|x| x == "proto"))
.map(|e| e.path().to_string_lossy().into_owned())
.collect();
if protos.is_empty() {{
return Ok(());
}}
tonic_build::configure()
.build_client({client})
.build_server({server})
.out_dir("{out_dir}")
.compile(&protos, &["proto"])?;
Ok(())
}}
"#,
client = if server { "false" } else { "true" },
server = if server { "true" } else { "false" }
);
if std::path::Path::new("build.rs").exists() {
println!("Note: overwriting existing build.rs");
}
fs::write("build.rs", build_rs).unwrap_or_else(|e| {
eprintln!("Failed to write build.rs: {}", e);
process::exit(1);
});
let toml = fs::read_to_string("Cargo.toml").unwrap_or_else(|e| {
eprintln!("Failed to read Cargo.toml: {}", e);
process::exit(1);
});
let toml = ensure_toml_section(&toml, "build-dependencies", &[("tonic-build", "0.12")]);
let toml = ensure_toml_section(
&toml,
"dependencies",
&[("tonic", "0.12"), ("prost", "0.13")],
);
fs::write("Cargo.toml", toml).unwrap_or_else(|e| {
eprintln!("Failed to write Cargo.toml: {}", e);
process::exit(1);
});
println!("Generated build.rs (tonic-build {kind} codegen)");
println!(" proto files: proto/ (scanned at build time)");
println!(" output: {out_dir}");
println!("Updated Cargo.toml: [build-dependencies] tonic-build, [dependencies] tonic + prost");
println!();
println!("Next steps:");
println!(" cargo build (requires protoc in PATH)");
println!(
" then add `mod pb_{};` to src/main.rs or src/lib.rs",
if server { "server" } else { "client" }
);
}
fn ensure_toml_section(toml: &str, section: &str, deps: &[(&str, &str)]) -> String {
let lines: Vec<&str> = toml.lines().collect();
let mut start: Option<usize> = None;
for (i, line) in lines.iter().enumerate() {
if line.trim() == format!("[{section}]") {
start = Some(i);
break;
}
}
let mut existing: std::collections::HashSet<String> = std::collections::HashSet::new();
let end = match start {
None => 0,
Some(s) => {
let mut e = lines.len();
for (i, line) in lines.iter().enumerate().skip(s + 1) {
let t = line.trim();
if t.starts_with('[') && t.ends_with(']') {
e = i;
break;
}
if !t.starts_with('#')
&& let Some(eq) = t.find('=')
{
existing.insert(t[..eq].trim().to_string());
}
}
e
}
};
let missing: Vec<(&str, &str)> = deps
.iter()
.copied()
.filter(|(k, _)| !existing.contains(*k))
.collect();
if missing.is_empty() {
return toml.to_string();
}
let insert: Vec<String> = missing
.iter()
.map(|(k, v)| format!("{k} = \"{v}\""))
.collect();
let mut out: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
match start {
Some(_) => {
out.splice(end..end, insert);
}
None => {
if out.last().is_none_or(|l| !l.is_empty()) {
out.push(String::new());
}
out.push(format!("[{section}]"));
out.extend(insert);
}
}
out.join("\n") + "\n"
}