tectonic_cfg_support/lib.rs
1// Copyright 2019-2020 the Tectonic Project
2// Licensed under the MIT License.
3
4//! This support crate helps deal with `CARGO_CFG_TARGET_*` variables. When
5//! cross-compiling with a `build.rs` script, these variables must be used
6//! instead of constructs such as `cfg!(target_arch = ...)` because the
7//! build.rs compilation targets the build host architecture, not the final
8//! target architecture.
9//!
10//! For more information, see the documentation on:
11//!
12//! * [cargo environment variables](https://doc.rust-lang.org/cargo/reference/environment-variables.html)
13//! * [conditional compilation](https://doc.rust-lang.org/reference/conditional-compilation.html)
14
15// Debugging help (requires nightly):
16//#![feature(trace_macros)]
17//trace_macros!(true);
18
19use lazy_static::lazy_static;
20
21lazy_static! {
22 pub static ref TARGET_CONFIG: TargetConfiguration = TargetConfiguration::default();
23}
24
25#[derive(Clone, Debug)]
26/// Information about the compilation target.
27///
28/// These parameters are derived from the `CARGO_TARGET_CFG_*` environment
29/// variables, which must be used to obtain correct results when
30/// cross-compiling a `build.rs` script. The configuration values are
31/// uppercased when they're loaded, to allow for case-insensitive comparisons
32/// later.
33pub struct TargetConfiguration {
34 pub arch: String,
35 pub feature: String,
36 pub os: String,
37 pub family: String,
38 pub env: String,
39 pub endian: String,
40 pub pointer_width: String,
41 pub vendor: String,
42}
43
44impl Default for TargetConfiguration {
45 /// Creates a TargetConfiguration from the `CARGO_CFG_TARGET_*`
46 /// [environment variables](https://doc.rust-lang.org/cargo/reference/environment-variables.html)
47 fn default() -> Self {
48 fn getenv(var: &'static str) -> String {
49 std::env::var(var)
50 .unwrap_or_else(|_| String::new())
51 .to_uppercase()
52 }
53
54 TargetConfiguration {
55 arch: getenv("CARGO_CFG_TARGET_ARCH"),
56 feature: getenv("CARGO_CFG_TARGET_FEATURE"),
57 os: getenv("CARGO_CFG_TARGET_OS"),
58 family: getenv("CARGO_CFG_TARGET_FAMILY"),
59 env: getenv("CARGO_CFG_TARGET_ENV"),
60 endian: getenv("CARGO_CFG_TARGET_ENDIAN"),
61 pointer_width: getenv("CARGO_CFG_TARGET_POINTER_WIDTH"),
62 vendor: getenv("CARGO_CFG_TARGET_VENDOR"),
63 }
64 }
65}
66
67impl TargetConfiguration {
68 /// Test whether the target architecture exactly matches the argument, in
69 /// case-insensitive fashion.
70 pub fn target_arch(&self, arch: &str) -> bool {
71 self.arch == arch.to_uppercase()
72 }
73
74 /// Test whether the target OS exactly matches the argument, in
75 /// case-insensitive fashion.
76 pub fn target_os(&self, os: &str) -> bool {
77 self.os == os.to_uppercase()
78 }
79
80 /// Test whether the target family exactly matches the argument, in
81 /// case-insensitive fashion.
82 pub fn target_family(&self, family: &str) -> bool {
83 self.family == family.to_uppercase()
84 }
85
86 /// Test whether the target "environment" exactly matches the argument, in
87 /// case-insensitive fashion.
88 pub fn target_env(&self, env: &str) -> bool {
89 self.env == env.to_uppercase()
90 }
91
92 /// Test whether the target endianness exactly matches the argument, in
93 /// case-insensitive fashion.
94 pub fn target_endian(&self, endian: &str) -> bool {
95 self.endian == endian.to_uppercase()
96 }
97
98 /// Test whether the target pointer width exactly matches the argument, in
99 /// case-insensitive fashion.
100 pub fn target_pointer_width(&self, pointer_width: &str) -> bool {
101 self.pointer_width == pointer_width.to_uppercase()
102 }
103
104 /// Test whether the target vendor exactly matches the argument, in
105 /// case-insensitive fashion.
106 pub fn target_vendor(&self, vendor: &str) -> bool {
107 self.vendor == vendor.to_uppercase()
108 }
109}
110
111/// Test for characteristics of the target machine.
112///
113/// Unlike the standard `cfg!` macro, this macro will give correct results
114/// when cross-compiling in a build.rs script. It attempts, but is not
115/// guaranteed, to emulate the syntax of the `cfg!` macro. Note, however,
116/// that the result of the macro must be evaluated at runtime, not compile-time.
117///
118/// Supported syntaxes:
119///
120/// ```notest
121/// target_cfg!(target_os = "macos");
122/// target_cfg!(not(target_os = "macos"));
123/// target_cfg!(any(target_os = "macos", target_endian = "big"));
124/// target_cfg!(all(target_os = "macos", target_endian = "big"));
125/// target_cfg!(all(target_os = "macos", not(target_endian = "big")));
126/// ```
127// Here we go with some exciting macro fun!
128//
129// Since each individual test can be evaluated to a boolean on-the-spot, the
130// macro expands out to a big boolean logical expression. Fundamentally, it's
131// not too hard to allow complex syntax because the macro can recurse:
132//
133// ```
134// target_cfg!(not(whatever)) => !(target_cfg!(whatever))
135// target_cfg!(any(c1, c2)) => target_cfg!(c1) || target_cfg!(c2)
136// ```
137//
138// The core implementation challenge here is that we need to parse
139// comma-separated lists where each term might contain all sorts of unexpected
140// content. Within the confines of the macro_rules! formalism, this means that
141// we need to scan through such comma-separated lists and group their tokens
142// before actually evaluating them.
143//
144// Some key points to remember about how this all works:
145//
146// 1. A "token tree" type is either a single token or a series of tokens
147// delimited by balanced delimiters such as ({[]}). As such, in order to
148// match an arbitrary token sequence, you need to use repetition
149// expressions of the form `$($toks:tt)+`.
150// 2. The macro evaluator looks at rules in order and cannot backtrack. That
151// is, if it is looking at a rule and has matched the first 5 tokens but
152// the 6th disagrees, there must be a subsequent rule that also matches
153// those first 5 tokens.
154// 3. Given the above, we use the standard trick of having different macro
155// "modes" prefixed with an expression like `@emit`. They are essentially
156// different sub-macros but this trick allows us to get everything done
157// with one named macro_rules! export.
158// 4. Also due to the above, the logical flow of the macro generally goes from
159// bottom to top, so that's probably the best way to read the code.
160//
161// Some links for reference:
162//
163// - https://users.rust-lang.org/t/top-down-macro-parsing-or-higher-order-macros/8879
164// - https://danielkeep.github.io/tlborm/book/pat-incremental-tt-munchers.html
165#[macro_export]
166macro_rules! target_cfg {
167 // "@emit" rules are used for comma-separated lists that have had their
168 // tokens grouped. The general pattern is: `target_cfg!(@emit $operation
169 // {clause1..} {clause2..} {clause3..})`.
170
171 // Emitting `any(clause1,clause2,...)`: convert to `target_cfg!(clause1) && target_cfg!(clause2) && ...`
172 (
173 @emit
174 all
175 $({$($grouped:tt)+})+
176 ) => {
177 ($(
178 (target_cfg!($($grouped)+))
179 )&&+)
180 };
181
182 // Likewise for `all(clause1,clause2,...)`.
183 (
184 @emit
185 any
186 $({$($grouped:tt)+})+
187 ) => {
188 ($(
189 (target_cfg!($($grouped)+))
190 )||+)
191 };
192
193 // "@clause" rules are used to parse the comma-separated lists. They munch
194 // their inputs token-by-token and finally invoke an "@emit" rule when the
195 // list is all grouped. The general pattern for recording the parser state
196 // is:
197 //
198 // ```
199 // target_cfg!(
200 // @clause $operation
201 // [{grouped-clause-1} {grouped-clause-2...}]
202 // [not-yet-parsed-tokens...]
203 // current-clause-tokens...
204 // )
205 // ```
206
207 // This rule must come first in this section. It fires when the next token
208 // to parse is a comma. When this happens, we take the tokens in the
209 // current clause and add them to the list of grouped clauses, adding
210 // delimeters so that the grouping can be easily extracted again in the
211 // emission stage.
212 (
213 @clause
214 $op:ident
215 [$({$($grouped:tt)+})*]
216 [, $($rest:tt)*]
217 $($current:tt)+
218 ) => {
219 target_cfg!(@clause $op [
220 $(
221 {$($grouped)+}
222 )*
223 {$($current)+}
224 ] [
225 $($rest)*
226 ])
227 };
228
229 // This rule comes next. It fires when the next un-parsed token is *not* a
230 // comma. In this case, we add that token to the list of tokens in the
231 // current clause, then move on to the next one.
232 (
233 @clause
234 $op:ident
235 [$({$($grouped:tt)+})*]
236 [$tok:tt $($rest:tt)*]
237 $($current:tt)*
238 ) => {
239 target_cfg!(@clause $op [
240 $(
241 {$($grouped)+}
242 )*
243 ] [
244 $($rest)*
245 ] $($current)* $tok)
246 };
247
248 // This rule fires when there are no more tokens to parse in this list. We
249 // finish off the "current" token group, then delegate to the emission
250 // rule.
251 (
252 @clause
253 $op:ident
254 [$({$($grouped:tt)+})*]
255 []
256 $($current:tt)+
257 ) => {
258 target_cfg!(@emit $op
259 $(
260 {$($grouped)+}
261 )*
262 {$($current)+}
263 )
264 };
265
266 // Finally, these are the "toplevel" syntaxes for specific tests that can
267 // be performed. Any construction not prefixed with one of the magic
268 // tokens must match one of these.
269
270 // `all(clause1, clause2...)` : we must parse this comma-separated list and
271 // partner with `@emit all` to output a bunch of && terms.
272 (
273 all($($tokens:tt)+)
274 ) => {
275 target_cfg!(@clause all [] [$($tokens)+])
276 };
277
278 // Likewise for `any(clause1, clause2...)`
279 (
280 any($($tokens:tt)+)
281 ) => {
282 target_cfg!(@clause any [] [$($tokens)+])
283 };
284
285 // `not(clause)`: compute the inner clause, then just negate it.
286 (
287 not($($tokens:tt)+)
288 ) => {
289 !(target_cfg!($($tokens)+))
290 };
291
292 // `param = value`: test for equality.
293 (
294 $e:tt = $v:expr
295 ) => {
296 $crate::TARGET_CONFIG.$e($v)
297 };
298}
299
300#[cfg(test)]
301mod tests {
302 /// Set up the environment variables for testing. We intentionally choose
303 /// values that don't occur in the real world, except for parameters that
304 /// have heavily constrained options, to avoid accidentally passing
305 /// if/when running the test suite on familiar hardware.
306 fn setup_test_env() {
307 std::env::set_var("CARGO_CFG_TARGET_ARCH", "testarch");
308 std::env::set_var("CARGO_CFG_TARGET_FEATURE", "testfeat1,testfeat2");
309 std::env::set_var("CARGO_CFG_TARGET_OS", "testos");
310 std::env::set_var("CARGO_CFG_TARGET_FAMILY", "testfamily");
311 std::env::set_var("CARGO_CFG_TARGET_ENV", "testenv");
312 std::env::set_var("CARGO_CFG_TARGET_ENDIAN", "little");
313 std::env::set_var("CARGO_CFG_TARGET_POINTER_WIDTH", "32");
314 std::env::set_var("CARGO_CFG_TARGET_VENDOR", "testvendor");
315 }
316
317 /// No recursion. Check all of the supported tests.
318 #[test]
319 fn test_level0() {
320 setup_test_env();
321
322 assert!(target_cfg!(target_arch = "testarch"));
323 assert!(!target_cfg!(target_arch = "wrong"));
324
325 assert!(target_cfg!(target_os = "testos"));
326 assert!(target_cfg!(target_family = "testfamily"));
327 assert!(target_cfg!(target_env = "testenv"));
328 assert!(target_cfg!(target_endian = "little"));
329 assert!(target_cfg!(target_pointer_width = "32"));
330 assert!(target_cfg!(target_vendor = "testvendor"));
331 }
332
333 /// Basic recursion.
334 #[test]
335 fn test_level1() {
336 setup_test_env();
337
338 assert!(target_cfg!(not(target_arch = "wrong")));
339 assert!(!target_cfg!(not(target_arch = "testarch")));
340
341 assert!(target_cfg!(all(target_arch = "testarch")));
342 assert!(!target_cfg!(all(target_arch = "wrong")));
343 assert!(target_cfg!(all(
344 target_arch = "testarch",
345 target_os = "testos"
346 )));
347 assert!(!target_cfg!(all(
348 target_arch = "testarch",
349 target_os = "wrong"
350 )));
351
352 assert!(target_cfg!(any(target_arch = "testarch")));
353 assert!(!target_cfg!(any(target_arch = "wrong")));
354 assert!(target_cfg!(any(
355 target_arch = "testarch",
356 target_os = "testos"
357 )));
358 assert!(target_cfg!(any(
359 target_arch = "testarch",
360 target_os = "wrong"
361 )));
362 assert!(!target_cfg!(any(
363 target_arch = "wrong1",
364 target_os = "wrong2"
365 )));
366 }
367
368 /// Even deeper recursion.
369 #[test]
370 fn test_level2() {
371 setup_test_env();
372
373 assert!(target_cfg!(all(not(target_arch = "wrong"))));
374 assert!(!target_cfg!(all(not(target_arch = "testarch"))));
375
376 assert!(target_cfg!(all(
377 target_arch = "testarch",
378 not(target_os = "wrong")
379 )));
380
381 assert!(target_cfg!(all(
382 any(target_arch = "testarch", target_os = "wrong"),
383 target_env = "testenv"
384 )));
385
386 assert!(target_cfg!(all(
387 any(target_arch = "testarch", target_os = "wrong"),
388 not(target_vendor = "wrong")
389 )));
390 }
391}