1use std::fs::OpenOptions;
2use std::path::PathBuf;
3use std::process::Command;
4use std::str::FromStr;
5use std::{env, fs};
6
7use pilota_build::ir::ItemKind;
8use pilota_build::parser::{Parser, ProtobufParser, ThriftParser};
9
10use crate::{
11 deal_output, exit_with_warning, os_arch::get_go_os_arch_from_env, GenMode, BUILD_MODE, GEN_MODE,
12};
13
14const CGOBIN: &'static str = "cgobin";
15
16#[derive(Default, Debug, Clone)]
17pub struct Config {
18 pub idl_file: PathBuf,
19 pub target_crate_dir: Option<PathBuf>,
21 pub go_root_path: Option<PathBuf>,
23 pub go_mod_parent: &'static str,
24 pub use_goffi_cdylib: bool,
26 pub add_clib_to_git: bool,
28}
29
30#[derive(Debug, Clone)]
31pub(crate) enum IdlType {
32 Proto,
33 Thrift,
34 ProtoNoCodec,
35 ThriftNoCodec,
36}
37impl Default for IdlType {
38 fn default() -> Self {
39 IdlType::Proto
40 }
41}
42#[derive(Debug, Clone)]
44pub struct UnitLikeStructPath(pub &'static str);
45
46#[derive(Debug, Clone)]
47pub struct GoObjectPath {
48 pub import: String,
50 pub object_ident: String,
52}
53
54#[derive(Default, Debug, Clone)]
55pub(crate) struct WorkConfig {
56 config: Config,
57 pub(crate) go_buildmode: &'static str,
58 pub(crate) rustc_link_kind_goffi: &'static str,
59 pub(crate) idl_file: PathBuf,
60 pub(crate) idl_include_dir: PathBuf,
61 pub(crate) idl_type: IdlType,
62 pub(crate) rust_clib_name_base: String,
63 pub(crate) go_clib_name_base: String,
64 pub(crate) target_out_dir: PathBuf,
65 pub(crate) pkg_dir: PathBuf,
66 pub(crate) pkg_name: String,
67 pub(crate) gomod_name: String,
68 pub(crate) gomod_path: String,
69 pub(crate) gomod_file: PathBuf,
70 pub(crate) rust_mod_dir: PathBuf,
71 pub(crate) rust_mod_gen_file: PathBuf,
72 pub(crate) rust_mod_impl_file: PathBuf,
73 pub(crate) rust_mod_gen_name: String,
74 pub(crate) go_lib_file: PathBuf,
75 pub(crate) clib_gen_dir: PathBuf,
76 pub(crate) go_main_dir: PathBuf,
77 pub(crate) go_main_file: PathBuf,
78 pub(crate) go_main_impl_file: PathBuf,
79 pub(crate) rust_clib_file: PathBuf,
80 pub(crate) rust_clib_header: PathBuf,
81 pub(crate) go_clib_file: PathBuf,
82 pub(crate) go_clib_header: PathBuf,
83 pub(crate) has_goffi: bool,
84 pub(crate) has_rustffi: bool,
85 pub(crate) rust_mod_impl_name: String,
86 pub(crate) fingerprint: String,
87 pub(crate) fingerprint_path: PathBuf,
88}
89
90impl WorkConfig {
91 pub(crate) fn new(config: Config) -> WorkConfig {
92 let mut c = WorkConfig::default();
93 c.config = config;
94 c.rust_mod_impl_name = "FfiImpl".to_string();
95 c.go_buildmode = if c.config.use_goffi_cdylib {
96 "c-shared"
97 } else {
98 "c-archive"
99 };
100 c.rustc_link_kind_goffi = if c.config.use_goffi_cdylib {
101 "dylib"
102 } else {
103 "static"
104 };
105 c.idl_file = c.config.idl_file.clone();
106 c.idl_include_dir = c.idl_file.parent().unwrap().to_path_buf();
107 c.idl_type = Self::new_idl_type(&c.idl_file);
108 c.rust_clib_name_base = env::var("CARGO_PKG_NAME").unwrap().replace("-", "_");
109 c.go_clib_name_base = "go_".to_string() + &c.rust_clib_name_base;
110 c.target_out_dir = Self::new_target_out_dir();
111 c.clib_gen_dir = c.target_out_dir.clone();
112 c.fingerprint_path = c.clib_gen_dir.join("fcplug.fingerprint");
113 c.pkg_dir = Self::new_pkg_dir(&c.config.target_crate_dir);
114 c.gomod_file = c.pkg_dir.join("go.mod");
115 c.pkg_name = Self::new_pkg_name(&c.pkg_dir);
116 c.gomod_name = c.pkg_name.clone();
117 c.gomod_path = format!(
118 "{}/{}",
119 c.config.go_mod_parent.trim_end_matches("/"),
120 c.gomod_name
121 );
122 c.rust_mod_dir = c.pkg_dir.join("src").join(c.pkg_name.clone() + "_ffi");
123 c.rust_mod_gen_name = format!("{}_gen", c.pkg_name.clone());
124 let file_name_base = &c.rust_mod_gen_name;
125 c.rust_mod_gen_file = c.rust_mod_dir.join(format!("{file_name_base}.rs"));
126 c.rust_mod_impl_file = c.rust_mod_dir.join("mod.rs");
127 c.go_main_dir = c.pkg_dir.join(CGOBIN);
128 let go_file_suffix = match get_go_os_arch_from_env() {
129 Ok((os, arch)) => {
130 format!("_{}_{}", os.as_ref(), arch.as_ref())
131 }
132 Err(err) => {
133 println!("cargo:warning={}", err);
134 String::new()
135 }
136 };
137 c.go_lib_file = c
138 .pkg_dir
139 .join(format!("{file_name_base}{go_file_suffix}.go"));
140 c.go_main_file = c
141 .go_main_dir
142 .join(format!("clib_goffi_gen{go_file_suffix}.go"));
143 c.go_main_impl_file = c.go_main_dir.join("clib_goffi_impl.go");
144 c.set_rust_clib_paths();
145 c.set_go_clib_paths();
146 c.check_go_mod_path();
147 c.set_fingerprint();
148 c.clean_idl();
149 let _ = c
150 .init_files()
151 .inspect_err(|e| exit_with_warning(-2, format!("failed init files to {e:?}")));
152 c.git_add();
153 c
154 }
155
156 fn new_idl_type(idl_file: &PathBuf) -> IdlType {
157 match idl_file.extension().unwrap().to_str().unwrap() {
158 "thrift" => match GEN_MODE {
159 GenMode::Codec => IdlType::Thrift,
160 GenMode::NoCodec => IdlType::ThriftNoCodec,
161 },
162 "proto" => match GEN_MODE {
163 GenMode::Codec => IdlType::Proto,
164 GenMode::NoCodec => IdlType::ProtoNoCodec,
165 },
166 x => {
167 println!("cargo:warning=unsupported idl file extension: {x}");
168 std::process::exit(404);
169 }
170 }
171 }
172
173 fn new_target_out_dir() -> PathBuf {
174 let target_dir = env::var("CARGO_TARGET_DIR").map_or_else(
175 |_| {
176 PathBuf::from(env::var("CARGO_WORKSPACE_DIR").unwrap_or_else(|_| {
177 let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap_or_default());
178 let mdir = env::var("CARGO_MANIFEST_DIR").unwrap_or_default();
179 if out_dir.starts_with(&mdir) {
180 mdir
181 } else {
182 let mut p = PathBuf::new();
183 let mut coms = Vec::new();
184 let mut start = false;
185 for x in out_dir.components().rev() {
186 if !start && x.as_os_str() == "target" {
187 start = true;
188 continue;
189 }
190 if start {
191 coms.insert(0, x);
192 }
193 }
194 for x in coms {
195 p = p.join(x);
196 }
197 p.to_str().unwrap().to_string()
198 }
199 }))
200 .join("target")
201 },
202 PathBuf::from,
203 );
204 let full_target_dir = target_dir.join(env::var("TARGET").unwrap());
205 if full_target_dir.is_dir()
206 && PathBuf::from(env::var("OUT_DIR").unwrap())
207 .canonicalize()
208 .unwrap()
209 .starts_with(full_target_dir.canonicalize().unwrap())
210 {
211 full_target_dir
212 } else {
213 target_dir
214 }
215 .join(BUILD_MODE)
216 .canonicalize()
217 .unwrap()
218 }
219
220 fn new_pkg_dir(target_crate_dir: &Option<PathBuf>) -> PathBuf {
221 if let Some(target_crate_dir) = target_crate_dir {
222 target_crate_dir.clone().canonicalize().unwrap()
223 } else {
224 PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
225 .canonicalize()
226 .unwrap()
227 }
228 }
229
230 fn new_pkg_name(pkg_dir: &PathBuf) -> String {
231 pkg_dir
232 .file_name()
233 .unwrap()
234 .to_str()
235 .unwrap()
236 .replace(".", "_")
237 .replace("-", "_")
238 .trim_start_matches("_")
239 .to_string()
240 .trim_end_matches("_")
241 .to_string()
242 }
243
244 fn set_rust_clib_paths(&mut self) {
245 self.rust_clib_file = self
246 .clib_gen_dir
247 .join(format!("lib{}.a", self.rust_clib_name_base));
248 self.rust_clib_header = self
249 .clib_gen_dir
250 .join(format!("{}.h", self.rust_clib_name_base));
251 }
252
253 fn set_go_clib_paths(&mut self) {
254 self.go_clib_file = self.clib_gen_dir.join(format!(
255 "lib{}{}",
256 self.go_clib_name_base,
257 if self.config.use_goffi_cdylib {
258 ".so"
259 } else {
260 ".a"
261 }
262 ));
263 self.go_clib_header = self
264 .clib_gen_dir
265 .join(format!("{}.h", self.go_clib_name_base));
266 }
267
268 fn git_add(&self) {
269 if !self.config.add_clib_to_git {
270 return;
271 }
272 deal_output(
273 Command::new("git")
274 .arg("add")
275 .arg("-f")
276 .args([
277 self.go_clib_header.display().to_string(),
278 self.go_clib_file.display().to_string(),
279 self.rust_clib_header.display().to_string(),
280 self.rust_clib_file.display().to_string(),
281 self.fingerprint_path.display().to_string(),
282 ])
283 .output(),
284 );
285 }
286
287 fn set_fingerprint(&mut self) {
288 self.fingerprint = walkdir::WalkDir::new(&self.pkg_dir)
289 .into_iter()
290 .filter_map(|entry| entry.ok())
291 .filter(|entry| {
292 if entry
293 .path()
294 .extension()
295 .map(|ext| ext == "go" || ext == "rs" || ext == "toml" || ext == "proto")
296 .unwrap_or_default()
297 {
298 if let Ok(metadata) = entry.metadata() {
299 return metadata.is_file();
300 }
301 };
302 return false;
303 })
304 .fold(String::new(), |acc, m| {
305 let digest = md5::compute(fs::read(m.path()).unwrap());
306 format!("{acc}|{digest:x}")
307 });
308 }
309 pub(crate) fn update_fingerprint(&self) -> bool {
310 if fs::read_to_string(&self.fingerprint_path).unwrap_or_default() != self.fingerprint {
311 fs::write(&self.fingerprint_path, self.fingerprint.as_str()).unwrap();
312 return true;
313 }
314 return false;
315 }
316
317 fn clean_idl(&mut self) {
318 let mut ret = match self.idl_type {
319 IdlType::Proto | IdlType::ProtoNoCodec => {
320 let mut parser = ProtobufParser::default();
321 Parser::include_dirs(&mut parser, vec![self.idl_include_dir.clone()]);
322 Parser::input(&mut parser, &self.idl_file);
323 let (descs, ret) = parser.parse_and_typecheck();
324 for desc in descs {
325 if desc.package.is_some() {
326 exit_with_warning(-1, "IDL-Check: The 'package' should not be configured");
327 }
328 if let Some(opt) = desc.options.as_ref() {
329 if opt.go_package.is_some() {
330 exit_with_warning(
331 -1,
332 "IDL-Check: The 'option go_package' should not be configured",
333 );
334 }
335 }
336 }
337 ret
338 }
339 IdlType::Thrift | IdlType::ThriftNoCodec => {
340 let mut parser = ThriftParser::default();
341 Parser::include_dirs(&mut parser, vec![self.idl_include_dir.clone()]);
342 Parser::input(&mut parser, &self.idl_file);
343 let ret = parser.parse();
344 ret
345 }
346 };
347
348 let file = ret.files.pop().unwrap();
349 if !file.uses.is_empty() {
350 match self.idl_type {
351 IdlType::Proto | IdlType::ProtoNoCodec => {
352 exit_with_warning(-1, "IDL-Check: Does not support Protobuf 'import'.")
353 }
354 IdlType::Thrift | IdlType::ThriftNoCodec => {
355 exit_with_warning(-1, "IDL-Check: Does not support Thrift 'include'.")
356 }
357 }
358 }
359
360 for item in &file.items {
361 match &item.kind {
362 ItemKind::Message(_) => {}
363 ItemKind::Service(service_item) => {
364 match service_item.name.to_lowercase().as_str() {
365 "goffi" => self.has_goffi = true,
366 "rustffi" => self.has_rustffi = true,
367 _ => exit_with_warning(
368 -1,
369 "IDL-Check: Protobuf Service name can only be: 'GoFFI', 'RustFFI'.",
370 ),
371 }
372 }
373 _ => match self.idl_type {
374 IdlType::Proto | IdlType::ProtoNoCodec => exit_with_warning(
375 -1,
376 format!(
377 "IDL-Check: Protobuf Item '{}' not supported.",
378 format!("{:?}", item)
379 .trim_start_matches("Item { kind: ")
380 .split_once("(")
381 .unwrap()
382 .0
383 .to_lowercase()
384 ),
385 ),
386 IdlType::Thrift | IdlType::ThriftNoCodec => exit_with_warning(
387 -1,
388 format!(
389 "Thrift Item '{}' not supported.",
390 format!("{:?}", item)
391 .split_once("(")
392 .unwrap()
393 .0
394 .to_lowercase()
395 ),
396 ),
397 },
398 }
399 }
400 self.tidy_idl()
401 }
402
403 fn tidy_idl(&mut self) {
404 let go_mod_name = &self.gomod_name;
405 match self.idl_type {
406 IdlType::Proto | IdlType::ProtoNoCodec => {
407 self.idl_file = self.target_out_dir.join(go_mod_name.clone() + ".proto");
408 fs::write(
409 &self.idl_file,
410 fs::read_to_string(&self.config.idl_file).unwrap()
411 + &format!(
412 "\noption go_package=\"./;{go_mod_name}\";\npackage {go_mod_name};\n"
413 ),
414 )
415 .unwrap();
416 }
417 IdlType::Thrift | IdlType::ThriftNoCodec => {
418 self.idl_file = self.target_out_dir.join(go_mod_name.clone() + ".thrift");
419 fs::copy(&self.config.idl_file, &self.idl_file).unwrap();
420 }
421 };
422 self.idl_include_dir = self.idl_file.parent().unwrap().to_path_buf();
423 }
424
425 pub(crate) fn rustc_link(&self) {
431 println!(
432 "cargo:rustc-link-search=native={}",
433 self.clib_gen_dir.to_str().unwrap()
434 );
435 println!(
436 "cargo:rustc-link-search=dependency={}",
437 self.clib_gen_dir.to_str().unwrap()
438 );
439 println!(
440 "cargo:rustc-link-lib={}={}",
441 self.rustc_link_kind_goffi, self.go_clib_name_base
442 );
443 }
444
445 pub(crate) fn rerun_if_changed(&self) {
446 println!("cargo:rerun-if-changed={}", self.pkg_dir.to_str().unwrap());
447 println!(
448 "cargo:rerun-if-changed={}",
449 self.target_out_dir.to_str().unwrap()
450 );
451 }
452
453 fn check_go_mod_path(&self) {
454 let f = &self.gomod_file;
455 if f.exists() {
456 if !f.is_file() {
457 exit_with_warning(
458 253,
459 format!("go mod file {} does not exist", f.to_str().unwrap()),
460 );
461 } else {
462 let p = &self.gomod_path;
463 let s = fs::read_to_string(f).unwrap();
464 if !s.contains(&format!("module {p}\n"))
465 && !s.contains(&format!("module {p}\t"))
466 && !s.contains(&format!("module {p}\r"))
467 && !s.contains(&format!("module {p} "))
468 {
469 exit_with_warning(
470 253,
471 format!("go mod path should be {p}, file={}", f.to_str().unwrap()),
472 );
473 }
474 }
475 }
476 }
477
478 fn init_files(&self) -> anyhow::Result<()> {
479 fs::create_dir_all(&self.go_main_dir)?;
480 fs::create_dir_all(&self.rust_mod_dir)?;
481 fs::create_dir_all(&self.clib_gen_dir)?;
482 for f in [
483 &self.rust_clib_file,
484 &self.rust_clib_header,
485 &self.go_clib_file,
486 &self.go_clib_header,
487 &self.fingerprint_path,
488 ] {
489 OpenOptions::new()
490 .write(true)
491 .create(true)
492 .open(&self.clib_gen_dir.join(f))?;
493 }
494 Ok(())
495 }
496
497 pub(crate) fn go_cmd_path(&self, cmd: &'static str) -> String {
498 if let Some(go_root_path) = &self.config.go_root_path {
499 go_root_path
500 .join("bin")
501 .join(cmd)
502 .to_str()
503 .unwrap()
504 .to_string()
505 } else if let Ok(go_root_path) = env::var("GOROOT") {
506 PathBuf::from_str(&go_root_path)
507 .unwrap()
508 .join("bin")
509 .join(cmd)
510 .to_str()
511 .unwrap()
512 .to_string()
513 } else {
514 cmd.to_string()
515 }
516 }
517}