• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.
2 
3 use std::env::VarError;
4 use std::io::prelude::*;
5 use std::io::BufReader;
6 use std::path::{Path, PathBuf};
7 use std::{env, fs, io};
8 
9 use cmake::Config as CmakeConfig;
10 use pkg_config::{Config as PkgConfig, Library};
11 use walkdir::WalkDir;
12 
13 const GRPC_VERSION: &str = "1.38.0";
14 
15 /// Following two arrays are generated by running pkg-config manually. We can
16 /// also choose to run pkg-config at build time, but it will requires pkg-config
17 /// in path, which is unfriendly for platforms like Windows.
18 // grpc_unsecure.pc is not accurate, see also grpc/grpc#24512. Should also include "address_sorting", "upb", "cares", "z".
19 #[rustfmt::skip]
20 const COMMON_DEPS: &[&str] = &[
21     "absl_bad_optional_access", "absl_bad_variant_access", "absl_base", "absl_city", "absl_civil_time",
22     "absl_cord", "absl_debugging_internal", "absl_demangle_internal", "absl_exponential_biased",
23     "absl_graphcycles_internal", "absl_hash", "absl_hashtablez_sampler", "absl_int128", "absl_log_severity",
24     "absl_malloc_internal", "absl_raw_hash_set", "absl_raw_logging_internal", "absl_spinlock_wait",
25     "absl_stacktrace", "absl_status", "absl_statusor", "absl_str_format_internal", "absl_strings",
26     "absl_strings_internal", "absl_symbolize", "absl_synchronization", "absl_throw_delegate", "absl_time",
27     "absl_time_zone", "absl_wyhash", "address_sorting", "cares", "gpr", "upb", "z",
28 ];
29 const GRPC_DEPS: &[&str] = &["grpc", "re2"];
30 const GRPC_UNSECURE_DEPS: &[&str] = &["grpc_unsecure"];
31 
probe_library(library: &str, cargo_metadata: bool) -> Library32 fn probe_library(library: &str, cargo_metadata: bool) -> Library {
33     match PkgConfig::new()
34         .atleast_version(GRPC_VERSION)
35         .cargo_metadata(cargo_metadata)
36         .probe(library)
37     {
38         Ok(lib) => lib,
39         Err(e) => panic!("can't find library {} via pkg-config: {:?}", library, e),
40     }
41 }
42 
prepare_grpc()43 fn prepare_grpc() {
44     let modules = vec![
45         "grpc",
46         "grpc/third_party/cares/cares",
47         "grpc/third_party/address_sorting",
48         "grpc/third_party/abseil-cpp",
49         "grpc/third_party/re2",
50     ];
51 
52     for module in modules {
53         if is_directory_empty(module).unwrap_or(true) {
54             panic!(
55                 "Can't find module {}. You need to run `git submodule \
56                  update --init --recursive` first to build the project.",
57                 module
58             );
59         }
60     }
61 }
62 
is_directory_empty<P: AsRef<Path>>(p: P) -> Result<bool, io::Error>63 fn is_directory_empty<P: AsRef<Path>>(p: P) -> Result<bool, io::Error> {
64     let mut entries = fs::read_dir(p)?;
65     Ok(entries.next().is_none())
66 }
67 
trim_start<'a>(s: &'a str, prefix: &str) -> Option<&'a str>68 fn trim_start<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
69     if s.starts_with(prefix) {
70         Some(s.trim_start_matches(prefix))
71     } else {
72         None
73     }
74 }
75 
76 /// If cache is stale, remove it to avoid compilation failure.
clean_up_stale_cache(cxx_compiler: String)77 fn clean_up_stale_cache(cxx_compiler: String) {
78     // We don't know the cmake output path before it's configured.
79     let build_dir = format!("{}/build", env::var("OUT_DIR").unwrap());
80     let path = format!("{}/CMakeCache.txt", build_dir);
81     let f = match std::fs::File::open(&path) {
82         Ok(f) => BufReader::new(f),
83         // It may be an empty directory.
84         Err(_) => return,
85     };
86     let cache_stale = f.lines().any(|l| {
87         let l = l.unwrap();
88         trim_start(&l, "CMAKE_CXX_COMPILER:").map_or(false, |s| {
89             let mut splits = s.splitn(2, '=');
90             splits.next();
91             splits.next().map_or(false, |p| p != cxx_compiler)
92         })
93     });
94     // CMake can't handle compiler change well, it will invalidate cache without respecting command
95     // line settings and result in configuration failure.
96     // See https://gitlab.kitware.com/cmake/cmake/-/issues/18959.
97     if cache_stale {
98         let _ = fs::remove_dir_all(&build_dir);
99     }
100 }
101 
build_grpc(cc: &mut cc::Build, library: &str)102 fn build_grpc(cc: &mut cc::Build, library: &str) {
103     prepare_grpc();
104 
105     let target = env::var("TARGET").unwrap();
106     let dst = {
107         let mut config = CmakeConfig::new("grpc");
108 
109         if get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "macos") {
110             config.cxxflag("-stdlib=libc++");
111         }
112 
113         // Ensure CoreFoundation be found in macos or ios
114         if get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "macos")
115             || get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "ios")
116         {
117             println!("cargo:rustc-link-lib=framework=CoreFoundation");
118         }
119 
120         let cxx_compiler = if let Some(val) = get_env("CXX") {
121             config.define("CMAKE_CXX_COMPILER", val.clone());
122             val
123         } else if env::var("CARGO_CFG_TARGET_ENV").unwrap() == "musl" {
124             config.define("CMAKE_CXX_COMPILER", "g++");
125             "g++".to_owned()
126         } else {
127             format!("{}", cc.get_compiler().path().display())
128         };
129         clean_up_stale_cache(cxx_compiler);
130 
131         // Cross-compile support for iOS
132         match target.as_str() {
133             "aarch64-apple-ios" => {
134                 config
135                     .define("CMAKE_OSX_SYSROOT", "iphoneos")
136                     .define("CMAKE_OSX_ARCHITECTURES", "arm64");
137             }
138             "armv7-apple-ios" => {
139                 config
140                     .define("CMAKE_OSX_SYSROOT", "iphoneos")
141                     .define("CMAKE_OSX_ARCHITECTURES", "armv7");
142             }
143             "armv7s-apple-ios" => {
144                 config
145                     .define("CMAKE_OSX_SYSROOT", "iphoneos")
146                     .define("CMAKE_OSX_ARCHITECTURES", "armv7s");
147             }
148             "i386-apple-ios" => {
149                 config
150                     .define("CMAKE_OSX_SYSROOT", "iphonesimulator")
151                     .define("CMAKE_OSX_ARCHITECTURES", "i386");
152             }
153             "x86_64-apple-ios" => {
154                 config
155                     .define("CMAKE_OSX_SYSROOT", "iphonesimulator")
156                     .define("CMAKE_OSX_ARCHITECTURES", "x86_64");
157             }
158             _ => {}
159         };
160 
161         // Allow overriding of the target passed to cmake
162         // (needed for Android crosscompile)
163         if let Ok(val) = env::var("CMAKE_TARGET_OVERRIDE") {
164             config.target(&val);
165         }
166 
167         // We don't need to generate install targets.
168         config.define("gRPC_INSTALL", "false");
169         // We don't need to build csharp target.
170         config.define("gRPC_BUILD_CSHARP_EXT", "false");
171         // We don't need to build codegen target.
172         config.define("gRPC_BUILD_CODEGEN", "false");
173         // We don't need to build benchmarks.
174         config.define("gRPC_BENCHMARK_PROVIDER", "none");
175 
176         // `package` should only be set for secure feature, otherwise cmake will always search for
177         // ssl library.
178         if cfg!(feature = "secure") {
179             config.define("gRPC_SSL_PROVIDER", "package");
180         }
181         #[cfg(feature = "secure")]
182         if cfg!(feature = "openssl") {
183             if cfg!(feature = "openssl-vendored") {
184                 config.register_dep("openssl");
185             }
186         } else {
187             build_boringssl(&mut config);
188         }
189         if cfg!(feature = "no-omit-frame-pointer") {
190             config
191                 .cflag("-fno-omit-frame-pointer")
192                 .cxxflag("-fno-omit-frame-pointer");
193         }
194         // Uses zlib from libz-sys.
195         setup_libz(&mut config);
196         config.build_target(library).uses_cxx11().build()
197     };
198 
199     let lib_suffix = if target.contains("msvc") {
200         ".lib"
201     } else {
202         ".a"
203     };
204     let build_dir = format!("{}/build", dst.display());
205     for e in WalkDir::new(&build_dir) {
206         let e = e.unwrap();
207         if e.file_name().to_string_lossy().ends_with(lib_suffix) {
208             println!(
209                 "cargo:rustc-link-search=native={}",
210                 e.path().parent().unwrap().display()
211             );
212         }
213     }
214 
215     let libs = if library.contains("unsecure") {
216         GRPC_UNSECURE_DEPS
217     } else {
218         GRPC_DEPS
219     };
220     for l in COMMON_DEPS.iter().chain(libs) {
221         println!("cargo:rustc-link-lib=static={}", l);
222     }
223 
224     if cfg!(feature = "secure") {
225         if cfg!(feature = "openssl") && !cfg!(feature = "openssl-vendored") {
226             figure_ssl_path(&build_dir);
227         } else {
228             println!("cargo:rustc-link-lib=static=ssl");
229             println!("cargo:rustc-link-lib=static=crypto");
230         }
231     }
232 
233     cc.include("grpc/include");
234 }
235 
figure_ssl_path(build_dir: &str)236 fn figure_ssl_path(build_dir: &str) {
237     let path = format!("{}/CMakeCache.txt", build_dir);
238     let f = BufReader::new(std::fs::File::open(&path).unwrap());
239     let mut cnt = 0;
240     for l in f.lines() {
241         let l = l.unwrap();
242         let t = trim_start(&l, "OPENSSL_CRYPTO_LIBRARY:FILEPATH=")
243             .or_else(|| trim_start(&l, "OPENSSL_SSL_LIBRARY:FILEPATH="));
244         if let Some(s) = t {
245             let path = Path::new(s);
246             println!(
247                 "cargo:rustc-link-search=native={}",
248                 path.parent().unwrap().display()
249             );
250             cnt += 1;
251         }
252     }
253     if cnt != 2 {
254         panic!(
255             "CMake cache invalid, file {} contains {} ssl keys!",
256             path, cnt
257         );
258     }
259     println!("cargo:rustc-link-lib=ssl");
260     println!("cargo:rustc-link-lib=crypto");
261 }
262 
263 #[cfg(feature = "secure")]
build_boringssl(config: &mut CmakeConfig)264 fn build_boringssl(config: &mut CmakeConfig) {
265     let boringssl_artifact = boringssl_src::Build::new().build();
266     config.define(
267         "OPENSSL_ROOT_DIR",
268         format!("{}", boringssl_artifact.root_dir().display()),
269     );
270     // To avoid linking system library, set lib path explicitly.
271     println!(
272         "cargo:rustc-link-search=native={}",
273         boringssl_artifact.lib_dir().display()
274     );
275 }
276 
setup_libz(config: &mut CmakeConfig)277 fn setup_libz(config: &mut CmakeConfig) {
278     config.define("gRPC_ZLIB_PROVIDER", "package");
279     config.register_dep("Z");
280     // cmake script expect libz.a being under ${DEP_Z_ROOT}/lib, but libz-sys crate put it
281     // under ${DEP_Z_ROOT}/build. Append the path to CMAKE_PREFIX_PATH to get around it.
282     let zlib_root = env::var("DEP_Z_ROOT").unwrap();
283     let prefix_path = if let Ok(prefix_path) = env::var("CMAKE_PREFIX_PATH") {
284         format!("{};{}/build", prefix_path, zlib_root)
285     } else {
286         format!("{}/build", zlib_root)
287     };
288     // To avoid linking system library, set lib path explicitly.
289     println!("cargo:rustc-link-search=native={}/build", zlib_root);
290     println!("cargo:rustc-link-search=native={}/lib", zlib_root);
291     env::set_var("CMAKE_PREFIX_PATH", prefix_path);
292 }
293 
get_env(name: &str) -> Option<String>294 fn get_env(name: &str) -> Option<String> {
295     println!("cargo:rerun-if-env-changed={}", name);
296     match env::var(name) {
297         Ok(s) => Some(s),
298         Err(VarError::NotPresent) => None,
299         Err(VarError::NotUnicode(s)) => {
300             panic!("unrecognize env var of {}: {:?}", name, s.to_string_lossy());
301         }
302     }
303 }
304 
305 // Generate the bindings to grpc C-core.
306 // Try to disable the generation of platform-related bindings.
307 #[cfg(feature = "use-bindgen")]
bindgen_grpc(file_path: &Path)308 fn bindgen_grpc(file_path: &Path) {
309     // create a config to generate binding file
310     let mut config = bindgen::Builder::default();
311     if cfg!(feature = "secure") {
312         config = config.clang_arg("-DGRPC_SYS_SECURE");
313     }
314 
315     if get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "windows") {
316         config = config.clang_arg("-D _WIN32_WINNT=0x600");
317     }
318 
319     // Search header files with API interface
320     let mut headers = Vec::new();
321     for result in WalkDir::new(Path::new("./grpc/include")) {
322         let dent = result.expect("Error happened when search headers");
323         if !dent.file_type().is_file() {
324             continue;
325         }
326         let mut file = fs::File::open(dent.path()).expect("couldn't open headers");
327         let mut buf = String::new();
328         file.read_to_string(&mut buf)
329             .expect("Coundn't read header content");
330         if buf.contains("GRPCAPI") || buf.contains("GPRAPI") {
331             headers.push(String::from(dent.path().to_str().unwrap()));
332         }
333     }
334 
335     // To control the order of bindings
336     headers.sort();
337     for path in headers {
338         config = config.header(path);
339     }
340 
341     println!("cargo:rerun-if-env-changed=TEST_BIND");
342     let gen_tests = env::var("TEST_BIND").map_or(false, |s| s == "1");
343 
344     let cfg = config
345         .header("grpc_wrap.cc")
346         .clang_arg("-xc++")
347         .clang_arg("-I./grpc/include")
348         .clang_arg("-std=c++11")
349         .rustfmt_bindings(true)
350         .impl_debug(true)
351         .size_t_is_usize(true)
352         .disable_header_comment()
353         .whitelist_function(r"\bgrpc_.*")
354         .whitelist_function(r"\bgpr_.*")
355         .whitelist_function(r"\bgrpcwrap_.*")
356         .whitelist_var(r"\bGRPC_.*")
357         .whitelist_type(r"\bgrpc_.*")
358         .whitelist_type(r"\bgpr_.*")
359         .whitelist_type(r"\bgrpcwrap_.*")
360         .whitelist_type(r"\bcensus_context.*")
361         .whitelist_type(r"\bverify_peer_options.*")
362         .blacklist_type(r"(__)?pthread.*")
363         .blacklist_function(r"\bgpr_mu_.*")
364         .blacklist_function(r"\bgpr_cv_.*")
365         .blacklist_function(r"\bgpr_once_.*")
366         .blacklist_type(r"gpr_mu")
367         .blacklist_type(r"gpr_cv")
368         .blacklist_type(r"gpr_once")
369         .constified_enum_module(r"grpc_status_code")
370         .layout_tests(gen_tests)
371         .default_enum_style(bindgen::EnumVariation::Rust {
372             non_exhaustive: false,
373         });
374     println!("running {}", cfg.command_line_flags().join(" "));
375     cfg.generate()
376         .expect("Unable to generate grpc bindings")
377         .write_to_file(file_path)
378         .expect("Couldn't write bindings!");
379 }
380 
381 // Determine if need to update bindings. Supported platforms do not
382 // need to be updated by default unless the UPDATE_BIND is specified.
383 // Other platforms use bindgen to generate the bindings every time.
config_binding_path()384 fn config_binding_path() {
385     let target = env::var("TARGET").unwrap();
386     let file_path: PathBuf = match target.as_str() {
387         "x86_64-unknown-linux-gnu" | "aarch64-unknown-linux-gnu" => {
388             // Cargo treats nonexistent files changed, so we only emit the rerun-if-changed
389             // directive when we expect the target-specific pre-generated binding file to be
390             // present.
391             println!("cargo:rerun-if-changed=bindings/{}-bindings.rs", &target);
392 
393             let file_path = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap())
394                 .join("bindings")
395                 .join(format!("{}-bindings.rs", &target));
396 
397             #[cfg(feature = "use-bindgen")]
398             if env::var("UPDATE_BIND").is_ok() {
399                 bindgen_grpc(&file_path);
400             }
401 
402             file_path
403         }
404         _ => {
405             let file_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("grpc-bindings.rs");
406 
407             #[cfg(feature = "use-bindgen")]
408             bindgen_grpc(&file_path);
409 
410             file_path
411         }
412     };
413 
414     println!(
415         "cargo:rustc-env=BINDING_PATH={}",
416         file_path.to_str().unwrap()
417     );
418 }
419 
main()420 fn main() {
421     println!("cargo:rerun-if-changed=grpc_wrap.cc");
422     println!("cargo:rerun-if-changed=grpc");
423     println!("cargo:rerun-if-env-changed=UPDATE_BIND");
424 
425     // create a builder to compile grpc_wrap.cc
426     let mut cc = cc::Build::new();
427 
428     let library = if cfg!(feature = "secure") {
429         cc.define("GRPC_SYS_SECURE", None);
430         "grpc"
431     } else {
432         "grpc_unsecure"
433     };
434 
435     if get_env("CARGO_CFG_TARGET_OS").map_or(false, |s| s == "windows") {
436         // At lease vista
437         cc.define("_WIN32_WINNT", Some("0x600"));
438     }
439 
440     if get_env("GRPCIO_SYS_USE_PKG_CONFIG").map_or(false, |s| s == "1") {
441         // Print cargo metadata.
442         let lib_core = probe_library(library, true);
443         for inc_path in lib_core.include_paths {
444             cc.include(inc_path);
445         }
446     } else {
447         build_grpc(&mut cc, library);
448     }
449 
450     cc.cpp(true);
451     if !cfg!(target_env = "msvc") {
452         cc.flag("-std=c++11");
453     }
454     cc.file("grpc_wrap.cc");
455     cc.warnings_into_errors(true);
456     cc.compile("libgrpc_wrap.a");
457 
458     config_binding_path();
459 }
460