Skip to content

Commit

Permalink
Implement spin plugin list
Browse files Browse the repository at this point in the history
Signed-off-by: itowlson <ivan.towlson@fermyon.com>
  • Loading branch information
itowlson committed Dec 15, 2022
1 parent 268ae0a commit fb64b70
Show file tree
Hide file tree
Showing 9 changed files with 272 additions and 2 deletions.
8 changes: 6 additions & 2 deletions crates/plugins/src/lookup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,13 @@ fn spin_plugins_repo_manifest_path(
plugin_version: &Option<Version>,
plugins_dir: &Path,
) -> PathBuf {
spin_plugins_repo_manifest_dir(plugins_dir)
.join(plugin_name)
.join(manifest_file_name_version(plugin_name, plugin_version))
}

pub fn spin_plugins_repo_manifest_dir(plugins_dir: &Path) -> PathBuf {
plugins_dir
.join(PLUGINS_REPO_LOCAL_DIRECTORY)
.join(PLUGINS_REPO_MANIFESTS_DIRECTORY)
.join(plugin_name)
.join(manifest_file_name_version(plugin_name, plugin_version))
}
21 changes: 21 additions & 0 deletions crates/plugins/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use anyhow::{anyhow, Context, Result};
use semver::{Version, VersionReq};
use serde::{Deserialize, Serialize};

use crate::PluginStore;

/// Expected schema of a plugin manifest. Should match the latest Spin plugin
/// manifest JSON schema:
/// https://github.com/fermyon/spin-plugins/tree/main/json-schema
Expand Down Expand Up @@ -30,9 +32,24 @@ impl PluginManifest {
pub fn name(&self) -> String {
self.name.to_lowercase()
}
pub fn version(&self) -> &str {
&self.version
}
pub fn license(&self) -> &str {
self.license.as_ref()
}
pub fn has_compatible_package(&self) -> bool {
self.packages.iter().any(|p| p.matches_current_os_arch())
}
pub fn is_compatible_spin_version(&self, spin_version: &str) -> bool {
check_supported_version(self, spin_version, false).is_ok()
}
pub fn is_installed_in(&self, store: &PluginStore) -> bool {
match store.read_plugin_manifest(&self.name) {
Ok(m) => m.version == self.version,
Err(_) => false,
}
}
}

/// Describes compatibility and location of a plugin source.
Expand All @@ -52,6 +69,10 @@ impl PluginPackage {
pub fn url(&self) -> String {
self.url.clone()
}
pub fn matches_current_os_arch(&self) -> bool {
self.os.rust_name() == std::env::consts::OS
&& self.arch.rust_name() == std::env::consts::ARCH
}
}

/// Describes the compatible OS of a plugin
Expand Down
52 changes: 52 additions & 0 deletions crates/plugins/src/store.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{anyhow, Result};
use flate2::read::GzDecoder;
use std::{
ffi::OsStr,
fs::{self, File},
path::{Path, PathBuf},
};
Expand Down Expand Up @@ -62,6 +63,57 @@ impl PluginStore {
binary
}

pub fn installed_manifests(&self) -> Result<Vec<PluginManifest>> {
let manifests_dir = self.installed_manifests_directory();
let manifest_paths = Self::json_files_in(&manifests_dir);
let manifests = manifest_paths
.iter()
.filter_map(|path| Self::try_read_manifest_from(path))
.collect();
Ok(manifests)
}

// TODO: report errors on individuals
pub fn catalogue_manifests(&self) -> Result<Vec<PluginManifest>> {
// Structure:
// CATALOGUE_DIR (spin/plugins/.spin-plugins/manifests)
// |- foo
// | |- foo@0.1.2.json
// | |- foo@1.2.3.json
// | |- foo.json
// |- bar
// |- bar.json
let catalogue_dir =
crate::lookup::spin_plugins_repo_manifest_dir(self.get_plugins_directory());
let plugin_dirs = catalogue_dir
.read_dir()?
.filter_map(|d| d.ok())
.map(|d| d.path())
.filter(|p| p.is_dir());
let manifest_paths = plugin_dirs.flat_map(|path| Self::json_files_in(&path));
let manifests: Vec<_> = manifest_paths
.filter_map(|path| Self::try_read_manifest_from(&path))
.collect();
Ok(manifests)
}

fn try_read_manifest_from(manifest_path: &Path) -> Option<PluginManifest> {
let manifest_file = File::open(manifest_path).ok()?;
serde_json::from_reader(manifest_file).ok()
}

fn json_files_in(dir: &Path) -> Vec<PathBuf> {
let json_ext = Some(OsStr::new("json"));
match dir.read_dir() {
Err(_) => vec![],
Ok(rd) => rd
.filter_map(|de| de.ok())
.map(|de| de.path())
.filter(|p| p.is_file() && p.extension() == json_ext)
.collect(),
}
}

/// Returns the PluginManifest for an installed plugin with a given name.
/// Looks up and parses the JSON plugin manifest file into object form.
pub fn read_plugin_manifest(&self, plugin_name: &str) -> PluginLookupResult<PluginManifest> {
Expand Down
8 changes: 8 additions & 0 deletions crates/plugins/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## Fake plugin manifests

These plugin manifests are fake - they exist to help exercise compatibility
failure paths. They are in a compatible layout for you to xcopy to your
catalogue snapshot directory (`~/.local/share/spin/plugins/.spin-plugins/manifests/`).

The actual package pointers are what we engineers call "hot garbage"; they
cannot be installed as actual plugins.
15 changes: 15 additions & 0 deletions crates/plugins/tests/arm-only/arm-only.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "arm-only",
"description": "A plugin which only works on ARM Linux.",
"version": "0.1.0",
"spinCompatibility": ">=0.4",
"license": "Apache-2.0",
"packages": [
{
"os": "linux",
"arch": "arm",
"url": "https://example.com/doesnt-exist",
"sha256": "11111111"
}
]
}
21 changes: 21 additions & 0 deletions crates/plugins/tests/not-spin-ver/not-spin-ver.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "not-spin-ver",
"description": "A plugin which doesn't work with your current version of Spin.",
"version": "10.2.0",
"spinCompatibility": "=999.999.999",
"license": "Apache-2.0",
"packages": [
{
"os": "linux",
"arch": "amd64",
"url": "https://example.com/doesnt-exist",
"sha256": "11111111"
},
{
"os": "macos",
"arch": "aarch64",
"url": "https://example.com/doesnt-exist",
"sha256": "11111111"
}
]
}
15 changes: 15 additions & 0 deletions crates/plugins/tests/not-spin-ver/not-spin-ver@9.1.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "not-spin-ver",
"description": "A plugin which doesn't work with your current version of Spin - Mac only build.",
"version": "9.1.0",
"spinCompatibility": "=999.999.999",
"license": "Apache-2.0",
"packages": [
{
"os": "macos",
"arch": "aarch64",
"url": "https://example.com/doesnt-exist",
"sha256": "11111111"
}
]
}
15 changes: 15 additions & 0 deletions crates/plugins/tests/windows-only/windows-only.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "windows-only",
"description": "A plugin which only works on Windows.",
"version": "0.1.0",
"spinCompatibility": ">=0.4",
"license": "Apache-2.0",
"packages": [
{
"os": "windows",
"arch": "amd64",
"url": "https://example.com/doesnt-exist",
"sha256": "11111111"
}
]
}
119 changes: 119 additions & 0 deletions src/commands/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ pub enum PluginCommands {
/// plugins directory.
Install(Install),

/// List available or installed plugins.
List(List),

/// Remove a plugin from your installation.
Uninstall(Uninstall),

Expand All @@ -36,6 +39,7 @@ impl PluginCommands {
pub async fn run(self) -> Result<()> {
match self {
PluginCommands::Install(cmd) => cmd.run().await,
PluginCommands::List(cmd) => cmd.run().await,
PluginCommands::Uninstall(cmd) => cmd.run().await,
PluginCommands::Upgrade(cmd) => cmd.run().await,
PluginCommands::Update => update().await,
Expand Down Expand Up @@ -285,6 +289,121 @@ impl Upgrade {
}
}

/// Install plugins from remote source
#[derive(Parser, Debug)]
pub struct List {
/// List only installed plugins.
#[clap(long = "installed", takes_value = false)]
pub installed: bool,
}

impl List {
pub async fn run(self) -> Result<()> {
let mut plugins = if self.installed {
Self::list_installed_plugins()
} else {
Self::list_catalogue_plugins()
}?;

plugins.sort_by(|p, q| p.cmp(q));

Self::print(&plugins);
Ok(())
}

fn list_installed_plugins() -> Result<Vec<PluginDescriptor>> {
let manager = PluginManager::try_default()?;
let store = manager.store();
let manifests = store.installed_manifests()?;
let descriptors = manifests
.iter()
.map(|m| PluginDescriptor {
name: m.name(),
version: m.version().to_owned(),
installed: true,
compatibility: PluginCompatibility::for_current(m),
})
.collect();
Ok(descriptors)
}

fn list_catalogue_plugins() -> Result<Vec<PluginDescriptor>> {
let manager = PluginManager::try_default()?;
let store = manager.store();
let manifests = store.catalogue_manifests();
let descriptors = manifests?
.iter()
.map(|m| PluginDescriptor {
name: m.name(),
version: m.version().to_owned(),
installed: m.is_installed_in(store),
compatibility: PluginCompatibility::for_current(m),
})
.collect();
Ok(descriptors)
}

fn print(plugins: &[PluginDescriptor]) {
if plugins.is_empty() {
println!("No plugins found");
} else {
for p in plugins {
let installed = if p.installed { " [installed]" } else { "" };
let compat = match p.compatibility {
PluginCompatibility::Compatible => "",
PluginCompatibility::IncompatibleSpin => " [requires other Spin version]",
PluginCompatibility::Incompatible => " [incompatible]",
};
println!("{} {}{}{}", p.name, p.version, installed, compat);
}
}
}
}

#[derive(Debug)]
enum PluginCompatibility {
Compatible,
IncompatibleSpin,
Incompatible,
}

impl PluginCompatibility {
fn for_current(manifest: &PluginManifest) -> Self {
if manifest.has_compatible_package() {
let spin_version = env!("VERGEN_BUILD_SEMVER");
if manifest.is_compatible_spin_version(spin_version) {
Self::Compatible
} else {
Self::IncompatibleSpin
}
} else {
Self::Incompatible
}
}
}

#[derive(Debug)]
struct PluginDescriptor {
name: String,
version: String,
compatibility: PluginCompatibility,
installed: bool,
}

impl PluginDescriptor {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let version_cmp = match (
semver::Version::parse(&self.version),
semver::Version::parse(&other.version),
) {
(Ok(v1), Ok(v2)) => v1.cmp(&v2),
_ => self.version.cmp(&other.version),
};

self.name.cmp(&other.name).then(version_cmp)
}
}

/// Updates the locally cached spin-plugins repository, fetching the latest plugins.
async fn update() -> Result<()> {
let manager = PluginManager::try_default()?;
Expand Down

0 comments on commit fb64b70

Please sign in to comment.