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}