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}