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