Skip to main content

dealer/
state.rs

1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4
5use crate::error::{DealerError, DealerResult};
6
7pub(crate) const DEFAULT_TOOLCHAIN_VERSION: &str = "0.1.0";
8pub(crate) const DEFAULT_RUST_BACKEND: &str = "default";
9
10#[derive(Debug, Clone)]
11pub(crate) struct DealerState {
12    pub(crate) dealer_home: PathBuf,
13}
14
15impl DealerState {
16    pub(crate) fn from_process_env(workspace_root: &Path) -> Self {
17        Self::from_home(resolve_dealer_home(
18            directories::BaseDirs::new().map(|dirs| dirs.home_dir().to_path_buf()),
19            workspace_root,
20        ))
21    }
22
23    pub(crate) fn from_home(dealer_home: PathBuf) -> Self {
24        Self { dealer_home }
25    }
26
27    #[cfg(test)]
28    pub(crate) fn for_home(dealer_home: PathBuf) -> Self {
29        Self::from_home(dealer_home)
30    }
31
32    pub(crate) fn config_dir(&self) -> PathBuf {
33        self.dealer_home.join("config")
34    }
35
36    pub(crate) fn cache_dir(&self) -> PathBuf {
37        self.dealer_home.join("cache")
38    }
39
40    pub(crate) fn xtazy_dir(&self) -> PathBuf {
41        self.dealer_home.join("xtazy")
42    }
43
44    pub(crate) fn rust_dir(&self) -> PathBuf {
45        self.dealer_home.join("rust")
46    }
47
48    pub(crate) fn active_toolchain_file(&self) -> PathBuf {
49        self.config_dir().join("active-toolchain")
50    }
51
52    pub(crate) fn active_rust_backend_file(&self) -> PathBuf {
53        self.config_dir().join("active-rust-backend")
54    }
55
56    pub(crate) fn auto_update_file(&self) -> PathBuf {
57        self.config_dir().join("auto-update")
58    }
59
60    pub(crate) fn ensure_base_dirs(&self) -> DealerResult<()> {
61        create_dir(&self.config_dir())?;
62        create_dir(&self.cache_dir())?;
63        create_dir(&self.xtazy_dir())?;
64        create_dir(&self.rust_dir())?;
65        Ok(())
66    }
67
68    pub(crate) fn active_version(&self) -> String {
69        read_trimmed(&self.active_toolchain_file())
70            .filter(|value| !value.is_empty())
71            .unwrap_or_else(|| DEFAULT_TOOLCHAIN_VERSION.to_string())
72    }
73
74    pub(crate) fn set_active_version(&self, version: &str) -> DealerResult<()> {
75        self.ensure_base_dirs()?;
76        fs::write(self.active_toolchain_file(), format!("{version}\n"))
77            .map_err(|error| DealerError::io(self.active_toolchain_file(), error))
78    }
79
80    pub(crate) fn active_rust_backend(&self) -> String {
81        read_trimmed(&self.active_rust_backend_file())
82            .filter(|value| !value.is_empty())
83            .unwrap_or_else(|| DEFAULT_RUST_BACKEND.to_string())
84    }
85
86    #[cfg(test)]
87    pub(crate) fn set_active_rust_backend(&self, backend: &str) -> DealerResult<()> {
88        self.ensure_base_dirs()?;
89        fs::write(self.active_rust_backend_file(), format!("{backend}\n"))
90            .map_err(|error| DealerError::io(self.active_rust_backend_file(), error))
91    }
92
93    pub(crate) fn auto_update_enabled(&self) -> bool {
94        matches!(
95            read_trimmed(&self.auto_update_file()).as_deref(),
96            Some("enabled")
97        )
98    }
99
100    pub(crate) fn set_auto_update_enabled(&self, enabled: bool) -> DealerResult<()> {
101        self.ensure_base_dirs()?;
102        let value = if enabled { "enabled\n" } else { "disabled\n" };
103        fs::write(self.auto_update_file(), value)
104            .map_err(|error| DealerError::io(self.auto_update_file(), error))
105    }
106
107    pub(crate) fn installed_toolchains(&self) -> DealerResult<Vec<InstalledToolchain>> {
108        let xtazy_dir = self.xtazy_dir();
109        if !xtazy_dir.exists() {
110            return Ok(Vec::new());
111        }
112
113        let mut toolchains = Vec::new();
114        let entries =
115            fs::read_dir(&xtazy_dir).map_err(|error| DealerError::io(&xtazy_dir, error))?;
116        for entry in entries {
117            let entry = entry.map_err(|error| DealerError::io(&xtazy_dir, error))?;
118            let path = entry.path();
119            if !path.is_dir() {
120                continue;
121            }
122            let Some(version) = path.file_name().and_then(|name| name.to_str()) else {
123                continue;
124            };
125            toolchains.push(InstalledToolchain {
126                version: version.to_string(),
127                path: path.clone(),
128                complete: toolchain_is_complete(&path),
129            });
130        }
131        toolchains.sort_by(|left, right| left.version.cmp(&right.version));
132        Ok(toolchains)
133    }
134
135    pub(crate) fn toolchain_dir(&self, version: &str) -> PathBuf {
136        self.xtazy_dir().join(version)
137    }
138
139    pub(crate) fn rust_backend_dir(&self, backend: &str) -> PathBuf {
140        self.rust_dir().join(backend)
141    }
142
143    pub(crate) fn has_complete_toolchain(&self, version: &str) -> bool {
144        toolchain_is_complete(&self.toolchain_dir(version))
145    }
146
147    pub(crate) fn remove_toolchain(&self, version: &str) -> DealerResult<()> {
148        let path = self.toolchain_dir(version);
149        if path.exists() {
150            fs::remove_dir_all(&path).map_err(|error| DealerError::io(&path, error))?;
151        }
152        Ok(())
153    }
154}
155
156pub(crate) fn resolve_dealer_home(user_home: Option<PathBuf>, workspace_root: &Path) -> PathBuf {
157    user_home
158        .map(|home| home.join(".dealer"))
159        .unwrap_or_else(|| workspace_root.join(".dealer"))
160}
161
162#[derive(Debug, Clone, PartialEq, Eq)]
163pub(crate) struct InstalledToolchain {
164    pub(crate) version: String,
165    pub(crate) path: PathBuf,
166    pub(crate) complete: bool,
167}
168
169pub(crate) fn toolchain_is_complete(toolchain_dir: &Path) -> bool {
170    toolchain_dir.join("piko").is_file()
171        && toolchain_dir.join("rusttime").is_dir()
172        && toolchain_dir.join("std").is_dir()
173}
174
175fn create_dir(path: &Path) -> DealerResult<()> {
176    fs::create_dir_all(path).map_err(|error| DealerError::io(path, error))
177}
178
179fn read_trimmed(path: &Path) -> Option<String> {
180    match fs::read_to_string(path) {
181        Ok(value) => Some(value.trim().to_string()),
182        Err(error) if error.kind() == io::ErrorKind::NotFound => None,
183        Err(_) => None,
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::test_support::TempProject;
191
192    #[test]
193    fn active_version_defaults_to_mvp_version() {
194        let temp = TempProject::new("state-default");
195        let state = DealerState::for_home(temp.path().join("dealer-home"));
196
197        assert_eq!(state.active_version(), DEFAULT_TOOLCHAIN_VERSION);
198    }
199
200    #[test]
201    fn active_version_round_trips_through_config() {
202        let temp = TempProject::new("state-active");
203        let state = DealerState::for_home(temp.path().join("dealer-home"));
204
205        state
206            .set_active_version("0.2.0")
207            .expect("active version should be written");
208
209        assert_eq!(state.active_version(), "0.2.0");
210        assert!(state.config_dir().is_dir());
211        assert!(state.cache_dir().is_dir());
212        assert!(state.xtazy_dir().is_dir());
213        assert!(state.rust_dir().is_dir());
214    }
215
216    #[test]
217    fn active_rust_backend_round_trips_through_config() {
218        let temp = TempProject::new("state-rust-backend");
219        let state = DealerState::for_home(temp.path().join("dealer-home"));
220
221        assert_eq!(state.active_rust_backend(), DEFAULT_RUST_BACKEND);
222        state
223            .set_active_rust_backend("rust-1")
224            .expect("active rust backend should be written");
225
226        assert_eq!(state.active_rust_backend(), "rust-1");
227    }
228
229    #[test]
230    fn installed_toolchains_mark_complete_layout_complete() {
231        let temp = TempProject::new("state-installed");
232        let state = DealerState::for_home(temp.path().join("dealer-home"));
233        let version_dir = state.toolchain_dir("0.1.0");
234        fs::create_dir_all(&version_dir).expect("toolchain dir should be created");
235        fs::write(version_dir.join("piko"), "").expect("piko should be written");
236        fs::create_dir_all(version_dir.join("rusttime")).expect("rusttime should be created");
237        fs::create_dir_all(version_dir.join("std")).expect("std should be created");
238
239        let installed = state
240            .installed_toolchains()
241            .expect("installed toolchains should be listed");
242
243        assert_eq!(installed.len(), 1);
244        assert_eq!(installed[0].version, "0.1.0");
245        assert!(installed[0].complete);
246    }
247}