use std::cmp::Ordering;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use mlua::{AsChunk, FromLuaMulti, IntoLua, Lua, Table, Value};
use once_cell::sync::OnceCell;
use crate::config::Config;
use crate::context::Context;
use crate::embedded_plugins::{self, EmbeddedPlugin};
use crate::error::Result;
use crate::metadata::Metadata;
use crate::runtime::Runtime;
use crate::sdk_info::SdkInfo;
use crate::{VfoxError, config, error, lua_mod};
#[derive(Debug)]
pub enum PluginSource {
Filesystem(PathBuf),
Embedded(&'static EmbeddedPlugin),
}
#[derive(Debug)]
pub struct Plugin {
pub name: String,
pub dir: PathBuf,
source: PluginSource,
lua: Lua,
metadata: OnceCell<Metadata>,
}
impl Plugin {
pub fn from_dir(dir: &Path) -> Result<Self> {
if !dir.exists() {
error!("Plugin directory not found: {:?}", dir);
}
let lua = Lua::new();
lua.set_named_registry_value("plugin_dir", dir.to_path_buf())?;
let name = dir.file_name().unwrap().to_string_lossy().to_string();
lua.set_named_registry_value("plugin_name", name.clone())?;
Ok(Self {
name,
dir: dir.to_path_buf(),
source: PluginSource::Filesystem(dir.to_path_buf()),
lua,
metadata: OnceCell::new(),
})
}
pub fn from_embedded(name: &str, embedded: &'static EmbeddedPlugin) -> Result<Self> {
let lua = Lua::new();
let dummy_dir = PathBuf::from(format!("embedded:{}", name));
lua.set_named_registry_value("plugin_dir", dummy_dir.clone())?;
lua.set_named_registry_value("embedded_plugin", true)?;
lua.set_named_registry_value("plugin_name", name.to_string())?;
Ok(Self {
name: name.to_string(),
dir: dummy_dir,
source: PluginSource::Embedded(embedded),
lua,
metadata: OnceCell::new(),
})
}
pub fn from_name(name: &str) -> Result<Self> {
let dir = Config::get().plugin_dir.join(name);
if dir.exists() {
return Self::from_dir(&dir);
}
if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) {
return Self::from_embedded(name, embedded);
}
Self::from_dir(&dir)
}
pub fn from_name_or_dir(name: &str, dir: &Path) -> Result<Self> {
if dir.exists() {
return Self::from_dir(dir);
}
if let Some(embedded) = embedded_plugins::get_embedded_plugin(name) {
return Self::from_embedded(name, embedded);
}
Self::from_dir(dir)
}
pub fn is_embedded(&self) -> bool {
matches!(self.source, PluginSource::Embedded(_))
}
pub fn set_cmd_env(&self, env: &indexmap::IndexMap<String, String>) -> Result<()> {
let table = self.lua.create_table()?;
for (k, v) in env {
table.set(k.as_str(), v.as_str())?;
}
self.lua.set_named_registry_value("mise_env", table)?;
Ok(())
}
pub fn list() -> Result<Vec<String>> {
let config = Config::get();
if !config.plugin_dir.exists() {
return Ok(vec![]);
}
let plugins = xx::file::ls(&config.plugin_dir)?;
let plugins = plugins
.iter()
.filter_map(|p| {
p.file_name()
.and_then(|f| f.to_str())
.map(|s| s.to_string())
})
.collect();
Ok(plugins)
}
pub fn get_metadata(&self) -> Result<Metadata> {
Ok(self.load()?.clone())
}
pub fn sdk_info(&self, version: String, install_dir: PathBuf) -> Result<SdkInfo> {
Ok(SdkInfo::new(
self.get_metadata()?.name.clone(),
version,
install_dir,
))
}
#[cfg(test)]
pub(crate) fn test(name: &str) -> Self {
let dir = PathBuf::from("plugins").join(name);
Self::from_dir(&dir).unwrap()
}
pub(crate) fn context(&self, version: Option<String>) -> Result<Context> {
let ctx = Context {
args: vec![],
version,
};
Ok(ctx)
}
pub(crate) async fn exec_async(&self, chunk: impl AsChunk) -> Result<()> {
self.load()?;
let chunk = self.lua.load(chunk);
chunk.exec_async().await?;
Ok(())
}
pub(crate) async fn eval_async<R>(&self, chunk: impl AsChunk) -> Result<R>
where
R: FromLuaMulti,
{
self.load()?;
let chunk = self.lua.load(chunk);
let result = chunk.eval_async().await?;
Ok(result)
}
fn load(&self) -> Result<&Metadata> {
self.metadata.get_or_try_init(|| {
debug!("[vfox] Getting metadata for {self}");
if let PluginSource::Filesystem(dir) = &self.source {
set_paths(
&self.lua,
&[
dir.join("?.lua"),
dir.join("hooks/?.lua"),
dir.join("lib/?.lua"),
],
)?;
}
lua_mod::archiver(&self.lua)?;
lua_mod::cmd(&self.lua)?;
lua_mod::file(&self.lua)?;
lua_mod::html(&self.lua)?;
lua_mod::http(&self.lua)?;
lua_mod::json(&self.lua)?;
lua_mod::semver(&self.lua)?;
lua_mod::strings(&self.lua)?;
lua_mod::env(&self.lua)?;
lua_mod::log(&self.lua)?;
if let PluginSource::Embedded(embedded) = &self.source {
self.load_embedded_libs(embedded)?;
}
let metadata = self.load_metadata()?;
self.set_global("PLUGIN", metadata.clone())?;
self.set_global("RUNTIME", Runtime::get(self.dir.clone()))?;
self.set_global("OS_TYPE", config::os())?;
self.set_global("ARCH_TYPE", config::arch())?;
let mut metadata: Metadata = metadata.try_into()?;
metadata.hooks = match &self.source {
PluginSource::Filesystem(dir) => lua_mod::hooks(&self.lua, dir)?,
PluginSource::Embedded(embedded) => lua_mod::hooks_embedded(&self.lua, embedded)?,
};
Ok(metadata)
})
}
fn load_embedded_libs(&self, embedded: &EmbeddedPlugin) -> Result<()> {
let package: Table = self.lua.globals().get("package")?;
let preload: Table = package.get("preload")?;
for (name, code) in embedded.lib {
let lua = self.lua.clone();
let code = *code;
let loader = lua.create_function(move |lua, _: ()| {
let module: Value = lua.load(code).eval()?;
Ok(module)
})?;
preload.set(*name, loader)?;
}
Ok(())
}
fn set_global<V>(&self, name: &str, value: V) -> Result<()>
where
V: IntoLua,
{
self.lua.globals().set(name, value)?;
Ok(())
}
fn load_metadata(&self) -> Result<Table> {
match &self.source {
PluginSource::Filesystem(_) => {
let metadata = self
.lua
.load(
r#"
require "metadata"
return PLUGIN
"#,
)
.eval()?;
Ok(metadata)
}
PluginSource::Embedded(embedded) => {
self.lua.load(embedded.metadata).exec()?;
let metadata = self.lua.globals().get("PLUGIN")?;
Ok(metadata)
}
}
}
}
fn get_package(lua: &Lua) -> Result<Table> {
let package = lua.globals().get::<Table>("package")?;
Ok(package)
}
fn set_paths(lua: &Lua, paths: &[PathBuf]) -> Result<()> {
let paths = paths
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect::<Vec<String>>()
.join(";");
get_package(lua)?.set("path", paths)?;
Ok(())
}
impl Display for Plugin {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name)
}
}
impl PartialEq<Self> for Plugin {
fn eq(&self, other: &Self) -> bool {
self.dir == other.dir
}
}
impl Eq for Plugin {}
impl PartialOrd for Plugin {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Plugin {
fn cmp(&self, other: &Self) -> Ordering {
self.name.cmp(&other.name)
}
}