1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
// Copyright 2019-2020 the Tectonic Project
// Licensed under the MIT License.

//! This support crate helps deal with `CARGO_CFG_TARGET_*` variables. When
//! cross-compiling with a `build.rs` script, these variables must be used
//! instead of constructs such as `cfg!(target_arch = ...)` because the
//! build.rs compilation targets the build host architecture, not the final
//! target architecture.
//!
//! For more information, see the documentation on:
//!
//! * [cargo environment variables](https://doc.rust-lang.org/cargo/reference/environment-variables.html)
//! * [conditional compilation](https://doc.rust-lang.org/reference/conditional-compilation.html)

// Debugging help (requires nightly):
//#![feature(trace_macros)]
//trace_macros!(true);

use lazy_static::lazy_static;

lazy_static! {
    pub static ref TARGET_CONFIG: TargetConfiguration = TargetConfiguration::default();
}

#[derive(Clone, Debug)]
/// Information about the compilation target.
///
/// These parameters are derived from the `CARGO_TARGET_CFG_*` environment
/// variables, which must be used to obtain correct results when
/// cross-compiling a `build.rs` script. The configuration values are
/// uppercased when they're loaded, to allow for case-insensitive comparisons
/// later.
pub struct TargetConfiguration {
    pub arch: String,
    pub feature: String,
    pub os: String,
    pub family: String,
    pub env: String,
    pub endian: String,
    pub pointer_width: String,
    pub vendor: String,
}

impl Default for TargetConfiguration {
    /// Creates a TargetConfiguration from the `CARGO_CFG_TARGET_*`
    /// [environment variables](https://doc.rust-lang.org/cargo/reference/environment-variables.html)
    fn default() -> Self {
        fn getenv(var: &'static str) -> String {
            std::env::var(var)
                .unwrap_or_else(|_| String::new())
                .to_uppercase()
        }

        TargetConfiguration {
            arch: getenv("CARGO_CFG_TARGET_ARCH"),
            feature: getenv("CARGO_CFG_TARGET_FEATURE"),
            os: getenv("CARGO_CFG_TARGET_OS"),
            family: getenv("CARGO_CFG_TARGET_FAMILY"),
            env: getenv("CARGO_CFG_TARGET_ENV"),
            endian: getenv("CARGO_CFG_TARGET_ENDIAN"),
            pointer_width: getenv("CARGO_CFG_TARGET_POINTER_WIDTH"),
            vendor: getenv("CARGO_CFG_TARGET_VENDOR"),
        }
    }
}

impl TargetConfiguration {
    /// Test whether the target architecture exactly matches the argument, in
    /// case-insensitive fashion.
    pub fn target_arch(&self, arch: &str) -> bool {
        self.arch == arch.to_uppercase()
    }

    /// Test whether the target OS exactly matches the argument, in
    /// case-insensitive fashion.
    pub fn target_os(&self, os: &str) -> bool {
        self.os == os.to_uppercase()
    }

    /// Test whether the target family exactly matches the argument, in
    /// case-insensitive fashion.
    pub fn target_family(&self, family: &str) -> bool {
        self.family == family.to_uppercase()
    }

    /// Test whether the target "environment" exactly matches the argument, in
    /// case-insensitive fashion.
    pub fn target_env(&self, env: &str) -> bool {
        self.env == env.to_uppercase()
    }

    /// Test whether the target endianness exactly matches the argument, in
    /// case-insensitive fashion.
    pub fn target_endian(&self, endian: &str) -> bool {
        self.endian == endian.to_uppercase()
    }

    /// Test whether the target pointer width exactly matches the argument, in
    /// case-insensitive fashion.
    pub fn target_pointer_width(&self, pointer_width: &str) -> bool {
        self.pointer_width == pointer_width.to_uppercase()
    }

    /// Test whether the target vendor exactly matches the argument, in
    /// case-insensitive fashion.
    pub fn target_vendor(&self, vendor: &str) -> bool {
        self.vendor == vendor.to_uppercase()
    }
}

/// Test for characteristics of the target machine.
///
/// Unlike the standard `cfg!` macro, this macro will give correct results
/// when cross-compiling in a build.rs script. It attempts, but is not
/// guaranteed, to emulate the syntax of the `cfg!` macro. Note, however,
/// that the result of the macro must be evaluated at runtime, not compile-time.
///
/// Supported syntaxes:
///
/// ```notest
/// target_cfg!(target_os = "macos");
/// target_cfg!(not(target_os = "macos"));
/// target_cfg!(any(target_os = "macos", target_endian = "big"));
/// target_cfg!(all(target_os = "macos", target_endian = "big"));
/// target_cfg!(all(target_os = "macos", not(target_endian = "big")));
/// ```
// Here we go with some exciting macro fun!
//
// Since each individual test can be evaluated to a boolean on-the-spot, the
// macro expands out to a big boolean logical expression. Fundamentally, it's
// not too hard to allow complex syntax because the macro can recurse:
//
// ```
// target_cfg!(not(whatever)) => !(target_cfg!(whatever))
// target_cfg!(any(c1, c2)) => target_cfg!(c1) || target_cfg!(c2)
// ```
//
// The core implementation challenge here is that we need to parse
// comma-separated lists where each term might contain all sorts of unexpected
// content. Within the confines of the macro_rules! formalism, this means that
// we need to scan through such comma-separated lists and group their tokens
// before actually evaluating them.
//
// Some key points to remember about how this all works:
//
// 1. A "token tree" type is either a single token or a series of tokens
//    delimited by balanced delimiters such as ({[]}). As such, in order to
//    match an arbitrary token sequence, you need to use repetition
//    expressions of the form `$($toks:tt)+`.
// 2. The macro evaluator looks at rules in order and cannot backtrack. That
//    is, if it is looking at a rule and has matched the first 5 tokens but
//    the 6th disagrees, there must be a subsequent rule that also matches
//    those first 5 tokens.
// 3. Given the above, we use the standard trick of having different macro
//    "modes" prefixed with an expression like `@emit`. They are essentially
//    different sub-macros but this trick allows us to get everything done
//    with one named macro_rules! export.
// 4. Also due to the above, the logical flow of the macro generally goes from
//    bottom to top, so that's probably the best way to read the code.
//
// Some links for reference:
//
// - https://users.rust-lang.org/t/top-down-macro-parsing-or-higher-order-macros/8879
// - https://danielkeep.github.io/tlborm/book/pat-incremental-tt-munchers.html
#[macro_export]
macro_rules! target_cfg {
    // "@emit" rules are used for comma-separated lists that have had their
    // tokens grouped. The general pattern is: `target_cfg!(@emit $operation
    // {clause1..} {clause2..} {clause3..})`.

    // Emitting `any(clause1,clause2,...)`: convert to `target_cfg!(clause1) && target_cfg!(clause2) && ...`
    (
        @emit
        all
        $({$($grouped:tt)+})+
    ) => {
        ($(
            (target_cfg!($($grouped)+))
        )&&+)
    };

    // Likewise for `all(clause1,clause2,...)`.
    (
        @emit
        any
        $({$($grouped:tt)+})+
    ) => {
        ($(
            (target_cfg!($($grouped)+))
        )||+)
    };

    // "@clause" rules are used to parse the comma-separated lists. They munch
    // their inputs token-by-token and finally invoke an "@emit" rule when the
    // list is all grouped. The general pattern for recording the parser state
    // is:
    //
    // ```
    // target_cfg!(
    //    @clause $operation
    //    [{grouped-clause-1} {grouped-clause-2...}]
    //    [not-yet-parsed-tokens...]
    //    current-clause-tokens...
    // )
    // ```

    // This rule must come first in this section. It fires when the next token
    // to parse is a comma. When this happens, we take the tokens in the
    // current clause and add them to the list of grouped clauses, adding
    // delimeters so that the grouping can be easily extracted again in the
    // emission stage.
    (
        @clause
        $op:ident
        [$({$($grouped:tt)+})*]
        [, $($rest:tt)*]
        $($current:tt)+
    ) => {
        target_cfg!(@clause $op [
            $(
                {$($grouped)+}
            )*
            {$($current)+}
        ] [
            $($rest)*
        ])
    };

    // This rule comes next. It fires when the next un-parsed token is *not* a
    // comma. In this case, we add that token to the list of tokens in the
    // current clause, then move on to the next one.
    (
        @clause
        $op:ident
        [$({$($grouped:tt)+})*]
        [$tok:tt $($rest:tt)*]
        $($current:tt)*
    ) => {
        target_cfg!(@clause $op [
            $(
                {$($grouped)+}
            )*
        ] [
            $($rest)*
        ] $($current)* $tok)
    };

    // This rule fires when there are no more tokens to parse in this list. We
    // finish off the "current" token group, then delegate to the emission
    // rule.
    (
        @clause
        $op:ident
        [$({$($grouped:tt)+})*]
        []
        $($current:tt)+
    ) => {
        target_cfg!(@emit $op
            $(
                {$($grouped)+}
            )*
            {$($current)+}
        )
    };

    // Finally, these are the "toplevel" syntaxes for specific tests that can
    // be performed. Any construction not prefixed with one of the magic
    // tokens must match one of these.

    // `all(clause1, clause2...)` : we must parse this comma-separated list and
    // partner with `@emit all` to output a bunch of && terms.
    (
        all($($tokens:tt)+)
    ) => {
        target_cfg!(@clause all [] [$($tokens)+])
    };

    // Likewise for `any(clause1, clause2...)`
    (
        any($($tokens:tt)+)
    ) => {
        target_cfg!(@clause any [] [$($tokens)+])
    };

    // `not(clause)`: compute the inner clause, then just negate it.
    (
        not($($tokens:tt)+)
    ) => {
        !(target_cfg!($($tokens)+))
    };

    // `param = value`: test for equality.
    (
        $e:tt = $v:expr
    ) => {
        $crate::TARGET_CONFIG.$e($v)
    };
}

#[cfg(test)]
mod tests {
    /// Set up the environment variables for testing. We intentionally choose
    /// values that don't occur in the real world, except for parameters that
    /// have heavily constrained options, to avoid accidentally passing
    /// if/when running the test suite on familiar hardware.
    fn setup_test_env() {
        std::env::set_var("CARGO_CFG_TARGET_ARCH", "testarch");
        std::env::set_var("CARGO_CFG_TARGET_FEATURE", "testfeat1,testfeat2");
        std::env::set_var("CARGO_CFG_TARGET_OS", "testos");
        std::env::set_var("CARGO_CFG_TARGET_FAMILY", "testfamily");
        std::env::set_var("CARGO_CFG_TARGET_ENV", "testenv");
        std::env::set_var("CARGO_CFG_TARGET_ENDIAN", "little");
        std::env::set_var("CARGO_CFG_TARGET_POINTER_WIDTH", "32");
        std::env::set_var("CARGO_CFG_TARGET_VENDOR", "testvendor");
    }

    /// No recursion. Check all of the supported tests.
    #[test]
    fn test_level0() {
        setup_test_env();

        assert!(target_cfg!(target_arch = "testarch"));
        assert!(!target_cfg!(target_arch = "wrong"));

        assert!(target_cfg!(target_os = "testos"));
        assert!(target_cfg!(target_family = "testfamily"));
        assert!(target_cfg!(target_env = "testenv"));
        assert!(target_cfg!(target_endian = "little"));
        assert!(target_cfg!(target_pointer_width = "32"));
        assert!(target_cfg!(target_vendor = "testvendor"));
    }

    /// Basic recursion.
    #[test]
    fn test_level1() {
        setup_test_env();

        assert!(target_cfg!(not(target_arch = "wrong")));
        assert!(!target_cfg!(not(target_arch = "testarch")));

        assert!(target_cfg!(all(target_arch = "testarch")));
        assert!(!target_cfg!(all(target_arch = "wrong")));
        assert!(target_cfg!(all(
            target_arch = "testarch",
            target_os = "testos"
        )));
        assert!(!target_cfg!(all(
            target_arch = "testarch",
            target_os = "wrong"
        )));

        assert!(target_cfg!(any(target_arch = "testarch")));
        assert!(!target_cfg!(any(target_arch = "wrong")));
        assert!(target_cfg!(any(
            target_arch = "testarch",
            target_os = "testos"
        )));
        assert!(target_cfg!(any(
            target_arch = "testarch",
            target_os = "wrong"
        )));
        assert!(!target_cfg!(any(
            target_arch = "wrong1",
            target_os = "wrong2"
        )));
    }

    /// Even deeper recursion.
    #[test]
    fn test_level2() {
        setup_test_env();

        assert!(target_cfg!(all(not(target_arch = "wrong"))));
        assert!(!target_cfg!(all(not(target_arch = "testarch"))));

        assert!(target_cfg!(all(
            target_arch = "testarch",
            not(target_os = "wrong")
        )));

        assert!(target_cfg!(all(
            any(target_arch = "testarch", target_os = "wrong"),
            target_env = "testenv"
        )));

        assert!(target_cfg!(all(
            any(target_arch = "testarch", target_os = "wrong"),
            not(target_vendor = "wrong")
        )));
    }
}