Skip to main content

rucc_driver/
cache.rs

1//! Where the sysroots and the other generated things are kept.
2//!
3//! Design: `spec/cross-compile/13-distribution.md` section 13.2, which names the directory and the
4//! variables that move it.
5//!
6//! This is the one place that answers the question, and it is here rather than in `rucc-sysroot`
7//! because answering it means reading the environment. `rucc-sysroot` reads nothing: that is what
8//! makes a link line a function of its arguments and what
9//! `spec/cross-compile/02-the-goal.md` claim 5 rests on. So the cache directory is resolved once,
10//! here, and handed down as a path, and the crate below stays a function of what it is given.
11//!
12//! Nothing here creates a directory or looks to see whether one is there. The answer is where a
13//! sysroot for a target would be, which is a question that has an answer before anything has been
14//! downloaded, and saying so is what lets a diagnostic name the directory that is missing.
15
16use std::path::PathBuf;
17
18/// The cache directory, from the environment.
19///
20/// `RUCC_CACHE_DIR` first, because somebody who set it meant it. Then the platform's own place for
21/// a cache that a user can delete without losing anything: `XDG_CACHE_HOME` or `~/.cache` on a
22/// Unix, `LOCALAPPDATA` on Windows, which is where a Windows program is expected to put this and
23/// not where `XDG_CACHE_HOME` would put it.
24///
25/// A temporary directory is the last answer rather than a failure. A machine with no home directory
26/// is a build container, and a build container that cannot link because nothing set `HOME` is worse
27/// than one that downloads a sysroot again on its next run.
28#[must_use]
29pub fn dir() -> PathBuf {
30    resolve(|name| std::env::var(name).ok())
31}
32
33/// The same answer from a function that says what the environment holds.
34///
35/// Split out so that the four cases are testable on one machine. A test that set the real
36/// environment would be a test that changed what the rest of the process sees, and these run in
37/// threads.
38fn resolve(var: impl Fn(&str) -> Option<String>) -> PathBuf {
39    // An empty value is a variable nobody set rather than a request to put the cache at the root of
40    // the filesystem, which is what `RUCC_CACHE_DIR=` in a makefile would otherwise mean.
41    let var = |name: &str| var(name).filter(|value| !value.is_empty());
42    if let Some(set) = var("RUCC_CACHE_DIR") {
43        return PathBuf::from(set);
44    }
45    if cfg!(windows) {
46        if let Some(local) = var("LOCALAPPDATA") {
47            return PathBuf::from(local).join("rucc").join("cache");
48        }
49    }
50    if let Some(xdg) = var("XDG_CACHE_HOME") {
51        return PathBuf::from(xdg).join("rucc");
52    }
53    if let Some(home) = var("HOME") {
54        return PathBuf::from(home).join(".cache").join("rucc");
55    }
56    std::env::temp_dir().join("rucc")
57}
58
59#[cfg(test)]
60mod tests {
61    use super::resolve;
62    use std::path::PathBuf;
63
64    /// An environment holding exactly these pairs.
65    fn env<'a>(pairs: &'a [(&'a str, &'a str)]) -> impl Fn(&str) -> Option<String> + 'a {
66        move |name| pairs.iter().find(|(key, _)| *key == name).map(|(_, value)| (*value).to_owned())
67    }
68
69    #[test]
70    fn the_variable_that_names_it_outright_wins() {
71        let dir = resolve(env(&[
72            ("RUCC_CACHE_DIR", "/build/cache"),
73            ("XDG_CACHE_HOME", "/home/a/.cache"),
74            ("HOME", "/home/a"),
75        ]));
76        assert_eq!(dir, PathBuf::from("/build/cache"));
77    }
78
79    #[test]
80    fn then_the_platforms_own_place_for_a_cache() {
81        let dir = resolve(env(&[("XDG_CACHE_HOME", "/home/a/.cache"), ("HOME", "/home/a")]));
82        assert_eq!(dir, PathBuf::from("/home/a/.cache/rucc"));
83    }
84
85    #[test]
86    #[cfg(windows)]
87    fn on_windows_it_is_where_a_windows_program_keeps_a_cache() {
88        // Not `~/.cache`, which is a Unix convention, and not the roaming profile either, because a
89        // cache that is copied between machines by a domain policy is a cache nobody wanted.
90        let dir = resolve(env(&[("LOCALAPPDATA", r"C:\Users\a\AppData\Local")]));
91        assert_eq!(dir, PathBuf::from(r"C:\Users\a\AppData\Local\rucc\cache"));
92    }
93
94    #[test]
95    fn then_the_home_directory() {
96        let dir = resolve(env(&[("HOME", "/home/a")]));
97        assert_eq!(dir, PathBuf::from("/home/a/.cache/rucc"));
98    }
99
100    #[test]
101    fn and_a_machine_with_nothing_set_still_gets_an_answer() {
102        // A build container with no `HOME`. It links, and it downloads again next time, which is
103        // the right way round for the two costs.
104        let dir = resolve(env(&[]));
105        assert!(dir.ends_with("rucc"), "{}", dir.display());
106        assert!(dir.is_absolute(), "{}", dir.display());
107    }
108
109    #[test]
110    fn an_empty_variable_is_not_an_answer() {
111        // `RUCC_CACHE_DIR=` is what a makefile that forwards a variable it was not given looks
112        // like, and taking it literally would put the cache at the root of the filesystem.
113        let dir = resolve(env(&[("RUCC_CACHE_DIR", ""), ("HOME", "/home/a")]));
114        assert_eq!(dir, PathBuf::from("/home/a/.cache/rucc"));
115    }
116}