Skip to main content

dealer/
toolchain.rs

1use crate::backend::{RustBackend, ToolSource};
2use crate::compiler::PikoExecutableBackend;
3use crate::state::{self, DealerState};
4use std::env;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
8pub(crate) struct ToolchainEnv {
9    dealer_home: Option<PathBuf>,
10    user_home: Option<PathBuf>,
11}
12
13impl ToolchainEnv {
14    pub(crate) fn from_process_env() -> Self {
15        Self {
16            dealer_home: None,
17            user_home: directories::BaseDirs::new().map(|dirs| dirs.home_dir().to_path_buf()),
18        }
19    }
20
21    #[cfg(test)]
22    pub(crate) fn for_test(dealer_home: Option<PathBuf>, user_home: Option<PathBuf>) -> Self {
23        Self {
24            dealer_home,
25            user_home,
26        }
27    }
28
29    fn dealer_home(&self, workspace_root: &Path) -> PathBuf {
30        self.dealer_home
31            .clone()
32            .unwrap_or_else(|| state::resolve_dealer_home(self.user_home.clone(), workspace_root))
33    }
34}
35
36#[derive(Debug, Clone)]
37pub(crate) struct ToolchainSelection {
38    pub(crate) dealer_home: PathBuf,
39    pub(crate) version: String,
40    pub(crate) toolchain_dir: PathBuf,
41    pub(crate) rust_backend_id: String,
42    pub(crate) rust_backend_dir: PathBuf,
43    pub(crate) backend: RustBackend,
44    pub(crate) piko_path: PathBuf,
45    pub(crate) piko_source: ToolSource,
46    pub(crate) rusttime_path: PathBuf,
47    pub(crate) rusttime_source: ToolSource,
48}
49
50impl ToolchainSelection {
51    pub(crate) fn discover(workspace_root: &Path, toolchain_env: &ToolchainEnv) -> Self {
52        let dealer_home = toolchain_env.dealer_home(workspace_root);
53        let state = DealerState::from_home(dealer_home.clone());
54        let version = state.active_version();
55        let rust_backend_id = state.active_rust_backend();
56        let toolchain_dir = state.toolchain_dir(&version);
57        let rust_backend_dir = state.rust_backend_dir(&rust_backend_id);
58
59        let pinned_cargo = rust_backend_dir
60            .join("bin")
61            .join(format!("cargo{}", env::consts::EXE_SUFFIX));
62        let backend = if pinned_cargo.is_file() {
63            RustBackend::pinned_toolchain(pinned_cargo)
64        } else {
65            RustBackend::development_system()
66        };
67
68        let xtazy_toolchain_ready = state::toolchain_is_complete(&toolchain_dir);
69        let pinned_piko = toolchain_dir.join(format!("piko{}", env::consts::EXE_SUFFIX));
70        let pinned_rusttime = toolchain_dir.join("rusttime");
71
72        let (piko_path, piko_source) = if xtazy_toolchain_ready {
73            (pinned_piko, ToolSource::PinnedToolchain)
74        } else {
75            (
76                workspace_root
77                    .join("target")
78                    .join("debug")
79                    .join(format!("piko{}", env::consts::EXE_SUFFIX)),
80                ToolSource::DevelopmentFallback,
81            )
82        };
83
84        let (rusttime_path, rusttime_source) = if xtazy_toolchain_ready {
85            (pinned_rusttime, ToolSource::PinnedToolchain)
86        } else {
87            (
88                workspace_root.join("xtazy-std"),
89                ToolSource::DevelopmentFallback,
90            )
91        };
92
93        Self {
94            dealer_home,
95            version,
96            toolchain_dir,
97            rust_backend_id,
98            rust_backend_dir,
99            backend,
100            piko_path,
101            piko_source,
102            rusttime_path,
103            rusttime_source,
104        }
105    }
106
107    pub(crate) fn backend_source(&self) -> ToolSource {
108        self.backend.source
109    }
110
111    pub(crate) fn compiler_backend(&self) -> PikoExecutableBackend {
112        PikoExecutableBackend {
113            piko_path: self.piko_path.clone(),
114        }
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::backend::ToolSource;
122    use crate::test_support::TempProject;
123    use std::fs;
124
125    fn workspace_root() -> PathBuf {
126        crate::workspace_root()
127    }
128
129    fn make_xtazy_toolchain(state: &DealerState, version: &str) -> (PathBuf, PathBuf, PathBuf) {
130        let dir = state.toolchain_dir(version);
131        let piko = dir.join(format!("piko{}", env::consts::EXE_SUFFIX));
132        let rusttime = dir.join("rusttime");
133        let std = dir.join("std");
134        fs::create_dir_all(&rusttime).expect("rusttime dir should be created");
135        fs::create_dir_all(&std).expect("std dir should be created");
136        fs::write(&piko, "").expect("piko marker should be written");
137        (piko, rusttime, std)
138    }
139
140    fn make_rust_backend(state: &DealerState, backend: &str) -> PathBuf {
141        let cargo = state
142            .rust_backend_dir(backend)
143            .join("bin")
144            .join(format!("cargo{}", env::consts::EXE_SUFFIX));
145        fs::create_dir_all(cargo.parent().unwrap()).expect("rust backend bin dir should be made");
146        fs::write(&cargo, "").expect("cargo marker should be written");
147        cargo
148    }
149
150    #[test]
151    fn discovery_uses_separate_xtazy_and_rust_backend_paths() {
152        let temp = TempProject::new("separate-toolchains");
153        let state = DealerState::for_home(temp.path().join("dealer-home"));
154        state
155            .set_active_version("0.2.0")
156            .expect("active xtazy version should be written");
157        state
158            .set_active_rust_backend("rust-1")
159            .expect("active rust backend should be written");
160        let (piko, rusttime, _) = make_xtazy_toolchain(&state, "0.2.0");
161        let cargo = make_rust_backend(&state, "rust-1");
162
163        let selection = ToolchainSelection::discover(
164            &workspace_root(),
165            &ToolchainEnv::for_test(Some(state.dealer_home.clone()), None),
166        );
167
168        assert_eq!(selection.dealer_home, state.dealer_home);
169        assert_eq!(selection.version, "0.2.0");
170        assert_eq!(selection.toolchain_dir, state.toolchain_dir("0.2.0"));
171        assert_eq!(selection.rust_backend_id, "rust-1");
172        assert_eq!(selection.rust_backend_dir, state.rust_backend_dir("rust-1"));
173        assert_eq!(selection.backend.cargo_path, cargo);
174        assert_eq!(selection.backend.source, ToolSource::PinnedToolchain);
175        assert_eq!(selection.piko_path, piko);
176        assert_eq!(selection.piko_source, ToolSource::PinnedToolchain);
177        assert_eq!(selection.rusttime_path, rusttime);
178        assert_eq!(selection.rusttime_source, ToolSource::PinnedToolchain);
179    }
180
181    #[test]
182    fn discovery_uses_development_fallback_when_xtazy_layout_is_incomplete() {
183        let temp = TempProject::new("incomplete-xtazy-toolchain");
184        let state = DealerState::for_home(temp.path().join("dealer-home"));
185        state
186            .set_active_version("0.2.0")
187            .expect("active version should be written");
188        let incomplete = state.toolchain_dir("0.2.0");
189        fs::create_dir_all(&incomplete).expect("incomplete toolchain dir should be made");
190        fs::write(incomplete.join("piko"), "").expect("piko marker should be written");
191
192        let workspace_root = workspace_root();
193        let selection = ToolchainSelection::discover(
194            &workspace_root,
195            &ToolchainEnv::for_test(Some(state.dealer_home), None),
196        );
197
198        assert_eq!(selection.backend.cargo_path, PathBuf::from("cargo"));
199        assert_eq!(selection.backend.source, ToolSource::DevelopmentFallback);
200        assert_eq!(selection.piko_source, ToolSource::DevelopmentFallback);
201        assert_eq!(selection.rusttime_path, workspace_root.join("xtazy-std"));
202        assert_eq!(selection.rusttime_source, ToolSource::DevelopmentFallback);
203    }
204
205    #[test]
206    fn discovery_uses_active_toolchain_config() {
207        let temp = TempProject::new("active-config-toolchain");
208        let state = DealerState::for_home(temp.path().join("dealer-home"));
209        state
210            .set_active_version("active-test")
211            .expect("active version should be written");
212
213        let selection = ToolchainSelection::discover(
214            &workspace_root(),
215            &ToolchainEnv::for_test(Some(state.dealer_home.clone()), None),
216        );
217
218        assert_eq!(selection.version, "active-test");
219        assert_eq!(selection.toolchain_dir, state.toolchain_dir("active-test"));
220    }
221
222    #[test]
223    fn discovery_uses_system_cargo_when_active_rust_backend_is_missing() {
224        let temp = TempProject::new("missing-rust-backend");
225        let state = DealerState::for_home(temp.path().join("dealer-home"));
226        state
227            .set_active_rust_backend("missing-rust")
228            .expect("active rust backend should be written");
229
230        let selection = ToolchainSelection::discover(
231            &workspace_root(),
232            &ToolchainEnv::for_test(Some(state.dealer_home.clone()), None),
233        );
234
235        assert_eq!(selection.rust_backend_id, "missing-rust");
236        assert_eq!(
237            selection.rust_backend_dir,
238            state.rust_backend_dir("missing-rust")
239        );
240        assert_eq!(selection.backend.cargo_path, PathBuf::from("cargo"));
241        assert_eq!(selection.backend.source, ToolSource::DevelopmentFallback);
242    }
243}