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}