Skip to main content

ghactions_toolcache/
cache.rs

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