Skip to main content

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