Skip to main content

ghactions_toolcache/
cache.rs

1//! Tool Cache
2
3use std::path::PathBuf;
4#[cfg(feature = "download")]
5use std::sync::{Arc, OnceLock};
6
7use super::{Tool, ToolCacheArch, platform::ToolPlatform};
8use crate::ToolCacheError;
9use crate::builder::ToolCacheBuilder;
10
11/// Number of times to retry a download
12pub(crate) const RETRY_COUNT: u8 = 10;
13
14/// Linux and MacOS Tool Cache Paths
15#[cfg(target_family = "unix")]
16const TOOL_CACHE_PATHS: [&str; 3] = [
17    "/opt/hostedtoolcache",
18    "/usr/local/share/toolcache",
19    "/tmp/toolcache",
20];
21/// Windows Tool Cache Paths
22#[cfg(target_family = "windows")]
23const TOOL_CACHE_PATHS: [&str; 3] = [
24    "C:\\hostedtoolcache",
25    "C:\\Program Files\\toolcache",
26    "C:\\tmp\\toolcache",
27];
28
29/// Tool Cache
30#[derive(Debug, Clone)]
31pub struct ToolCache {
32    /// Tool Cache Path
33    pub(crate) tool_cache: PathBuf,
34
35    /// Platform Architecture
36    pub(crate) arch: ToolCacheArch,
37
38    /// Platform (OS)
39    pub(crate) platform: ToolPlatform,
40
41    /// Number of times to retry a download
42    pub(crate) retry_count: u8,
43
44    /// Client to use for downloads
45    #[cfg(feature = "download")]
46    pub(crate) client: Arc<OnceLock<reqwest::Client>>,
47}
48
49impl ToolCache {
50    /// Create a new Tool Cache
51    ///
52    /// This will either use the `RUNNER_TOOL_CACHE` environment variable or
53    /// it will try to find the tool cache in the default locations.
54    ///
55    /// There are 3 default locations:
56    ///  
57    /// - `/opt/hostedtoolcache` (Unix)
58    /// - `/usr/local/share/toolcache` (Unix)
59    /// - `/tmp/toolcache` (Unix)
60    /// - `C:\\hostedtoolcache` (Windows)
61    /// - `C:\\Program Files\\toolcache` (Windows)
62    /// - `C:\\tmp\\toolcache` (Windows)
63    ///
64    /// If no locations are found or writeable, it will create a new tool cache
65    /// in the current directory at `./.toolcache`.
66    ///
67    pub fn new() -> Self {
68        Self::default()
69    }
70
71    /// Create a new ToolCacheBuilder
72    pub fn build() -> ToolCacheBuilder {
73        ToolCacheBuilder::new()
74    }
75
76    /// Get the platform for the tool cache
77    ///
78    /// By default this is set to the current platform of the system.
79    /// You can override this by using the `platform` method on the `ToolCacheBuilder`.
80    pub fn platform(&self) -> ToolPlatform {
81        self.platform
82    }
83
84    /// Get the architecture for the tool cache
85    ///
86    /// By default this is set to the current architecture of the system.
87    /// You can override this by using the `arch` method on the `ToolCacheBuilder`.
88    pub fn arch(&self) -> ToolCacheArch {
89        self.arch
90    }
91
92    /// Get the Tool Cache Path
93    ///
94    /// This is either set by the `RUNNER_TOOL_CACHE` environment variable
95    /// or it is one of the default locations.
96    pub fn get_tool_cache(&self) -> &PathBuf {
97        &self.tool_cache
98    }
99
100    /// Find a tool in the cache
101    pub async fn find(
102        &self,
103        tool: impl Into<String>,
104        version: impl Into<String>,
105    ) -> Result<Tool, ToolCacheError> {
106        match self.platform() {
107            ToolPlatform::Windows => self.find_with_arch(tool, version, ToolCacheArch::X64).await,
108            ToolPlatform::Linux => self.find_with_arch(tool, version, ToolCacheArch::X64).await,
109            ToolPlatform::MacOS => {
110                self.find_with_arch(tool, version, ToolCacheArch::ARM64)
111                    .await
112            }
113            ToolPlatform::Any => self.find_with_arch(tool, version, ToolCacheArch::Any).await,
114        }
115    }
116
117    /// Find all versions of a tool in the cache
118    pub async fn find_all_version(
119        &self,
120        tool: impl Into<String>,
121    ) -> Result<Vec<Tool>, ToolCacheError> {
122        Tool::find(self.get_tool_cache(), tool, "*", ToolCacheArch::Any)
123    }
124
125    /// Find a tool in the cache with a specific architecture
126    pub async fn find_with_arch(
127        &self,
128        tool: impl Into<String>,
129        version: impl Into<String>,
130        arch: impl Into<ToolCacheArch>,
131    ) -> Result<Tool, ToolCacheError> {
132        let tool = tool.into();
133        let version = version.into();
134        let arch = arch.into();
135
136        Tool::find(self.get_tool_cache(), tool.clone(), &version, arch)?
137            .into_iter()
138            .find(|t| t.name() == tool)
139            .ok_or(crate::ToolCacheError::ToolNotFound {
140                name: tool,
141                version,
142                arch: Some(arch),
143            })
144    }
145
146    /// Create a path for the tool in the cache to be used
147    pub fn new_tool_path(&self, tool: impl Into<String>, version: impl Into<String>) -> PathBuf {
148        Tool::tool_path(self.get_tool_cache(), tool, version, self.arch())
149    }
150
151    /// Set the number of times to retry a download (default is 10)
152    #[deprecated(since = "0.17.0", note = "Use the ToolCacheBuilder instead")]
153    pub fn set_retry_count(&mut self, count: u8) {
154        self.retry_count = count;
155    }
156}
157
158/// Get the tool cache path
159pub(crate) fn get_tool_cache_path() -> PathBuf {
160    let tool_cache = std::env::var("RUNNER_TOOL_CACHE")
161        .map(PathBuf::from)
162        .unwrap_or_else(|_| {
163            TOOL_CACHE_PATHS
164                .iter()
165                .find_map(|path| {
166                    let path = PathBuf::from(path);
167                    // Exists and can be written to
168                    if let Err(err) = std::fs::create_dir_all(&path) {
169                        log::trace!("Error creating tool cache dir: {:?}", err);
170                        None
171                    } else {
172                        log::debug!("Using tool cache found at: {:?}", path);
173                        Some(path)
174                    }
175                })
176                .unwrap_or_else(|| PathBuf::from("./.toolcache").canonicalize().unwrap())
177        });
178
179    if !tool_cache.exists() {
180        log::debug!("Creating tool cache at: {:?}", tool_cache);
181        std::fs::create_dir_all(&tool_cache)
182            .unwrap_or_else(|_| panic!("Failed to create tool cache directory: {:?}", tool_cache));
183    }
184    tool_cache
185}
186
187impl From<&str> for ToolCache {
188    fn from(cache: &str) -> Self {
189        let tool_cache = PathBuf::from(cache);
190        if !tool_cache.exists() {
191            panic!("Tool Cache does not exist: {:?}", tool_cache);
192        }
193        Self {
194            tool_cache,
195            ..Default::default()
196        }
197    }
198}
199
200impl From<PathBuf> for ToolCache {
201    fn from(value: PathBuf) -> Self {
202        let tool_cache = value;
203        if !tool_cache.exists() {
204            panic!("Tool Cache does not exist: {:?}", tool_cache);
205        }
206        Self {
207            tool_cache,
208            ..Default::default()
209        }
210    }
211}
212
213impl Default for ToolCache {
214    fn default() -> Self {
215        let tool_cache = get_tool_cache_path();
216
217        Self {
218            tool_cache,
219            retry_count: RETRY_COUNT,
220            arch: match std::env::consts::ARCH {
221                "x86_64" | "amd64" => ToolCacheArch::X64,
222                "aarch64" => ToolCacheArch::ARM64,
223                _ => ToolCacheArch::Any,
224            },
225            platform: ToolPlatform::from_current_os(),
226            #[cfg(feature = "download")]
227            client: Arc::new(OnceLock::new()),
228        }
229    }
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    fn local_toolcache() -> (PathBuf, ToolCache) {
237        // working dir + examples/toolcache
238        let cwd = std::env::current_dir()
239            .unwrap()
240            .join("..")
241            .canonicalize()
242            .unwrap();
243
244        (cwd.clone(), ToolCache::from(cwd.join("examples/toolcache")))
245    }
246
247    #[test]
248    fn test_tool_cache() {
249        // Default
250        let tool_cache = ToolCache::default();
251        if let Ok(env_path) = std::env::var("RUNNER_TOOL_CACHE") {
252            assert_eq!(tool_cache.get_tool_cache(), &PathBuf::from(env_path));
253        } else {
254            assert!(tool_cache.get_tool_cache().exists());
255        }
256    }
257
258    #[tokio::test]
259    async fn test_find_all_version() {
260        let (_cwd, tool_cache) = local_toolcache();
261        let versions = tool_cache.find_all_version("node").await.unwrap();
262        assert!(!versions.is_empty());
263    }
264}