tectonic_dep_support/
lib.rs

1// Copyright 2020 the Tectonic Project
2// Licensed under the MIT License.
3
4#![deny(missing_docs)]
5
6//! Support for locating third-party libraries (“dependencies”) when building
7//! Tectonic. The main point of interest is that both pkg-config and vcpkg are
8//! supported as dep-finding backends. This crate does *not* deal with the
9//! choice of whether to provide a library externally or through vendoring.
10
11use std::{
12    env,
13    path::{Path, PathBuf},
14};
15
16/// Supported depedency-finding backends.
17#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
18pub enum Backend {
19    /// pkg-config
20    #[default]
21    PkgConfig,
22
23    /// vcpkg
24    Vcpkg,
25}
26
27/// Dep-finding configuration.
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct Configuration {
30    /// The dep-finding backend being used.
31    pub backend: Backend,
32
33    semi_static_mode: bool,
34}
35
36impl Default for Configuration {
37    /// This default function will fetch settings from the environment. Is that a no-no?
38    fn default() -> Self {
39        println!("cargo:rerun-if-env-changed=TECTONIC_DEP_BACKEND");
40        println!("cargo:rerun-if-env-changed=TECTONIC_PKGCONFIG_FORCE_SEMI_STATIC");
41
42        // This should use FromStr or whatever, but meh.
43        let backend = if let Ok(dep_backend_str) = env::var("TECTONIC_DEP_BACKEND") {
44            match dep_backend_str.as_ref() {
45                "pkg-config" => Backend::PkgConfig,
46                "vcpkg" => Backend::Vcpkg,
47                "default" => Backend::default(),
48                other => panic!("unrecognized TECTONIC_DEP_BACKEND setting {:?}", other),
49            }
50        } else {
51            Backend::default()
52        };
53
54        let semi_static_mode = env::var("TECTONIC_PKGCONFIG_FORCE_SEMI_STATIC").is_ok();
55
56        Configuration {
57            backend,
58            semi_static_mode,
59        }
60    }
61}
62
63/// Information specifying a dependency.
64pub trait Spec {
65    /// Get the pkg-config specification used to check for this dependency. This
66    /// text will be passed into `pkg_config::Config::probe()`.
67    fn get_pkgconfig_spec(&self) -> &str;
68
69    /// Get the vcpkg packages used to check for this dependency. These will be
70    /// passed into `vcpkg::Config::find_package()`.
71    fn get_vcpkg_spec(&self) -> &[&str];
72}
73
74/// Build-script state when using pkg-config as the backend.
75#[derive(Debug)]
76struct PkgConfigState {
77    libs: pkg_config::Library,
78}
79
80/// Build-script state when using vcpkg as the backend.
81#[derive(Clone, Debug)]
82struct VcPkgState {
83    include_paths: Vec<PathBuf>,
84}
85
86/// State for discovering and managing a dependency, which may vary
87/// depending on the framework that we're using to discover them.
88#[derive(Debug)]
89#[allow(clippy::large_enum_variant)]
90enum DepState {
91    /// pkg-config
92    PkgConfig(PkgConfigState),
93
94    /// vcpkg
95    VcPkg(VcPkgState),
96}
97
98impl DepState {
99    /// Probe the dependency.
100    fn new<T: Spec>(spec: &T, config: &Configuration) -> Self {
101        match config.backend {
102            Backend::PkgConfig => DepState::new_from_pkg_config(spec, config),
103            Backend::Vcpkg => DepState::new_from_vcpkg(spec, config),
104        }
105    }
106
107    /// Probe using pkg-config.
108    fn new_from_pkg_config<T: Spec>(spec: &T, config: &Configuration) -> Self {
109        let libs = pkg_config::Config::new()
110            .cargo_metadata(false)
111            .statik(config.semi_static_mode)
112            .probe(spec.get_pkgconfig_spec())
113            .unwrap();
114
115        DepState::PkgConfig(PkgConfigState { libs })
116    }
117
118    /// Probe using vcpkg.
119    fn new_from_vcpkg<T: Spec>(spec: &T, _config: &Configuration) -> Self {
120        let mut include_paths = vec![];
121
122        for dep in spec.get_vcpkg_spec() {
123            let library = match vcpkg::Config::new().cargo_metadata(false).find_package(dep) {
124                Ok(lib) => lib,
125                Err(e) => {
126                    if let vcpkg::Error::LibNotFound(_) = e {
127                        // We should potentially be referencing the CARGO_CFG_TARGET_*
128                        // variables to handle cross-compilation (cf. the
129                        // tectonic_cfg_support crate), but vcpkg-rs doesn't use them
130                        // either.
131                        let target = env::var("TARGET").unwrap_or_default();
132
133                        if target == "x86_64-pc-windows-msvc" {
134                            println!("cargo:warning=you may need to export VCPKGRS_TRIPLET=x64-windows-static-release ...");
135                            println!("cargo:warning=... which is a custom triplet used by Tectonic's cargo-vcpkg integration");
136                        }
137                    }
138
139                    panic!("failed to load package {} from vcpkg: {}", dep, e)
140                }
141            };
142
143            include_paths.extend(library.include_paths.iter().cloned());
144        }
145
146        DepState::VcPkg(VcPkgState { include_paths })
147    }
148}
149
150/// A dependency.
151pub struct Dependency<'a, T: Spec> {
152    config: &'a Configuration,
153    spec: T,
154    state: DepState,
155}
156
157impl<'a, T: Spec> Dependency<'a, T> {
158    /// Probe the dependency.
159    pub fn probe(spec: T, config: &'a Configuration) -> Self {
160        let state = DepState::new(&spec, config);
161
162        Dependency {
163            config,
164            spec,
165            state,
166        }
167    }
168
169    /// Invoke a callback for each C/C++ include directory injected by our
170    /// dependencies.
171    pub fn foreach_include_path<F>(&self, mut f: F)
172    where
173        F: FnMut(&Path),
174    {
175        match self.state {
176            DepState::PkgConfig(ref s) => {
177                for p in &s.libs.include_paths {
178                    f(p);
179                }
180            }
181
182            DepState::VcPkg(ref s) => {
183                for p in &s.include_paths {
184                    f(p);
185                }
186            }
187        }
188    }
189
190    /// Emit build information about this dependency. This should be called
191    /// after all information for in-crate builds is emitted.
192    pub fn emit(&self) {
193        match self.state {
194            DepState::PkgConfig(ref state) => {
195                if self.config.semi_static_mode {
196                    // pkg-config will prevent "system libraries" from being
197                    // linked statically even when PKG_CONFIG_ALL_STATIC=1,
198                    // but its definition of a system library isn't always
199                    // perfect. For Debian cross builds, we'd like to make
200                    // binaries that are dynamically linked with things like
201                    // libc and libm but not libharfbuzz, etc. In this mode we
202                    // override pkg-config's logic by emitting the metadata
203                    // ourselves.
204                    for link_path in &state.libs.link_paths {
205                        println!("cargo:rustc-link-search=native={}", link_path.display());
206                    }
207
208                    for fw_path in &state.libs.framework_paths {
209                        println!("cargo:rustc-link-search=framework={}", fw_path.display());
210                    }
211
212                    for libbase in &state.libs.libs {
213                        let do_static = match libbase.as_ref() {
214                            "c" | "m" | "dl" | "pthread" => false,
215                            _ => {
216                                // Frustratingly, graphite2 seems to have
217                                // issues with static builds; e.g. static
218                                // graphite2 is not available on Debian. So
219                                // let's jump through the hoops of testing
220                                // whether the static archive seems findable.
221                                let libname = format!("lib{libbase}.a");
222                                state
223                                    .libs
224                                    .link_paths
225                                    .iter()
226                                    .any(|d| d.join(&libname).exists())
227                            }
228                        };
229
230                        let mode = if do_static { "static=" } else { "" };
231                        println!("cargo:rustc-link-lib={mode}{libbase}");
232                    }
233
234                    for fw in &state.libs.frameworks {
235                        println!("cargo:rustc-link-lib=framework={fw}");
236                    }
237                } else {
238                    // Just let pkg-config do its thing.
239                    pkg_config::Config::new()
240                        .cargo_metadata(true)
241                        .probe(self.spec.get_pkgconfig_spec())
242                        .unwrap();
243                }
244            }
245
246            DepState::VcPkg(_) => {
247                for dep in self.spec.get_vcpkg_spec() {
248                    vcpkg::find_package(dep).unwrap_or_else(|e| {
249                        panic!("failed to load package {} from vcpkg: {}", dep, e)
250                    });
251                }
252            }
253        }
254    }
255}