polykit_core/remote_cache/
mod.rs1mod artifact;
4mod backend;
5mod cache_key;
6mod config;
7mod filesystem;
8mod http;
9mod integrity;
10
11pub use artifact::Artifact;
12pub use backend::{BackendError, RemoteCacheBackend};
13pub use cache_key::{detect_toolchain_version, CacheKey, CacheKeyBuilder};
14pub use config::RemoteCacheConfig;
15pub use filesystem::FilesystemBackend;
16pub use http::HttpBackend;
17pub use integrity::ArtifactVerifier;
18
19use crate::error::Result;
20use crate::graph::DependencyGraph;
21use crate::package::Package;
22
23pub struct RemoteCache {
27 backend: Box<dyn RemoteCacheBackend>,
28 config: RemoteCacheConfig,
29}
30
31impl RemoteCache {
32 pub fn new(backend: Box<dyn RemoteCacheBackend>, config: RemoteCacheConfig) -> Self {
34 Self { backend, config }
35 }
36
37 pub fn from_config(config: RemoteCacheConfig) -> Result<Self> {
45 let backend: Box<dyn RemoteCacheBackend> = if config.is_http() {
46 Box::new(HttpBackend::new(&config)?)
47 } else {
48 Box::new(FilesystemBackend::new(&config.url)?)
49 };
50
51 Ok(Self::new(backend, config))
52 }
53
54 pub fn disabled() -> Self {
56 Self {
57 backend: Box::new(DisabledBackend),
58 config: RemoteCacheConfig::default(),
59 }
60 }
61
62 pub fn is_enabled(&self) -> bool {
64 !self.config.url.is_empty()
65 }
66
67 pub async fn fetch_artifact(&self, key: &CacheKey) -> Result<Option<Artifact>> {
81 if !self.is_enabled() {
82 return Ok(None);
83 }
84
85 self.backend.fetch_artifact(key).await
86 }
87
88 pub async fn upload_artifact(&self, key: &CacheKey, artifact: &Artifact) -> Result<()> {
99 if !self.is_enabled() || self.config.read_only {
100 return Ok(());
101 }
102
103 self.backend.upload_artifact(key, artifact).await
104 }
105
106 pub async fn has_artifact(&self, key: &CacheKey) -> Result<bool> {
120 if !self.is_enabled() {
121 return Ok(false);
122 }
123
124 self.backend.has_artifact(key).await
125 }
126
127 pub async fn build_cache_key(
141 &self,
142 package: &Package,
143 task_name: &str,
144 command: &str,
145 graph: &DependencyGraph,
146 package_path: &std::path::Path,
147 ) -> Result<CacheKey> {
148 use std::collections::BTreeMap;
149 use std::env;
150 use std::fs;
151 use sha2::{Digest, Sha256};
152 use walkdir::WalkDir;
153
154 let deps = graph.dependencies(&package.name).unwrap_or_default();
156 let mut dep_hash_input = format!("{}:{}", package.name, task_name);
157 for dep in &deps {
158 dep_hash_input.push_str(&format!(":{}", dep));
159 }
160 let mut dep_hasher = Sha256::new();
161 dep_hasher.update(dep_hash_input.as_bytes());
162 let dependency_graph_hash = format!("{:x}", dep_hasher.finalize());
163
164 let mut env_vars = BTreeMap::new();
166 for var_name in &self.config.env_vars {
167 if let Ok(value) = env::var(var_name) {
168 env_vars.insert(var_name.clone(), value);
169 }
170 }
171
172 let mut input_file_hashes = rustc_hash::FxHashMap::default();
174 if !self.config.input_files.is_empty() {
175 for pattern in &self.config.input_files {
176 let pattern_path = package_path.join(pattern);
178 if pattern_path.exists() {
179 if pattern_path.is_file() {
180 if let Ok(content) = fs::read(&pattern_path) {
181 let mut hasher = Sha256::new();
182 hasher.update(&content);
183 let hash = format!("{:x}", hasher.finalize());
184 input_file_hashes.insert(
185 pattern_path
186 .strip_prefix(package_path)
187 .unwrap_or(&pattern_path)
188 .to_path_buf(),
189 hash,
190 );
191 }
192 } else if pattern_path.is_dir() {
193 for entry in WalkDir::new(&pattern_path).into_iter().flatten() {
195 if entry.file_type().is_file() {
196 if let Ok(content) = fs::read(entry.path()) {
197 let mut hasher = Sha256::new();
198 hasher.update(&content);
199 let hash = format!("{:x}", hasher.finalize());
200 if let Ok(relative) = entry.path().strip_prefix(package_path) {
201 input_file_hashes.insert(relative.to_path_buf(), hash);
202 }
203 }
204 }
205 }
206 }
207 }
208 }
209 }
210
211 let toolchain_version = detect_toolchain_version(package.language)?;
213
214 let package_path_str = package_path.to_string_lossy();
216 let mut package_hasher = Sha256::new();
217 package_hasher.update(package_path_str.as_bytes());
218 let package_path_hash = format!("{:x}", package_hasher.finalize())[..8].to_string();
219 let package_id = format!("{}-{}", package.name, package_path_hash);
220
221 CacheKey::builder()
222 .package_id(package_id)
223 .task_name(task_name.to_string())
224 .command(command.to_string())
225 .env_vars(env_vars)
226 .input_files(input_file_hashes)
227 .dependency_graph_hash(dependency_graph_hash)
228 .toolchain_version(toolchain_version)
229 .build()
230 }
231
232 pub fn config(&self) -> &RemoteCacheConfig {
234 &self.config
235 }
236}
237
238struct DisabledBackend;
240
241#[async_trait::async_trait]
242impl RemoteCacheBackend for DisabledBackend {
243 async fn upload_artifact(&self, _key: &CacheKey, _artifact: &Artifact) -> Result<()> {
244 Ok(())
245 }
246
247 async fn fetch_artifact(&self, _key: &CacheKey) -> Result<Option<Artifact>> {
248 Ok(None)
249 }
250
251 async fn has_artifact(&self, _key: &CacheKey) -> Result<bool> {
252 Ok(false)
253 }
254}