Skip to main content

waterui_cli/toolchain/
sccache.rs

1//! Toolchain support for `sccache` - shared compilation cache.
2
3use std::path::{Path, PathBuf};
4
5use smol::process::Command;
6
7use crate::{
8    brew::Brew,
9    toolchain::linux::{
10        LinuxPackageManagerError, has_supported_package_manager, install_named_packages,
11    },
12    toolchain::winget::{WingetInstallError, ensure_package_installed},
13    toolchain::{Host, Installation, Toolchain, ToolchainError},
14    utils::{CommandError, sccache_install_hint},
15};
16
17/// Route a Cargo invocation's compiles through `sccache`.
18///
19/// Caching only bites because generated-crate builds also disable incremental
20/// compilation — Cargo does not pass `-C incremental` to registry dependencies but does
21/// pass it to every *path* dependency, which for a `WaterUI` build is the entire
22/// framework, and `sccache` refuses to cache an incremental compile. That setting lives
23/// in [`crate::build::configure_generated_crate_compilation`] rather than here, because
24/// it must not depend on whether a machine happens to have `sccache` installed: it
25/// changes the compiled ABI, and two builds in one flow have to agree on it.
26pub fn configure_compilation_cache(command: &mut Command, sccache_path: &Path) {
27    command.env("RUSTC_WRAPPER", sccache_path);
28}
29
30/// Toolchain for `sccache` - a shared compilation cache for Rust.
31///
32/// sccache is optional but significantly improves build times by caching
33/// compiled artifacts across builds and projects.
34#[derive(Debug, Clone, Default)]
35pub struct Sccache;
36
37impl Sccache {
38    /// Get the path to the `sccache` executable if available.
39    ///
40    /// # Errors
41    /// Returns an error if `sccache` is not found in the system PATH.
42    pub async fn path(&self, host: &Host) -> Result<PathBuf, which::Error> {
43        host.which("sccache").await
44    }
45
46    /// Check if sccache is available on `host` without returning an error.
47    pub async fn is_available(&self, host: &Host) -> bool {
48        self.path(host).await.is_ok()
49    }
50}
51
52impl Toolchain for Sccache {
53    type Installation = SccacheInstallation;
54
55    async fn check(&self, host: &Host) -> Result<(), ToolchainError<Self::Installation>> {
56        if host.which("sccache").await.is_ok() {
57            Ok(())
58        } else if cfg!(target_os = "windows") {
59            if host.which("winget").await.is_ok() {
60                Err(ToolchainError::fixable(SccacheInstallation))
61            } else {
62                Err(ToolchainError::unfixable(
63                    "sccache not found and winget is unavailable",
64                    format!(
65                        "Install Microsoft App Installer to provide winget, or install manually with {}.",
66                        sccache_install_hint()
67                    ),
68                ))
69            }
70        } else if cfg!(target_os = "macos") {
71            if host.which("brew").await.is_ok() {
72                Err(ToolchainError::fixable(SccacheInstallation))
73            } else {
74                Err(ToolchainError::unfixable(
75                    "sccache not found and Homebrew is unavailable",
76                    format!(
77                        "Install Homebrew to enable automatic fixes, or install manually with {}.",
78                        sccache_install_hint()
79                    ),
80                ))
81            }
82        } else if cfg!(target_os = "linux") {
83            if has_supported_package_manager(host).await {
84                Err(ToolchainError::fixable(SccacheInstallation))
85            } else {
86                Err(ToolchainError::unfixable(
87                    "sccache is missing and no supported package manager was found",
88                    format!("Install manually with {}", sccache_install_hint()),
89                ))
90            }
91        } else {
92            Err(ToolchainError::unfixable(
93                "sccache not found",
94                format!(
95                    "Install sccache manually ({}) and ensure `sccache` is available in PATH.",
96                    sccache_install_hint()
97                ),
98            ))
99        }
100    }
101}
102
103/// Installation plan for `sccache`.
104#[derive(Debug, Clone)]
105pub struct SccacheInstallation;
106
107/// Errors that can occur during `sccache` installation.
108#[derive(Debug, thiserror::Error)]
109pub enum FailToInstallSccache {
110    /// Homebrew not found error.
111    #[error("Homebrew not found. Please install Homebrew to proceed.")]
112    BrewNotFound,
113
114    /// An installation command failed.
115    #[error("Failed to install sccache: {0}")]
116    Command(#[from] CommandError),
117
118    /// winget is required for Windows automatic installation.
119    #[error(
120        "winget is required for automatic sccache installation on Windows. Install App Installer and retry."
121    )]
122    WingetNotFound,
123
124    /// Windows installation via winget failed.
125    #[error("Failed to install sccache via winget: {0}")]
126    WingetInstallFailed(String),
127
128    /// Linux package manager is required for automatic installation.
129    #[error(
130        "No supported Linux package manager found (apt-get, dnf, pacman, zypper, apk). Install sccache manually."
131    )]
132    UnsupportedPackageManager,
133
134    /// Unsupported platform error.
135    #[error(
136        "Automatic installation of sccache is not supported on this platform. \
137         Install manually with: cargo install sccache"
138    )]
139    UnsupportedPlatform,
140}
141
142impl Installation for SccacheInstallation {
143    type Error = FailToInstallSccache;
144
145    async fn install(&self, host: &Host) -> Result<(), Self::Error> {
146        if cfg!(target_os = "macos") {
147            let brew = Brew::default();
148
149            brew.check(host)
150                .await
151                .map_err(|_| FailToInstallSccache::BrewNotFound)?;
152            brew.install(host, "sccache").await?;
153
154            Ok(())
155        } else if cfg!(target_os = "windows") {
156            ensure_package_installed(host, "Mozilla.sccache")
157                .await
158                .map_err(map_winget_error_for_sccache)
159        } else if cfg!(target_os = "linux") {
160            install_named_packages(host, &["sccache"])
161                .await
162                .map_err(map_linux_error_for_sccache)
163        } else {
164            Err(FailToInstallSccache::UnsupportedPlatform)
165        }
166    }
167}
168
169fn map_linux_error_for_sccache(error: LinuxPackageManagerError) -> FailToInstallSccache {
170    match error {
171        LinuxPackageManagerError::UnsupportedPackageManager => {
172            FailToInstallSccache::UnsupportedPackageManager
173        }
174        LinuxPackageManagerError::Command(source) => FailToInstallSccache::Command(source),
175    }
176}
177
178fn map_winget_error_for_sccache(error: WingetInstallError) -> FailToInstallSccache {
179    match error {
180        WingetInstallError::WingetNotFound => FailToInstallSccache::WingetNotFound,
181        WingetInstallError::CommandFailed(err) => {
182            FailToInstallSccache::WingetInstallFailed(err.to_string())
183        }
184        WingetInstallError::NotInstalled { package_id } => {
185            FailToInstallSccache::WingetInstallFailed(format!(
186                "Package `{package_id}` is still missing after winget install; verify winget sources and retry."
187            ))
188        }
189    }
190}
191
192#[cfg(test)]
193mod host_tests {
194    use super::{Sccache, SccacheInstallation};
195    use crate::toolchain::testing::TestMachine;
196    use crate::toolchain::{Toolchain, ToolchainError};
197
198    fn check(machine: &TestMachine) -> Result<(), ToolchainError<SccacheInstallation>> {
199        let host = machine.host(Vec::<(String, String)>::new());
200        smol::block_on(Sccache.check(&host))
201    }
202
203    #[test]
204    fn ok_when_sccache_on_path() {
205        let machine = TestMachine::new();
206        machine.install("sccache");
207        check(&machine).expect("sccache on PATH must be ok");
208    }
209
210    #[test]
211    fn missing_without_installer_is_unfixable() {
212        let machine = TestMachine::new();
213        let result = check(&machine);
214        assert!(
215            matches!(result, Err(ToolchainError::Unfixable(_))),
216            "missing sccache without a package manager must be unfixable: {result:?}"
217        );
218    }
219
220    #[test]
221    fn missing_with_installer_is_fixable() {
222        let machine = TestMachine::new();
223        #[cfg(target_os = "macos")]
224        machine.install("brew");
225        #[cfg(target_os = "linux")]
226        machine.install("apt-get");
227        #[cfg(target_os = "windows")]
228        machine.install("winget");
229        let result = check(&machine);
230        assert!(
231            matches!(result, Err(ToolchainError::Fixable(_))),
232            "missing sccache with a package manager must be fixable: {result:?}"
233        );
234    }
235}