1  // Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.
2  
3  use std::collections::HashSet;
4  use std::env::VarError;
5  use std::fs::File;
6  use std::io::prelude::*;
7  use std::io::BufReader;
8  use std::path::{Path, PathBuf};
9  use std::{env, fs, io};
10  
11  use cmake::Config as CmakeConfig;
12  use pkg_config::{Config as PkgConfig, Library};
13  use walkdir::WalkDir;
14  
grpc_version() -> &'static str15  fn grpc_version() -> &'static str {
16      let mut version = env!("CARGO_PKG_VERSION").split('+');
17      version.next().unwrap();
18      let label = version.next().unwrap();
19      label.split('-').next().unwrap()
20  }
21  
22  include!("link-deps.rs");
23  
probe_library(library: &str, cargo_metadata: bool) -> Library24  fn probe_library(library: &str, cargo_metadata: bool) -> Library {
25      match PkgConfig::new()
26          .atleast_version(grpc_version())
27          .cargo_metadata(cargo_metadata)
28          .probe(library)
29      {
30          Ok(lib) => lib,
31          Err(e) => panic!("can't find library {} via pkg-config: {:?}", library, e),
32      }
33  }
34  
prepare_grpc()35  fn prepare_grpc() {
36      let modules = vec![
37          "grpc",
38          "grpc/third_party/cares/cares",
39          "grpc/third_party/address_sorting",
40          "grpc/third_party/abseil-cpp",
41          "grpc/third_party/re2",
42      ];
43  
44      for module in modules {
45          if is_directory_empty(module).unwrap_or(true) {
46              panic!(
47                  "Can't find module {}. You need to run `git submodule \
48                   update --init --recursive` first to build the project.",
49                  module
50              );
51          }
52      }
53  }
54  
is_directory_empty<P: AsRef<Path>>(p: P) -> Result<bool, io::Error>55  fn is_directory_empty<P: AsRef<Path>>(p: P) -> Result<bool, io::Error> {
56      let mut entries = fs::read_dir(p)?;
57      Ok(entries.next().is_none())
58  }
59  
trim_start<'a>(s: &'a str, prefix: &str) -> Option<&'a str>60  fn trim_start<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
61      if s.starts_with(prefix) {
62          Some(s.trim_start_matches(prefix))
63      } else {
64          None
65      }
66  }
67  
68  /// If cache is stale, remove it to avoid compilation failure.
clean_up_stale_cache(cxx_compiler: String)69  fn clean_up_stale_cache(cxx_compiler: String) {
70      // We don't know the cmake output path before it's configured.
71      let build_dir = format!("{}/build", env::var("OUT_DIR").unwrap());
72      let path = format!("{build_dir}/CMakeCache.txt");
73      let f = match std::fs::File::open(path) {
74          Ok(f) => BufReader::new(f),
75          // It may be an empty directory.
76          Err(_) => return,
77      };
78      let cache_stale = f.lines().any(|l| {
79          let l = l.unwrap();
80          trim_start(&l, "CMAKE_CXX_COMPILER:").map_or(false, |s| {
81              let mut splits = s.splitn(2, '=');
82              splits.next();
83              splits.next().map_or(false, |p| p != cxx_compiler)
84          })
85      });
86      // CMake can't handle compiler change well, it will invalidate cache without respecting command
87      // line settings and result in configuration failure.
88      // See https://gitlab.kitware.com/cmake/cmake/-/issues/18959.
89      if cache_stale {
90          let _ = fs::remove_dir_all(&build_dir);
91      }
92  }
93  
94  /// List packages needed for linking in working directory.
list_packages(dst: &Path)95  fn list_packages(dst: &Path) {
96      env::set_var(
97          "PKG_CONFIG_PATH",
98          format!("{}/lib/pkgconfig", dst.display()),
99      );
100      let mut cfg = PkgConfig::new();
101      cfg.print_system_cflags(false)
102          .print_system_libs(false)
103          .env_metadata(false)
104          .cargo_metadata(false)
105          .atleast_version(grpc_version());
106      let grpc = cfg.probe("grpc").unwrap();
107      let mut grpc_libs: HashSet<_> = grpc.libs.iter().cloned().collect();
108      let grpc_unsecure = cfg.probe("grpc_unsecure").unwrap();
109      let mut grpc_unsecure_libs: HashSet<_> = grpc_unsecure.libs.iter().cloned().collect();
110  
111      // grpc_unsecure.pc is not accurate, see also grpc/grpc#24512. Should also include "address_sorting", "upb", "cares", "z".
112      const EXTRA_LIBS: [&str; 5] = ["address_sorting", "upb", "cares", "r2", "z"];
113      grpc_unsecure_libs.extend(EXTRA_LIBS.iter().map(ToString::to_string));
114      grpc_libs.extend(EXTRA_LIBS.iter().map(ToString::to_string));
115      // There is no "rt" on Windows and MacOS.
116      grpc_libs.remove("rt");
117      grpc_unsecure_libs.remove("rt");
118  
119      // ssl, crypto is managed by us according to different features.
120      grpc_libs.remove("ssl");
121      grpc_libs.remove("crypto");
122  
123      let mut common_libs: Vec<_> = grpc_libs.intersection(&grpc_unsecure_libs).collect();
124      let mut secure_only: Vec<_> = grpc_libs.difference(&grpc_unsecure_libs).collect();
125      let mut unsecure_only: Vec<_> = grpc_unsecure_libs.difference(&grpc_libs).collect();
126  
127      common_libs.sort();
128      secure_only.sort();
129      unsecure_only.sort();
130  
131      let outputs = &[
132          ("COMMON_DEPS", common_libs),
133          ("GRPC_DEPS", secure_only),
134          ("GRPC_UNSECURE_DEPS", unsecure_only),
135      ];
136  
137      let mut f = File::create("link-deps.rs").unwrap();
138      f.write_all(
139          b"/// Following two arrays are generated by running pkg-config manually. We can
140  /// also choose to run pkg-config at build time, but it will requires pkg-config
141  /// in path, which is unfriendly for platforms like Windows.
142  ",
143      )
144      .unwrap();
145      for (name, libs) in outputs {
146          writeln!(f, "const {name}: &[&str] = &[").unwrap();
147          for lib in libs {
148              writeln!(f, "\"{lib}\",").unwrap();
149          }
150          writeln!(f, "];").unwrap();
151      }
152  }
153  
build_grpc(cc: &mut cc::Build, library: &str)154  fn build_grpc(cc: &mut cc::Build, library: &str) {
155      prepare_grpc();
156  
157      let target = env::var("TARGET").unwrap();
158      let dst = {
159          let mut config = CmakeConfig::new("grpc");
160  
161          if get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "macos") {
162              config.cxxflag("-stdlib=libc++");
163              println!("cargo:rustc-link-lib=resolv");
164          }
165  
166          // Ensure CoreFoundation be found in macos or ios
167          if get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "macos")
168              || get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "ios")
169          {
170              println!("cargo:rustc-link-lib=framework=CoreFoundation");
171          }
172  
173          let cxx_compiler = if let Some(val) = get_env("CXX") {
174              config.define("CMAKE_CXX_COMPILER", val.clone());
175              val
176          } else if env::var("CARGO_CFG_TARGET_ENV").unwrap() == "musl" {
177              config.define("CMAKE_CXX_COMPILER", "g++");
178              "g++".to_owned()
179          } else {
180              format!("{}", cc.get_compiler().path().display())
181          };
182          clean_up_stale_cache(cxx_compiler);
183  
184          // Cross-compile support for iOS
185          match target.as_str() {
186              "aarch64-apple-ios" => {
187                  config
188                      .define("CMAKE_OSX_SYSROOT", "iphoneos")
189                      .define("CMAKE_OSX_ARCHITECTURES", "arm64");
190              }
191              "armv7-apple-ios" => {
192                  config
193                      .define("CMAKE_OSX_SYSROOT", "iphoneos")
194                      .define("CMAKE_OSX_ARCHITECTURES", "armv7");
195              }
196              "armv7s-apple-ios" => {
197                  config
198                      .define("CMAKE_OSX_SYSROOT", "iphoneos")
199                      .define("CMAKE_OSX_ARCHITECTURES", "armv7s");
200              }
201              "i386-apple-ios" => {
202                  config
203                      .define("CMAKE_OSX_SYSROOT", "iphonesimulator")
204                      .define("CMAKE_OSX_ARCHITECTURES", "i386");
205              }
206              "x86_64-apple-ios" => {
207                  config
208                      .define("CMAKE_OSX_SYSROOT", "iphonesimulator")
209                      .define("CMAKE_OSX_ARCHITECTURES", "x86_64");
210              }
211              _ => {}
212          };
213  
214          // Allow overriding of the target passed to cmake
215          // (needed for Android crosscompile)
216          if let Ok(val) = env::var("CMAKE_TARGET_OVERRIDE") {
217              config.target(&val);
218          }
219  
220          // We don't need to generate install targets.
221          config.define("gRPC_INSTALL", cfg!(feature = "_list-package").to_string());
222          // We don't need to build csharp target.
223          config.define("gRPC_BUILD_CSHARP_EXT", "false");
224          // We don't need to build codegen target.
225          config.define("gRPC_BUILD_CODEGEN", "false");
226          // We don't need to build benchmarks.
227          config.define("gRPC_BENCHMARK_PROVIDER", "none");
228          // Check https://github.com/protocolbuffers/protobuf/issues/12185
229          config.define("ABSL_ENABLE_INSTALL", "ON");
230  
231          // `package` should only be set for secure feature, otherwise cmake will always search for
232          // ssl library.
233          if cfg!(feature = "_secure") {
234              config.define("gRPC_SSL_PROVIDER", "package");
235          }
236          #[cfg(feature = "_secure")]
237          if cfg!(feature = "openssl") {
238              if cfg!(feature = "openssl-vendored") {
239                  config.register_dep("openssl");
240              }
241          } else {
242              #[cfg(feature = "boringssl")]
243              build_boringssl(&mut config);
244          }
245          if cfg!(feature = "no-omit-frame-pointer") {
246              config
247                  .cflag("-fno-omit-frame-pointer")
248                  .cxxflag("-fno-omit-frame-pointer");
249          }
250          // Uses zlib from libz-sys.
251          setup_libz(&mut config);
252          if !cfg!(feature = "_list-package") {
253              config.build_target(library);
254          }
255          config.uses_cxx11().build()
256      };
257  
258      let lib_suffix = if target.contains("msvc") {
259          ".lib"
260      } else {
261          ".a"
262      };
263      let build_dir = format!("{}/build", dst.display());
264      for e in WalkDir::new(&build_dir) {
265          let e = e.unwrap();
266          if e.file_name().to_string_lossy().ends_with(lib_suffix) {
267              println!(
268                  "cargo:rustc-link-search=native={}",
269                  e.path().parent().unwrap().display()
270              );
271          }
272      }
273  
274      if cfg!(feature = "_list-package") {
275          list_packages(&dst);
276      }
277  
278      let libs = if library.contains("unsecure") {
279          GRPC_UNSECURE_DEPS
280      } else {
281          GRPC_DEPS
282      };
283      for l in COMMON_DEPS.iter().chain(libs) {
284          println!("cargo:rustc-link-lib=static={l}");
285      }
286  
287      if cfg!(feature = "_secure") {
288          if cfg!(feature = "openssl") && !cfg!(feature = "openssl-vendored") {
289              figure_ssl_path(&build_dir);
290          } else {
291              println!("cargo:rustc-link-lib=static=ssl");
292              println!("cargo:rustc-link-lib=static=crypto");
293          }
294      }
295  
296      figure_systemd_path(&build_dir);
297  
298      cc.include("grpc/include");
299  }
300  
figure_systemd_path(build_dir: &str)301  fn figure_systemd_path(build_dir: &str) {
302      let path = format!("{build_dir}/CMakeCache.txt");
303      let f = BufReader::new(std::fs::File::open(&path).unwrap());
304      let mut libdir: Option<String> = None;
305      let mut libname: Option<String> = None;
306      for l in f.lines() {
307          let l = l.unwrap();
308          if let Some(s) = trim_start(&l, "SYSTEMD_LIBDIR:INTERNAL=").filter(|s| !s.is_empty()) {
309              libdir = Some(s.to_owned());
310              if libname.is_some() {
311                  break;
312              }
313          } else if let Some(s) =
314              trim_start(&l, "SYSTEMD_LIBRARIES:INTERNAL=").filter(|s| !s.is_empty())
315          {
316              libname = Some(s.to_owned());
317              if libdir.is_some() {
318                  break;
319              }
320          }
321      }
322      if let (Some(libdir), Some(libname)) = (libdir, libname) {
323          println!("cargo:rustc-link-search=native={}", libdir);
324          println!("cargo:rustc-link-lib={}", libname);
325      }
326  }
327  
figure_ssl_path(build_dir: &str)328  fn figure_ssl_path(build_dir: &str) {
329      let path = format!("{build_dir}/CMakeCache.txt");
330      let f = BufReader::new(std::fs::File::open(&path).unwrap());
331      let mut cnt = 0;
332      for l in f.lines() {
333          let l = l.unwrap();
334          let t = trim_start(&l, "OPENSSL_CRYPTO_LIBRARY:FILEPATH=")
335              .or_else(|| trim_start(&l, "OPENSSL_SSL_LIBRARY:FILEPATH="));
336          if let Some(s) = t {
337              let path = Path::new(s);
338              println!(
339                  "cargo:rustc-link-search=native={}",
340                  path.parent().unwrap().display()
341              );
342              cnt += 1;
343          }
344      }
345      if cnt != 2 {
346          panic!(
347              "CMake cache invalid, file {} contains {} ssl keys!",
348              path, cnt
349          );
350      }
351      println!("cargo:rustc-link-lib=ssl");
352      println!("cargo:rustc-link-lib=crypto");
353  }
354  
355  #[cfg(feature = "boringssl")]
build_boringssl(config: &mut CmakeConfig)356  fn build_boringssl(config: &mut CmakeConfig) {
357      let boringssl_artifact = boringssl_src::Build::new().build();
358      config.define(
359          "OPENSSL_ROOT_DIR",
360          format!("{}", boringssl_artifact.root_dir().display()),
361      );
362      // To avoid linking system library, set lib path explicitly.
363      println!(
364          "cargo:rustc-link-search=native={}",
365          boringssl_artifact.lib_dir().display()
366      );
367  }
368  
setup_libz(config: &mut CmakeConfig)369  fn setup_libz(config: &mut CmakeConfig) {
370      config.define("gRPC_ZLIB_PROVIDER", "package");
371      config.register_dep("Z");
372      // cmake script expect libz.a being under ${DEP_Z_ROOT}/lib, but libz-sys crate put it
373      // under ${DEP_Z_ROOT}/build. Append the path to CMAKE_PREFIX_PATH to get around it.
374      let zlib_root = env::var("DEP_Z_ROOT").unwrap();
375      let prefix_path = if let Ok(prefix_path) = env::var("CMAKE_PREFIX_PATH") {
376          format!("{prefix_path};{zlib_root}/build")
377      } else {
378          format!("{zlib_root}/build")
379      };
380      // To avoid linking system library, set lib path explicitly.
381      println!("cargo:rustc-link-search=native={zlib_root}/build");
382      println!("cargo:rustc-link-search=native={zlib_root}/lib");
383      env::set_var("CMAKE_PREFIX_PATH", prefix_path);
384  }
385  
get_env(name: &str) -> Option<String>386  fn get_env(name: &str) -> Option<String> {
387      println!("cargo:rerun-if-env-changed={name}");
388      match env::var(name) {
389          Ok(s) => Some(s),
390          Err(VarError::NotPresent) => None,
391          Err(VarError::NotUnicode(s)) => {
392              panic!("unrecognize env var of {name}: {:?}", s.to_string_lossy());
393          }
394      }
395  }
396  
397  // Generate the bindings to grpc C-core.
398  // Try to disable the generation of platform-related bindings.
399  #[cfg(any(
400      feature = "_gen-bindings",
401      not(all(
402          any(target_os = "linux", target_os = "macos"),
403          any(target_arch = "x86_64", target_arch = "aarch64")
404      ))
405  ))]
bindgen_grpc(file_path: &Path)406  fn bindgen_grpc(file_path: &Path) {
407      // create a config to generate binding file
408      let mut config = bindgen::Builder::default();
409      if cfg!(feature = "_secure") {
410          config = config.clang_arg("-DGRPC_SYS_SECURE");
411      }
412  
413      if get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "windows") {
414          config = config.clang_arg("-D _WIN32_WINNT=0x600");
415      }
416  
417      // Search header files with API interface
418      let mut headers = Vec::new();
419      for result in WalkDir::new(Path::new("./grpc/include")) {
420          let dent = result.expect("Error happened when search headers");
421          if !dent.file_type().is_file() {
422              continue;
423          }
424          let mut file = fs::File::open(dent.path()).expect("couldn't open headers");
425          let mut buf = String::new();
426          file.read_to_string(&mut buf)
427              .expect("Coundn't read header content");
428          if buf.contains("GRPCAPI") || buf.contains("GPRAPI") {
429              headers.push(String::from(dent.path().to_str().unwrap()));
430          }
431      }
432  
433      // To control the order of bindings
434      headers.sort();
435      for path in headers {
436          config = config.header(path);
437      }
438  
439      println!("cargo:rerun-if-env-changed=TEST_BIND");
440      let gen_tests = env::var("TEST_BIND").map_or(false, |s| s == "1");
441  
442      let cfg = config
443          .header("grpc_wrap.cc")
444          .clang_arg("-xc++")
445          .clang_arg("-I./grpc/include")
446          .clang_arg("-std=c++11")
447          .rustfmt_bindings(true)
448          .impl_debug(true)
449          .size_t_is_usize(true)
450          .disable_header_comment()
451          .allowlist_function(r"\bgrpc_.*")
452          .allowlist_function(r"\bgpr_.*")
453          .allowlist_function(r"\bgrpcwrap_.*")
454          .allowlist_var(r"\bGRPC_.*")
455          .allowlist_type(r"\bgrpc_.*")
456          .allowlist_type(r"\bgpr_.*")
457          .allowlist_type(r"\bgrpcwrap_.*")
458          .allowlist_type(r"\bcensus_context.*")
459          .allowlist_type(r"\bverify_peer_options.*")
460          // Block all system headers.
461          .blocklist_file(r"^/.*")
462          .blocklist_function(r"\bgpr_mu_.*")
463          .blocklist_function(r"\bgpr_cv_.*")
464          .blocklist_function(r"\bgpr_once_.*")
465          .blocklist_type(r"gpr_mu")
466          .blocklist_type(r"gpr_cv")
467          .blocklist_type(r"gpr_once")
468          .constified_enum_module(r"grpc_status_code")
469          .layout_tests(gen_tests)
470          .default_enum_style(bindgen::EnumVariation::Rust {
471              non_exhaustive: false,
472          });
473      println!("running {}", cfg.command_line_flags().join(" "));
474      cfg.generate()
475          .expect("Unable to generate grpc bindings")
476          .write_to_file(file_path)
477          .expect("Couldn't write bindings!");
478  }
479  
480  // Determine if need to update bindings. Supported platforms do not
481  // need to be updated by default unless the _gen-bindings feature is specified.
482  // Other platforms use bindgen to generate the bindings every time.
config_binding_path()483  fn config_binding_path() {
484      let target = env::var("TARGET").unwrap();
485      let file_path: PathBuf = match target.as_str() {
486          "x86_64-unknown-linux-gnu"
487          | "x86_64-unknown-linux-musl"
488          | "aarch64-unknown-linux-musl"
489          | "aarch64-unknown-linux-gnu"
490          | "x86_64-apple-darwin"
491          | "aarch64-apple-darwin" => {
492              // Cargo treats nonexistent files changed, so we only emit the rerun-if-changed
493              // directive when we expect the target-specific pre-generated binding file to be
494              // present.
495              println!("cargo:rerun-if-changed=bindings/bindings.rs");
496  
497              PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
498                  .join("bindings")
499                  .join("bindings.rs")
500          }
501          _ => PathBuf::from(env::var("OUT_DIR").unwrap()).join("grpc-bindings.rs"),
502      };
503  
504      #[cfg(any(
505          feature = "_gen-bindings",
506          not(all(
507              any(target_os = "linux", target_os = "macos"),
508              any(target_arch = "x86_64", target_arch = "aarch64")
509          ))
510      ))]
511      {
512          // On some system (like Windows), stack size of main thread may
513          // be too small.
514          let f = file_path.clone();
515          std::thread::Builder::new()
516              .stack_size(8 * 1024 * 1024)
517              .name("bindgen_grpc".to_string())
518              .spawn(move || bindgen_grpc(&f))
519              .unwrap()
520              .join()
521              .unwrap();
522      }
523  
524      println!(
525          "cargo:rustc-env=BINDING_PATH={}",
526          file_path.to_str().unwrap()
527      );
528  }
529  
main()530  fn main() {
531      println!("cargo:rerun-if-changed=grpc_wrap.cc");
532      println!("cargo:rerun-if-changed=grpc");
533  
534      // create a builder to compile grpc_wrap.cc
535      let mut cc = cc::Build::new();
536  
537      let library = if cfg!(feature = "_secure") {
538          cc.define("GRPC_SYS_SECURE", None);
539          "grpc"
540      } else {
541          "grpc_unsecure"
542      };
543  
544      if get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "windows") {
545          // At lease vista
546          cc.define("_WIN32_WINNT", Some("0x600"));
547      }
548  
549      if get_env("GRPCIO_SYS_USE_PKG_CONFIG").map_or(false, |s| s == "1") {
550          // Print cargo metadata.
551          let lib_core = probe_library(library, true);
552          for inc_path in lib_core.include_paths {
553              cc.include(inc_path);
554          }
555      } else {
556          build_grpc(&mut cc, library);
557      }
558  
559      cc.cpp(true);
560      if !cfg!(target_env = "msvc") {
561          cc.flag("-std=c++11");
562      }
563      cc.file("grpc_wrap.cc");
564      cc.warnings_into_errors(true);
565      cc.compile("libgrpc_wrap.a");
566  
567      config_binding_path();
568  }
569