find_folly/
lib.rs

1// find-folly/src/lib.rs
2//
3//! This crate is a simple build dependency you can use in your `build.rs` scripts to compile and
4//! link against the [Folly C++ library](https://github.com/facebook/folly).
5//! 
6//! In theory, the [`pkg-config`](https://crates.io/crates/pkg-config) library would be all you
7//! need in order to locate Folly, because Folly is typically packed with a `.pc` file. In
8//! practice, that is insufficient, because the `.pc` file doesn't fully describe all the
9//! dependencies that Folly has, and it has bugs. This crate knows about these idiosyncrasies and
10//! provides workarounds for them.
11//! 
12//! The following snippet should suffice for most use cases:
13//!
14//! ```ignore
15//! let folly = find_folly::probe_folly().unwrap();
16//! let mut build = cc::Build::new();
17//! ... populate `build` ...
18//! build.includes(&folly.include_paths);
19//! for other_cflag in &folly.other_cflags {
20//!     build.flag(other_cflag);
21//! }
22//! ```
23
24use pkg_config::{Config, Error as PkgConfigError};
25use shlex::Shlex;
26use std::io::Error as IoError;
27use std::path::{Path, PathBuf};
28use std::process::Command;
29use std::str::FromStr;
30use thiserror::Error;
31
32/// Information about the Folly library.
33///
34/// You can the information in this structure to populate a `cc::Build` in order to compile code
35/// that uses Folly:
36///
37///     let folly = find_folly::probe_folly().unwrap();
38///     let mut build = cc::Build::new();
39///     ... populate `build` ...
40///     build.includes(&folly.include_paths);
41///     for other_cflag in &folly.other_cflags {
42///         build.flag(other_cflag);
43///     }
44pub struct Folly {
45    pub lib_dirs: Vec<PathBuf>,
46    pub include_paths: Vec<PathBuf>,
47    pub other_cflags: Vec<String>,
48    _priv: (),
49}
50
51#[derive(Error, Debug)]
52pub enum FollyError {
53    #[error("`fmt` dependency couldn't be located")]
54    FmtDependency(PkgConfigError),
55    #[error("`gflags` dependency couldn't be located")]
56    GflagsDependency(PkgConfigError),
57    #[error("main `folly` package couldn't be located")]
58    MainPackage(IoError),
59    #[error("could not find `boost_context`; make sure either `libboost_context.a` or \
60            `libboost_context-mt.a` is located in the same directory as Folly")]
61    BoostContext,
62}
63
64pub fn probe_folly() -> Result<Folly, FollyError> {
65    // Folly's `.pc` file is missing the `fmt` and `gflags` dependencies. Find them here.
66    Config::new()
67        .statik(true)
68        .probe("fmt")
69        .map_err(FollyError::FmtDependency)?;
70    Config::new()
71        .statik(true)
72        .probe("gflags")
73        .map_err(FollyError::GflagsDependency)?;
74
75    // Unfortunately, the `pkg-config` crate doesn't successfully parse some of Folly's
76    // dependencies, because it passes the raw `.so` files instead of using `-l` flags. So call
77    // `pkg-config` manually.
78    let mut folly = Folly::new();
79    let output = Command::new("pkg-config")
80        .args(&["--static", "--libs", "libfolly"])
81        .output()
82        .map_err(FollyError::MainPackage)?;
83    let output = String::from_utf8(output.stdout).expect("`pkg-config --libs` wasn't UTF-8!");
84    for arg in Shlex::new(&output) {
85        if arg.starts_with('-') {
86            if let Some(rest) = arg.strip_prefix("-L") {
87                folly.lib_dirs.push(PathBuf::from(rest));
88            } else if let Some(rest) = arg.strip_prefix("-l") {
89                println!("cargo:rustc-link-lib={}", rest);
90            }
91            continue;
92        }
93
94        let path = PathBuf::from_str(&arg).unwrap();
95        let (parent, lib_name) = match (path.parent(), path.file_stem()) {
96            (Some(parent), Some(lib_name)) => (parent, lib_name),
97            _ => continue,
98        };
99        let lib_name = lib_name.to_string_lossy();
100        if let Some(rest) = lib_name.strip_prefix("lib") {
101            println!("cargo:rustc-link-search={}", parent.display());
102            println!("cargo:rustc-link-lib={}", rest);
103        }
104    }
105
106    // Unfortunately, just like `fmt` and `gflags`, Folly's `.pc` file doesn't contain a link flag
107    // for `boost_context`. What's worse, the name varies based on different systems
108    // (`libboost_context.a` vs.  `libboost_context-mt.a`). So find that library manually. We assume
109    // it's in the same directory as the Folly installation itself.
110    let mut found_boost_context = false;
111    for lib_dir in &folly.lib_dirs {
112        println!("cargo:rustc-link-search={}", lib_dir.display());
113
114        if found_boost_context {
115            continue;
116        }
117        for possible_lib_name in &["boost_context", "boost_context-mt"] {
118            let mut lib_dir = (*lib_dir).clone();
119            lib_dir.push(&format!("lib{}.a", possible_lib_name));
120            if !lib_dir.exists() {
121                continue;
122            }
123            println!("cargo:rustc-link-lib={}", possible_lib_name);
124            found_boost_context = true;
125            break;
126        }
127    }
128    if !found_boost_context {
129        return Err(FollyError::BoostContext);
130    }
131
132    let output = Command::new("pkg-config")
133        .args(&["--static", "--cflags", "libfolly"])
134        .output()
135        .map_err(FollyError::MainPackage)?;
136    let output = String::from_utf8(output.stdout).expect("`pkg-config --libs` wasn't UTF-8!");
137
138    for arg in output.split_whitespace() {
139        if let Some(rest) = arg.strip_prefix("-I") {
140            let path = Path::new(rest);
141            if path.starts_with("/Library/Developer/CommandLineTools/SDKs")
142                && path.ends_with("usr/include")
143            {
144                // Change any attempt to specify system headers from `-I` to `-isysroot`. `-I` is
145                // not the proper way to include a system header and will cause compilation failures
146                // on macOS Catalina.
147                //
148                // Pop off the trailing `usr/include`.
149                let sysroot = path.parent().unwrap().parent().unwrap();
150                folly.other_cflags.push("-isysroot".to_owned());
151                folly.other_cflags.push(sysroot.to_string_lossy().into_owned());
152            } else {
153                folly.include_paths.push(path.to_owned());
154            }
155        }
156    }
157
158    Ok(folly)
159}
160
161impl Folly {
162    fn new() -> Self {
163        Self {
164            lib_dirs: vec![],
165            include_paths: vec![],
166            other_cflags: vec![],
167            _priv: (),
168        }
169    }
170}