|
1 | | -use std::path::Path; |
| 1 | +use std::{collections::HashMap, path::Path}; |
2 | 2 |
|
3 | 3 | use anyhow::Context; |
| 4 | +use futures::future::try_join_all; |
4 | 5 | use spin_common::ui::quoted_path; |
5 | 6 | use spin_manifest::schema::v2::TargetEnvironmentRef; |
6 | 7 |
|
7 | 8 | const DEFAULT_REGISTRY: &str = "fermyon.com"; |
8 | 9 |
|
9 | | -/// Loads the given `TargetEnvironment` from a registry. |
10 | | -pub async fn load_environment( |
| 10 | +/// Serialisation format for the lockfile: registry -> { name -> digest } |
| 11 | +#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] |
| 12 | +struct TargetEnvironmentLockfile(HashMap<String, HashMap<String, String>>); |
| 13 | + |
| 14 | +impl TargetEnvironmentLockfile { |
| 15 | + fn digest(&self, registry: &str, env_id: &str) -> Option<&str> { |
| 16 | + self.0 |
| 17 | + .get(registry) |
| 18 | + .and_then(|m| m.get(env_id)) |
| 19 | + .map(|s| s.as_str()) |
| 20 | + } |
| 21 | + |
| 22 | + fn set_digest(&mut self, registry: &str, env_id: &str, digest: &str) { |
| 23 | + match self.0.get_mut(registry) { |
| 24 | + Some(map) => { |
| 25 | + map.insert(env_id.to_string(), digest.to_string()); |
| 26 | + } |
| 27 | + None => { |
| 28 | + let map = vec![(env_id.to_string(), digest.to_string())] |
| 29 | + .into_iter() |
| 30 | + .collect(); |
| 31 | + self.0.insert(registry.to_string(), map); |
| 32 | + } |
| 33 | + } |
| 34 | + } |
| 35 | +} |
| 36 | + |
| 37 | +/// Load all the listed environments from their registries or paths. |
| 38 | +/// Registry data will be cached, with a lockfile under `.spin` mapping |
| 39 | +/// environment IDs to digests (to allow cache lookup without needing |
| 40 | +/// to fetch the digest from the registry). |
| 41 | +pub async fn load_environments( |
| 42 | + env_ids: &[TargetEnvironmentRef], |
| 43 | + cache_root: Option<std::path::PathBuf>, |
| 44 | + app_dir: &std::path::Path, |
| 45 | +) -> anyhow::Result<Vec<TargetEnvironment>> { |
| 46 | + if env_ids.is_empty() { |
| 47 | + return Ok(Default::default()); |
| 48 | + } |
| 49 | + |
| 50 | + let cache = spin_loader::cache::Cache::new(cache_root) |
| 51 | + .await |
| 52 | + .context("Unable to create cache")?; |
| 53 | + let lockfile_dir = app_dir.join(".spin"); |
| 54 | + let lockfile_path = lockfile_dir.join("target-environments.lock"); |
| 55 | + |
| 56 | + let orig_lockfile: TargetEnvironmentLockfile = tokio::fs::read_to_string(&lockfile_path) |
| 57 | + .await |
| 58 | + .ok() |
| 59 | + .and_then(|s| serde_json::from_str(&s).ok()) |
| 60 | + .unwrap_or_default(); |
| 61 | + let lockfile = std::sync::Arc::new(tokio::sync::RwLock::new(orig_lockfile.clone())); |
| 62 | + |
| 63 | + let envs = try_join_all( |
| 64 | + env_ids |
| 65 | + .iter() |
| 66 | + .map(|e| load_environment(e, &cache, &lockfile)), |
| 67 | + ) |
| 68 | + .await?; |
| 69 | + |
| 70 | + let final_lockfile = &*lockfile.read().await; |
| 71 | + if *final_lockfile != orig_lockfile { |
| 72 | + if let Ok(lockfile_json) = serde_json::to_string_pretty(&final_lockfile) { |
| 73 | + _ = tokio::fs::create_dir_all(lockfile_dir).await; |
| 74 | + _ = tokio::fs::write(&lockfile_path, lockfile_json).await; // failure to update lockfile is not an error |
| 75 | + } |
| 76 | + } |
| 77 | + |
| 78 | + Ok(envs) |
| 79 | +} |
| 80 | + |
| 81 | +/// Loads the given `TargetEnvironment` from a registry or directory. |
| 82 | +async fn load_environment( |
11 | 83 | env_id: &TargetEnvironmentRef, |
12 | 84 | cache: &spin_loader::cache::Cache, |
| 85 | + lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>, |
13 | 86 | ) -> anyhow::Result<TargetEnvironment> { |
14 | 87 | match env_id { |
15 | 88 | TargetEnvironmentRef::DefaultRegistry(package) => { |
16 | | - load_environment_from_registry(DEFAULT_REGISTRY, package, cache).await |
| 89 | + load_environment_from_registry(DEFAULT_REGISTRY, package, cache, lockfile).await |
17 | 90 | } |
18 | 91 | TargetEnvironmentRef::Registry { registry, package } => { |
19 | | - load_environment_from_registry(registry, package, cache).await |
| 92 | + load_environment_from_registry(registry, package, cache, lockfile).await |
20 | 93 | } |
21 | 94 | TargetEnvironmentRef::WitDirectory { path } => load_environment_from_dir(path), |
22 | 95 | } |
23 | 96 | } |
24 | 97 |
|
| 98 | +/// Loads the given `TargetEnvironment` from the given registry, or |
| 99 | +/// from cache if available. If the environment is not in cache, the |
| 100 | +/// encoded WIT will be cached, and the in-memory lockfile object |
| 101 | +/// updated. |
25 | 102 | async fn load_environment_from_registry( |
26 | 103 | registry: &str, |
27 | 104 | env_id: &str, |
28 | 105 | cache: &spin_loader::cache::Cache, |
| 106 | + lockfile: &std::sync::Arc<tokio::sync::RwLock<TargetEnvironmentLockfile>>, |
29 | 107 | ) -> anyhow::Result<TargetEnvironment> { |
30 | 108 | use futures_util::TryStreamExt; |
31 | 109 |
|
32 | | - let cache_file = cache.wit_path(registry, env_id); |
33 | | - if cache.wit_path(registry, env_id).exists() { |
34 | | - // Failure to read from cache is not fatal - fall through to origin. |
35 | | - if let Ok(bytes) = tokio::fs::read(cache_file).await { |
36 | | - return TargetEnvironment::from_package_bytes(env_id, bytes); |
| 110 | + if let Some(digest) = lockfile.read().await.digest(registry, env_id) { |
| 111 | + if let Ok(cache_file) = cache.wasm_file(digest) { |
| 112 | + if let Ok(bytes) = tokio::fs::read(&cache_file).await { |
| 113 | + return TargetEnvironment::from_package_bytes(env_id, bytes); |
| 114 | + } |
37 | 115 | } |
38 | 116 | } |
39 | 117 |
|
@@ -76,7 +154,9 @@ async fn load_environment_from_registry( |
76 | 154 | .with_context(|| format!("Failed to get {env_id} package data from registry"))? |
77 | 155 | .to_vec(); |
78 | 156 |
|
79 | | - _ = cache.write_wit(&bytes, registry, env_id).await; // Failure to cache is not fatal |
| 157 | + let digest = release.content_digest.to_string(); |
| 158 | + _ = cache.write_wasm(&bytes, &digest).await; // Failure to cache is not fatal |
| 159 | + lockfile.write().await.set_digest(registry, env_id, &digest); |
80 | 160 |
|
81 | 161 | TargetEnvironment::from_package_bytes(env_id, bytes) |
82 | 162 | } |
|
0 commit comments