build_env/
lib.rs

1use std::borrow::ToOwned;
2use std::collections::BTreeSet;
3use std::env;
4use std::ffi::{OsStr, OsString};
5use std::{any, error, fmt};
6
7/**
8 * Allow retrieval of values pretaining to a `build` process that may be related to the `target`
9 * and/or `host` triple.
10 *
11 */
12#[derive(Debug, Clone)]
13pub struct BuildEnv {
14    /*
15     * restricted to String due to our use of String::replace
16     */
17    target: String,
18    host: String,
19
20    // env vars accessed. note that we use a BTreeSet to get deterministic ordering
21    used_env_vars: BTreeSet<OsString>,
22}
23
24/// If variable retrieval fails, it will be for one of these reasons
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum VarErrorKind {
27    NotString(OsString),
28    RequiredEnvMissing(env::VarError),
29}
30
31/// Describes a variable retrieval failure
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct VarError<K: AsRef<OsStr>> {
34    key: K,
35    kind: VarErrorKind,
36}
37
38impl<K: AsRef<OsStr>> fmt::Display for VarError<K> {
39    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
40        match self.kind {
41            VarErrorKind::NotString(ref x) => write!(
42                fmt,
43                "Variable {:?} was found, but is not utf-8: {:?}",
44                self.key.as_ref(),
45                x
46            ),
47            VarErrorKind::RequiredEnvMissing(ref x) => write!(
48                fmt,
49                "Variable {:?} is required, but retrival failed: {}",
50                self.key.as_ref(),
51                x
52            ),
53        }
54    }
55}
56
57impl<K: AsRef<OsStr> + fmt::Debug + any::Any> error::Error for VarError<K> {
58    fn description(&self) -> &str {
59        match self.kind {
60            VarErrorKind::NotString(_) => "found but not utf-8",
61            VarErrorKind::RequiredEnvMissing(_) => "other required env var missing",
62        }
63    }
64}
65
66fn required_env_var(key: &str) -> Result<String, VarError<String>> {
67    env::var(key).map_err(|e| VarError {
68        key: key.to_owned(),
69        kind: VarErrorKind::RequiredEnvMissing(e),
70    })
71}
72
73impl BuildEnv {
74    /**
75     * Use environment variables (such as those set by cargo) to determine values for `target` and
76     * `host` via the environment variables `TARGET` and `HOST`.
77     */
78    pub fn from_env() -> Result<BuildEnv, VarError<String>> {
79        // NOTE: we don't consider these env vars "used" because cargo already will call build
80        // scripts again if they change
81        let target = required_env_var("TARGET")?;
82        let host = required_env_var("HOST")?;
83
84        Ok(BuildEnv {
85            target,
86            host,
87            used_env_vars: Default::default(),
88        })
89    }
90
91    /**
92     * Construct a BuildEnv where the host and target _may_ be different.
93     */
94    pub fn new_cross(host: String, target: String) -> BuildEnv {
95        BuildEnv {
96            host,
97            target,
98            used_env_vars: Default::default(),
99        }
100    }
101
102    /**
103     * Construct a BuildEnv where target and host are the same.
104     */
105    pub fn new(trip: String) -> BuildEnv {
106        BuildEnv {
107            host: trip.clone(),
108            target: trip,
109            used_env_vars: Default::default(),
110        }
111    }
112
113    /**
114     * The target we're supplying values for
115     */
116    pub fn target(&self) -> &str {
117        &self.target
118    }
119
120    /**
121     * The host we're supplying values for
122     */
123    pub fn host(&self) -> &str {
124        &self.host
125    }
126
127    /// Get the env vars that have been used by build-env queries so far
128    ///
129    pub fn used_env_vars(&self) -> impl Iterator<Item = &OsString> {
130        self.used_env_vars.iter()
131    }
132
133    /// Print the used environment variables in the form interpreted by cargo: `cargo:rerun-if-env-changed=FOO`
134    pub fn cargo_print_used_env_vars(&self) {
135        for used in self.used_env_vars() {
136            // NOTE: complains loudly if we use a env-var we can't track because it isn't utf-8
137            println!("cargo:rerun-if-env-changed={}", used.to_str().unwrap());
138        }
139    }
140
141    pub fn mark_used(&mut self, var: OsString) {
142        println!(
143            "cargo:rerun-if-env-changed={}",
144            var.to_str().expect("tried to examine non-utf-8 variable")
145        );
146        self.used_env_vars.insert(var);
147    }
148
149    fn env_one(&mut self, var: OsString) -> Option<OsString> {
150        let v = env::var_os(&var);
151        self.mark_used(var);
152        v
153    }
154
155    /// Query the environment for a value, trying the most specific first, before querying more
156    /// general variables.
157    ///
158    /// 1. `<var>_<target>` - for example, `CC_x86_64-unknown-linux-gnu`
159    /// 2. `<var>_<target_with_underscores>` - for example, `CC_x86_64_unknown_linux_gnu`
160    /// 3. `<build-kind>_<var>` - for example, `HOST_CC` or `TARGET_CFLAGS`
161    /// 4. `<var>` - a plain `CC`, `AR` as above.
162    pub fn var<K: AsRef<OsStr>>(&mut self, var_base: K) -> Option<OsString> {
163        /* try the most specific item to the least specific item */
164        let target = self.target();
165        let host = self.host();
166        let kind = if host == target { "HOST" } else { "TARGET" };
167        let target_u = target.replace("-", "_");
168        let mut a: OsString = var_base.as_ref().to_owned();
169        a.push("_");
170
171        let mut b = a.clone();
172
173        a.push(target);
174        b.push(target_u);
175
176        let mut c: OsString = AsRef::<OsStr>::as_ref(kind).to_owned();
177        c.push("_");
178        c.push(&var_base);
179
180        self.env_one(a)
181            .or_else(|| self.env_one(b))
182            .or_else(|| self.env_one(c))
183            .or_else(|| self.env_one(var_base.as_ref().to_owned()))
184    }
185
186    /// The same as [`var()`], but converts the return to an OsString and provides a useful error
187    /// message
188    pub fn var_str<K: AsRef<OsStr> + fmt::Debug + any::Any>(
189        &mut self,
190        var_base: K,
191    ) -> Option<Result<String, VarError<K>>> {
192        self.var(&var_base).map(|v| {
193            v.into_string().map_err(|o| VarError {
194                key: var_base,
195                kind: VarErrorKind::NotString(o),
196            })
197        })
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::BuildEnv;
204    use std::env;
205
206    fn clear(trip: &str, var: &[&str]) {
207        for v in var {
208            env::remove_var(&format!("HOST_{}", v));
209            env::remove_var(&format!("TARGET_{}", v));
210            env::remove_var(&format!("{}_{}", v, trip));
211            env::remove_var(&format!("{}_{}", v, trip.replace("-", "_")));
212            env::remove_var(v);
213        }
214    }
215
216    fn most_general() {
217        let t = "this-is-a-target";
218        let cc = "a-cc-value";
219        clear(t, &["CC"]);
220        env::set_var("CC", cc);
221
222        let mut b = BuildEnv::new(t.to_owned());
223
224        assert_eq!(b.var_str("CC"), Some(Ok(cc.to_owned())));
225        let used_env_vars: Vec<_> = b.used_env_vars().collect();
226        assert_eq!(
227            &used_env_vars[..],
228            [
229                "CC",
230                "CC_this-is-a-target",
231                "CC_this_is_a_target",
232                "HOST_CC"
233            ]
234        );
235        clear(t, &["CC"]);
236    }
237
238    fn exact_target() {
239        let t = "this-is-a-target";
240        let cc = "a-cc-value";
241        clear(t, &["CC"]);
242
243        env::set_var("CC", "notThis");
244        env::set_var("HOST_CC", "not-this");
245        env::set_var(format!("CC_{}", t), cc);
246
247        let mut b = BuildEnv::new(t.to_owned());
248
249        assert_eq!(b.var_str("CC"), Some(Ok(cc.to_owned())));
250        let used_env_vars: Vec<_> = b.used_env_vars().collect();
251        assert_eq!(&used_env_vars[..], ["CC_this-is-a-target"]);
252        clear(t, &["CC"]);
253    }
254
255    fn underscore_target() {
256        let t = "this-is-a-target";
257        let cc = "a-cc-value";
258        clear(t, &["CC"]);
259
260        env::set_var("CC", "notThis");
261        env::set_var("HOST_CC", "not-this");
262        env::set_var("CC_this_is_a_target", cc);
263
264        let mut b = BuildEnv::new(t.to_owned());
265
266        assert_eq!(b.var_str("CC"), Some(Ok(cc.to_owned())));
267        let used_env_vars: Vec<_> = b.used_env_vars().collect();
268        assert_eq!(
269            &used_env_vars[..],
270            ["CC_this-is-a-target", "CC_this_is_a_target"]
271        );
272        clear(t, &["CC"]);
273    }
274
275    fn v_host() {
276        let t = "this-is-a-target";
277        let cc = "a-cc-value";
278        clear(t, &["CC"]);
279
280        env::set_var("CC", "not-this-value");
281        env::set_var("HOST_CC", cc);
282
283        let mut b = BuildEnv::new(t.to_owned());
284
285        assert_eq!(b.var_str("CC"), Some(Ok(cc.to_owned())));
286        let used_env_vars: Vec<_> = b.used_env_vars().collect();
287        assert_eq!(
288            &used_env_vars[..],
289            ["CC_this-is-a-target", "CC_this_is_a_target", "HOST_CC"]
290        );
291        clear(t, &["CC"]);
292    }
293
294    fn v_target() {
295        let t = "this-is-a-target";
296        let t2 = "some-target";
297        let cc = "a-cc-value";
298        clear(t, &["CC"]);
299        clear(t2, &["CC"]);
300
301        env::set_var("CC", "not-this-value");
302        env::set_var("HOST_CC", "not this!");
303        env::set_var("TARGET_CC", cc);
304        env::set_var(format!("CC_{}", t), "not this either");
305
306        let mut b = BuildEnv::new_cross(t.to_owned(), t2.to_owned());
307
308        assert_eq!(b.var_str("CC"), Some(Ok(cc.to_owned())));
309        let used_env_vars: Vec<_> = b.used_env_vars().collect();
310        assert_eq!(
311            &used_env_vars[..],
312            ["CC_some-target", "CC_some_target", "TARGET_CC"]
313        );
314        clear(t, &["CC"]);
315    }
316
317    /* tests are only run in seperate threads, and seperate threads share environment between them.
318     * This causes our tests to fail when run concurrently.
319     *
320     * Workaround this for now by explicitly running them sequentially. Correct fix is probably to
321     * provide a "virtual" environment of sorts.
322     */
323    #[test]
324    fn all() {
325        most_general();
326        exact_target();
327        underscore_target();
328        v_host();
329        v_target();
330    }
331}