Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement spin plugin list #972

Merged
merged 1 commit into from
Dec 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 {
kate-goldenring marked this conversation as resolved.
Show resolved Hide resolved
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();
kate-goldenring marked this conversation as resolved.
Show resolved Hide resolved
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
kate-goldenring marked this conversation as resolved.
Show resolved Hide resolved

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