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}