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}