waterui_cli/toolchain/
cmake.rs1use std::path::PathBuf;
4
5use crate::{
6 brew::Brew,
7 toolchain::linux::{
8 LinuxPackageManagerError, has_supported_package_manager, install_named_packages,
9 },
10 toolchain::winget::{WingetInstallError, ensure_package_installed},
11 toolchain::{Host, Installation, Toolchain, ToolchainError},
12 utils::CommandError,
13};
14
15#[derive(Debug, Clone, Default)]
17pub struct Cmake {}
18
19impl Cmake {
20 pub async fn path(&self, host: &Host) -> Result<PathBuf, which::Error> {
25 host.which("cmake").await
26 }
27}
28
29impl Toolchain for Cmake {
30 type Installation = CmakeInstallation;
31
32 async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
33 if host.which("cmake").await.is_ok() {
36 Ok(())
37 } else if cfg!(target_os = "windows") {
38 if host.which("winget").await.is_ok() {
39 Err(ToolchainError::fixable(CmakeInstallation))
40 } else {
41 Err(ToolchainError::unfixable(
42 "CMake not found and winget is unavailable",
43 "Install Microsoft App Installer to provide winget, or install CMake manually and ensure `cmake` is available in PATH.",
44 ))
45 }
46 } else if cfg!(target_os = "macos") {
47 if host.which("brew").await.is_ok() {
48 Err(ToolchainError::fixable(CmakeInstallation))
49 } else {
50 Err(ToolchainError::unfixable(
51 "CMake not found and Homebrew is unavailable",
52 "Install Homebrew to enable automatic fixes, or install CMake manually and ensure `cmake` is available in PATH.",
53 ))
54 }
55 } else if cfg!(target_os = "linux") {
56 if has_supported_package_manager(host).await {
57 Err(ToolchainError::fixable(CmakeInstallation))
58 } else {
59 Err(ToolchainError::unfixable(
60 "CMake is missing and no supported package manager was found",
61 "Install CMake manually and ensure `cmake` is available in PATH.",
62 ))
63 }
64 } else {
65 Err(ToolchainError::unfixable(
66 "CMake not found",
67 "Install CMake manually for your platform and ensure `cmake` is available in PATH.",
68 ))
69 }
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct CmakeInstallation;
76
77#[derive(Debug, thiserror::Error)]
79pub enum FailToInstallCmake {
80 #[error("Homebrew not found. Please install Homebrew to proceed.")]
82 BrewNotFound,
83
84 #[error("Failed to install CMake: {0}")]
86 Command(#[from] CommandError),
87
88 #[error(
90 "winget is required for automatic CMake installation on Windows. Install App Installer and retry."
91 )]
92 WingetNotFound,
93
94 #[error("Failed to install CMake via winget: {0}")]
96 WingetInstallFailed(String),
97
98 #[error(
100 "No supported Linux package manager found (apt-get, dnf, pacman, zypper, apk). Install CMake manually."
101 )]
102 UnsupportedPackageManager,
103
104 #[error(
106 "Automatic installation of CMake is not supported on this platform. Please install CMake manually."
107 )]
108 UnsupportedPlatform,
109}
110
111impl Installation for CmakeInstallation {
112 type Error = FailToInstallCmake;
113
114 async fn install(&self, host: &Host) -> Result<(), Self::Error> {
115 if cfg!(target_os = "macos") {
116 let brew = Brew::default();
117
118 brew.check(host)
119 .await
120 .map_err(|_| FailToInstallCmake::BrewNotFound)?;
121 brew.install(host, "cmake").await?;
122
123 Ok(())
124 } else if cfg!(target_os = "windows") {
125 ensure_package_installed(host, "Kitware.CMake")
126 .await
127 .map_err(map_winget_error_for_cmake)
128 } else if cfg!(target_os = "linux") {
129 install_named_packages(host, &["cmake"])
130 .await
131 .map_err(map_linux_error_for_cmake)
132 } else {
133 Err(FailToInstallCmake::UnsupportedPlatform)
134 }
135 }
136}
137
138fn map_linux_error_for_cmake(error: LinuxPackageManagerError) -> FailToInstallCmake {
139 match error {
140 LinuxPackageManagerError::UnsupportedPackageManager => {
141 FailToInstallCmake::UnsupportedPackageManager
142 }
143 LinuxPackageManagerError::Command(source) => FailToInstallCmake::Command(source),
144 }
145}
146
147fn map_winget_error_for_cmake(error: WingetInstallError) -> FailToInstallCmake {
148 match error {
149 WingetInstallError::WingetNotFound => FailToInstallCmake::WingetNotFound,
150 WingetInstallError::CommandFailed(err) => {
151 FailToInstallCmake::WingetInstallFailed(err.to_string())
152 }
153 WingetInstallError::NotInstalled { package_id } => {
154 FailToInstallCmake::WingetInstallFailed(format!(
155 "Package `{package_id}` is still missing after winget install; verify winget sources and retry."
156 ))
157 }
158 }
159}
160
161#[cfg(test)]
162mod host_tests {
163 use super::{Cmake, CmakeInstallation};
164 use crate::toolchain::testing::TestMachine;
165 use crate::toolchain::{Installation, Toolchain, ToolchainError};
166
167 fn check(machine: &TestMachine) -> Result<(), ToolchainError<CmakeInstallation>> {
168 let host = machine.host(Vec::<(String, String)>::new());
169 smol::block_on(Cmake::default().check(&host))
170 }
171
172 #[test]
173 fn ok_when_cmake_on_path() {
174 let machine = TestMachine::new();
175 machine.install("cmake");
176 check(&machine).expect("cmake on PATH must be ok");
177 }
178
179 #[test]
180 fn missing_without_installer_is_unfixable() {
181 let machine = TestMachine::new();
182 let result = check(&machine);
183 assert!(
184 matches!(result, Err(ToolchainError::Unfixable(_))),
185 "missing cmake without a package manager must be unfixable: {result:?}"
186 );
187 }
188
189 #[test]
190 fn missing_with_installer_is_fixable() {
191 let machine = TestMachine::new();
192 #[cfg(target_os = "macos")]
193 machine.install("brew");
194 #[cfg(target_os = "linux")]
195 machine.install("apt-get");
196 #[cfg(target_os = "windows")]
197 machine.install("winget");
198 let result = check(&machine);
199 assert!(
200 matches!(result, Err(ToolchainError::Fixable(_))),
201 "missing cmake with a package manager must be fixable: {result:?}"
202 );
203 }
204
205 #[test]
206 #[cfg(any(target_os = "macos", target_os = "linux"))]
207 fn install_runs_the_package_manager() {
208 let machine = TestMachine::new();
209 #[cfg(target_os = "macos")]
210 machine.install("brew");
211 #[cfg(target_os = "linux")]
212 machine.install("apt-get");
213 let host = machine.host(Vec::<(String, String)>::new());
214 smol::block_on(CmakeInstallation.install(&host))
215 .expect("installing cmake through the host's package manager must succeed");
216 }
217
218 #[test]
222 #[cfg(target_os = "windows")]
223 fn install_fails_when_winget_leaves_the_package_missing() {
224 let machine = TestMachine::new();
225 machine.install("winget");
226 let host = machine.host(Vec::<(String, String)>::new());
227 let result = smol::block_on(CmakeInstallation.install(&host));
228 assert!(
229 matches!(
230 result,
231 Err(super::FailToInstallCmake::WingetInstallFailed(_))
232 ),
233 "a package still missing after winget install must be an error: {result:?}"
234 );
235 }
236
237 #[test]
238 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
239 fn install_unsupported_platform() {
240 let machine = TestMachine::new();
241 let host = machine.host(Vec::<(String, String)>::new());
242 let result = smol::block_on(CmakeInstallation.install(&host));
243 assert!(
244 matches!(result, Err(super::FailToInstallCmake::UnsupportedPlatform)),
245 "install on unsupported platforms must fail fast: {result:?}"
246 );
247 }
248}