fcplug_build/
config.rs

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    /// Target crate directory for code generation
20    pub target_crate_dir: Option<PathBuf>,
21    /// go command dir, default to find from $GOROOT > $PATH
22    pub go_root_path: Option<PathBuf>,
23    pub go_mod_parent: &'static str,
24    /// If use_goffi_cdylib is true, go will be compiled into a c dynamic library.
25    pub use_goffi_cdylib: bool,
26    /// If add_clib_to_git is true, the c lib files will be automatically added to the git version management list.
27    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/// unit-like struct path, e.g. `::mycrate::Abc`
43#[derive(Debug, Clone)]
44pub struct UnitLikeStructPath(pub &'static str);
45
46#[derive(Debug, Clone)]
47pub struct GoObjectPath {
48    /// e.g. `github.com/xxx/mypkg`
49    pub import: String,
50    /// e.g. `mypkg.Abc`
51    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    // rustc-link-lib=[KIND=]NAME indicates that the specified value is a library name and should be passed to the compiler as a -l flag. The optional KIND can be one of static, dylib (the default), or framework, see rustc --help for more details.
426    //
427    // rustc-link-search=[KIND=]PATH indicates the specified value is a library search path and should be passed to the compiler as a -L flag. The optional KIND can be one of dependency, crate, native, framework or all (the default), see rustc --help for more details.
428    //
429    // rustc-flags=FLAGS is a set of flags passed to the compiler, only -l and -L flags are supported.
430    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}