1use std::io::IsTerminal;
2
3use anyhow::{anyhow, Context, Result};
4use semver::{Version, VersionReq};
5use serde::{Deserialize, Serialize};
6use url::Url;
7
8use crate::PluginStore;
9
10#[derive(Serialize, Debug, Deserialize, PartialEq)]
14#[serde(rename_all = "camelCase")]
15pub struct PluginManifest {
16 name: String,
18 #[serde(default, skip_serializing_if = "Option::is_none")]
20 description: Option<String>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 homepage: Option<String>,
24 pub(crate) version: String,
26 pub(crate) spin_compatibility: String,
28 license: String,
30 pub(crate) packages: Vec<PluginPackage>,
32}
33
34impl PluginManifest {
35 pub fn name(&self) -> String {
36 self.name.to_lowercase()
37 }
38 pub fn version(&self) -> &str {
39 &self.version
40 }
41 pub fn license(&self) -> &str {
42 self.license.as_ref()
43 }
44
45 pub fn spin_compatibility(&self) -> String {
46 self.spin_compatibility.clone()
47 }
48
49 pub fn description(&self) -> Option<&str> {
50 self.description.as_deref()
51 }
52
53 pub fn homepage_url(&self) -> Option<Url> {
54 Url::parse(self.homepage.as_deref()?).ok()
55 }
56
57 pub fn has_compatible_package(&self) -> bool {
58 self.packages.iter().any(|p| p.matches_current_os_arch())
59 }
60 pub fn is_compatible_spin_version(&self, spin_version: &str) -> bool {
61 is_version_compatible_enough(&self.spin_compatibility, spin_version).unwrap_or(false)
62 }
63 pub fn is_installed_in(&self, store: &PluginStore) -> bool {
64 match store.read_plugin_manifest(&self.name) {
65 Ok(m) => m.eq(self),
66 Err(_) => false,
67 }
68 }
69
70 pub fn try_version(&self) -> Result<semver::Version, semver::Error> {
71 semver::Version::parse(&self.version)
72 }
73
74 pub fn compare_versions(&self, other: &Self) -> Option<std::cmp::Ordering> {
76 if let Ok(this_version) = self.try_version() {
77 if let Ok(other_version) = other.try_version() {
78 return Some(this_version.cmp_precedence(&other_version));
79 }
80 }
81 None
82 }
83}
84
85#[derive(Serialize, Debug, Deserialize, PartialEq)]
87pub struct PluginPackage {
88 pub(crate) os: Os,
90 pub(crate) arch: Architecture,
92 pub(crate) url: String,
94 pub(crate) sha256: String,
96}
97
98impl PluginPackage {
99 pub fn url(&self) -> String {
100 self.url.clone()
101 }
102 pub fn matches_current_os_arch(&self) -> bool {
103 self.os.rust_name() == std::env::consts::OS
104 && self.arch.rust_name() == std::env::consts::ARCH
105 }
106}
107
108#[derive(Serialize, Debug, Deserialize, PartialEq)]
110#[serde(rename_all = "camelCase")]
111pub(crate) enum Os {
112 Linux,
113 Macos,
114 Windows,
115}
116
117impl Os {
118 pub(crate) fn rust_name(&self) -> &'static str {
121 match self {
122 Os::Linux => "linux",
123 Os::Macos => "macos",
124 Os::Windows => "windows",
125 }
126 }
127}
128
129#[derive(Serialize, Debug, Deserialize, PartialEq)]
131#[serde(rename_all = "camelCase")]
132pub(crate) enum Architecture {
133 Amd64,
134 Aarch64,
135 Arm,
136}
137
138impl Architecture {
139 pub(crate) fn rust_name(&self) -> &'static str {
142 match self {
143 Architecture::Amd64 => "x86_64",
144 Architecture::Aarch64 => "aarch64",
145 Architecture::Arm => "arm",
146 }
147 }
148}
149
150pub fn warn_unsupported_version(
153 manifest: &PluginManifest,
154 spin_version: &str,
155 override_compatibility_check: bool,
156) -> Result<()> {
157 let supported_on = &manifest.spin_compatibility;
158 inner_warn_unsupported_version(supported_on, spin_version, override_compatibility_check)
159}
160
161fn is_version_fully_compatible(supported_on: &str, spin_version: &str) -> Result<bool> {
164 let comparator = VersionReq::parse(supported_on).with_context(|| {
165 format!("Could not parse manifest compatibility version {supported_on} as valid semver")
166 })?;
167 let version = Version::parse(spin_version)?;
168 Ok(comparator.matches(&version))
169}
170
171pub(crate) fn is_version_compatible_enough(supported_on: &str, spin_version: &str) -> Result<bool> {
176 if is_version_fully_compatible(supported_on, spin_version)? {
177 Ok(true)
178 } else {
179 let is_spin_prerelease = Version::parse(spin_version)
182 .map(|v| !v.pre.is_empty())
183 .unwrap_or_default();
184 Ok(is_spin_prerelease)
185 }
186}
187
188fn inner_warn_unsupported_version(
189 supported_on: &str,
190 spin_version: &str,
191 override_compatibility_check: bool,
192) -> Result<()> {
193 if !is_version_fully_compatible(supported_on, spin_version)? {
194 let show_warnings = !suppress_compatibility_warnings();
195 let version = Version::parse(spin_version)?;
196 if !version.pre.is_empty() {
197 if std::io::stderr().is_terminal() && show_warnings {
198 terminal::warn!("You're using a pre-release version of Spin ({spin_version}). This plugin might not be compatible (supported: {supported_on}). Continuing anyway.");
199 }
200 } else if override_compatibility_check {
201 if show_warnings {
202 terminal::warn!("Plugin is not compatible with this version of Spin (supported: {supported_on}, actual: {spin_version}). Check overridden ... continuing to install or execute plugin.");
203 }
204 } else {
205 return Err(anyhow!(
206 "Plugin is not compatible with this version of Spin (supported: {supported_on}, actual: {spin_version}). Try running `spin plugins update && spin plugins upgrade --all` to install latest or override with `--override-compatibility-check`."
207 ));
208 }
209 }
210 Ok(())
211}
212
213fn suppress_compatibility_warnings() -> bool {
214 match std::env::var("SPIN_PLUGINS_SUPPRESS_COMPATIBILITY_WARNINGS") {
215 Ok(s) => !s.is_empty(),
216 Err(_) => false,
217 }
218}
219
220#[cfg(test)]
221mod test {
222 use super::*;
223
224 fn generate_test_manifest(
225 name: &str,
226 version: &str,
227 license: &str,
228 description: Option<&str>,
229 homepage: Option<&str>,
230 ) -> PluginManifest {
231 let mut plugin_json = serde_json::json!(
232 {
233 "name": name,
234 "version": version,
235 "spinCompatibility": "=0.4",
236 "license": license,
237 "packages": [
238 {
239 "os": "linux",
240 "arch": "amd64",
241 "url": "www.example.com/releases/1.0/binary.tgz",
242 "sha256": "c474f00b12345e38acae2d19b2a707a4fhdjdfdd22875efeefdf052ce19c90b"
243 },
244 {
245 "os": "windows",
246 "arch": "amd64",
247 "url": "www.example.com/releases/1.0/binary.tgz",
248 "sha256": "eee4f00b12345e38acae2d19b2a707a4fhdjdfdd22875efeefdf052ce19c90b"
249 },
250 {
251 "os": "macos",
252 "arch": "aarch64",
253 "url": "www.example.com/releases/1.0/binary.tgz",
254 "sha256": "eeegf00b12345e38acae2d19b2a707a4fhdjdfdd22875efeefdf052ce19c90b"
255 }
256 ]
257 });
258 if let Some(homepage) = homepage {
259 plugin_json
260 .as_object_mut()
261 .unwrap()
262 .insert("homepage".to_string(), serde_json::json!(homepage));
263 }
264 if let Some(description) = description {
265 plugin_json
266 .as_object_mut()
267 .unwrap()
268 .insert("description".to_string(), serde_json::json!(description));
269 }
270 serde_json::from_value(plugin_json).unwrap()
271 }
272
273 #[test]
274 fn test_supported_version() {
275 let test_case = ">=1.2.3, <1.8.0";
276 let input_output = [
277 ("1.3.0", true),
278 ("1.2.3", true),
279 ("1.8.0", false),
280 ("1.9.0", false),
281 ("1.2.0", false),
282 ];
283 input_output.into_iter().for_each(|(i, o)| {
284 assert_eq!(
285 inner_warn_unsupported_version(test_case, i, false).is_ok(),
286 o
287 )
288 });
289 }
290
291 #[test]
292 fn test_plugin_json() {
293 let name = "test";
294 let description = "Some description.";
295 let homepage = "www.example.com";
296 let version = "1.0";
297 let license = "Mit";
298 let deserialized_plugin =
299 generate_test_manifest(name, version, license, Some(description), Some(homepage));
300 assert_eq!(deserialized_plugin.name(), name.to_owned());
301 assert_eq!(
302 deserialized_plugin.description,
303 Some(description.to_owned())
304 );
305 assert_eq!(deserialized_plugin.homepage, Some(homepage.to_owned()));
306 assert_eq!(deserialized_plugin.version, version.to_owned());
307 assert_eq!(deserialized_plugin.license, license.to_owned());
308 assert_eq!(deserialized_plugin.packages.len(), 3);
309 }
310
311 #[test]
312 fn test_plugin_json_empty_options() {
313 let deserialized_plugin = generate_test_manifest("name", "0.1.1", "Mit", None, None);
314 assert_eq!(deserialized_plugin.description, None);
315 assert_eq!(deserialized_plugin.homepage, None);
316 }
317}