mirror of
https://github.com/nushell/nushell.git
synced 2024-11-24 17:34:00 +01:00
Overhaul the plugin cache file with a new msgpack+brotli format (#12579)
# Description - Plugin signatures are now saved to `plugin.msgpackz`, which is brotli-compressed MessagePack. - The file is updated incrementally, rather than writing all plugin commands in the engine every time. - The file always contains the result of the `Signature` call to the plugin, even if commands were removed. - Invalid data for a particular plugin just causes an error to be reported, but the rest of the plugins can still be parsed # User-Facing Changes - The plugin file has a different filename, and it's not a nushell script. - The default `plugin.nu` file will be automatically migrated the first time, but not other plugin config files. - We don't currently provide any utilities that could help edit this file, beyond `plugin add` and `plugin rm` - `from msgpackz`, `to msgpackz` could also help - New commands: `plugin add`, `plugin rm` # Tests + Formatting Tests added for the format and for the invalid handling. - 🟢 `toolkit fmt` - 🟢 `toolkit clippy` - 🟢 `toolkit test` - 🟢 `toolkit test stdlib` # After Submitting - [ ] Check for documentation changes - [ ] Definitely needs release notes
This commit is contained in:
parent
6cba7c6b40
commit
2595f31541
41
Cargo.lock
generated
41
Cargo.lock
generated
@ -520,7 +520,18 @@ checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
"brotli-decompressor 2.5.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor 4.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -533,6 +544,16 @@ dependencies = [
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "4.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6221fe77a248b9117d431ad93761222e1cf8ff282d9d1d5d9f53d6299a1cf76"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bstr"
|
||||
version = "1.9.1"
|
||||
@ -2861,6 +2882,7 @@ dependencies = [
|
||||
"nu-cmd-dataframe",
|
||||
"nu-cmd-extra",
|
||||
"nu-cmd-lang",
|
||||
"nu-cmd-plugin",
|
||||
"nu-command",
|
||||
"nu-engine",
|
||||
"nu-explore",
|
||||
@ -2914,6 +2936,7 @@ dependencies = [
|
||||
"nu-engine",
|
||||
"nu-parser",
|
||||
"nu-path",
|
||||
"nu-plugin",
|
||||
"nu-protocol",
|
||||
"nu-test-support",
|
||||
"nu-utils",
|
||||
@ -3000,6 +3023,17 @@ dependencies = [
|
||||
"shadow-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-cmd-plugin"
|
||||
version = "0.92.3"
|
||||
dependencies = [
|
||||
"itertools 0.12.1",
|
||||
"nu-engine",
|
||||
"nu-path",
|
||||
"nu-plugin",
|
||||
"nu-protocol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-color-config"
|
||||
version = "0.92.3"
|
||||
@ -3262,6 +3296,7 @@ dependencies = [
|
||||
name = "nu-protocol"
|
||||
version = "0.92.3"
|
||||
dependencies = [
|
||||
"brotli 5.0.0",
|
||||
"byte-unit",
|
||||
"chrono",
|
||||
"chrono-humanize",
|
||||
@ -3274,6 +3309,8 @@ dependencies = [
|
||||
"nu-test-support",
|
||||
"nu-utils",
|
||||
"num-format",
|
||||
"pretty_assertions",
|
||||
"rmp-serde",
|
||||
"rstest",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -4282,7 +4319,7 @@ dependencies = [
|
||||
"ahash 0.8.11",
|
||||
"async-stream",
|
||||
"base64 0.21.7",
|
||||
"brotli",
|
||||
"brotli 3.5.0",
|
||||
"ethnum",
|
||||
"flate2",
|
||||
"futures",
|
||||
|
@ -31,6 +31,7 @@ members = [
|
||||
"crates/nu-cmd-base",
|
||||
"crates/nu-cmd-extra",
|
||||
"crates/nu-cmd-lang",
|
||||
"crates/nu-cmd-plugin",
|
||||
"crates/nu-cmd-dataframe",
|
||||
"crates/nu-command",
|
||||
"crates/nu-color-config",
|
||||
@ -62,6 +63,7 @@ alphanumeric-sort = "1.5"
|
||||
ansi-str = "0.8"
|
||||
base64 = "0.22"
|
||||
bracoxide = "0.1.2"
|
||||
brotli = "5.0"
|
||||
byteorder = "1.5"
|
||||
bytesize = "1.3"
|
||||
calamine = "0.24.0"
|
||||
@ -126,6 +128,7 @@ ratatui = "0.26"
|
||||
rayon = "1.10"
|
||||
reedline = "0.31.0"
|
||||
regex = "1.9.5"
|
||||
rmp-serde = "1.2"
|
||||
ropey = "1.6.1"
|
||||
roxmltree = "0.19"
|
||||
rstest = { version = "0.18", default-features = false }
|
||||
@ -168,6 +171,7 @@ winreg = "0.52"
|
||||
nu-cli = { path = "./crates/nu-cli", version = "0.92.3" }
|
||||
nu-cmd-base = { path = "./crates/nu-cmd-base", version = "0.92.3" }
|
||||
nu-cmd-lang = { path = "./crates/nu-cmd-lang", version = "0.92.3" }
|
||||
nu-cmd-plugin = { path = "./crates/nu-cmd-plugin", version = "0.92.3", optional = true }
|
||||
nu-cmd-dataframe = { path = "./crates/nu-cmd-dataframe", version = "0.92.3", features = [
|
||||
"dataframe",
|
||||
], optional = true }
|
||||
@ -223,6 +227,7 @@ tempfile = { workspace = true }
|
||||
[features]
|
||||
plugin = [
|
||||
"nu-plugin",
|
||||
"nu-cmd-plugin",
|
||||
"nu-cli/plugin",
|
||||
"nu-parser/plugin",
|
||||
"nu-command/plugin",
|
||||
|
@ -21,6 +21,7 @@ nu-cmd-base = { path = "../nu-cmd-base", version = "0.92.3" }
|
||||
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||
nu-parser = { path = "../nu-parser", version = "0.92.3" }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.92.3", optional = true }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3" }
|
||||
nu-utils = { path = "../nu-utils", version = "0.92.3" }
|
||||
nu-color-config = { path = "../nu-color-config", version = "0.92.3" }
|
||||
@ -44,5 +45,5 @@ uuid = { workspace = true, features = ["v4"] }
|
||||
which = { workspace = true }
|
||||
|
||||
[features]
|
||||
plugin = []
|
||||
plugin = ["nu-plugin"]
|
||||
system-clipboard = ["reedline/system_clipboard"]
|
||||
|
@ -6,13 +6,15 @@ use nu_protocol::{
|
||||
report_error, HistoryFileFormat, PipelineData,
|
||||
};
|
||||
#[cfg(feature = "plugin")]
|
||||
use nu_protocol::{ParseError, Spanned};
|
||||
use nu_protocol::{ParseError, PluginCacheFile, Spanned};
|
||||
#[cfg(feature = "plugin")]
|
||||
use nu_utils::utils::perf;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
const PLUGIN_FILE: &str = "plugin.nu";
|
||||
const PLUGIN_FILE: &str = "plugin.msgpackz";
|
||||
#[cfg(feature = "plugin")]
|
||||
const OLD_PLUGIN_FILE: &str = "plugin.nu";
|
||||
|
||||
const HISTORY_FILE_TXT: &str = "history.txt";
|
||||
const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
|
||||
@ -20,14 +22,38 @@ const HISTORY_FILE_SQLITE: &str = "history.sqlite3";
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn read_plugin_file(
|
||||
engine_state: &mut EngineState,
|
||||
stack: &mut Stack,
|
||||
plugin_file: Option<Spanned<String>>,
|
||||
storage_path: &str,
|
||||
) {
|
||||
use std::path::Path;
|
||||
|
||||
use nu_protocol::{report_error_new, ShellError};
|
||||
|
||||
let span = plugin_file.as_ref().map(|s| s.span);
|
||||
|
||||
// Check and warn + abort if this is a .nu plugin file
|
||||
if plugin_file
|
||||
.as_ref()
|
||||
.and_then(|p| Path::new(&p.item).extension())
|
||||
.is_some_and(|ext| ext == "nu")
|
||||
{
|
||||
report_error_new(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Wrong plugin file format".into(),
|
||||
msg: ".nu plugin files are no longer supported".into(),
|
||||
span,
|
||||
help: Some("please recreate this file in the new .msgpackz format".into()),
|
||||
inner: vec![],
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut start_time = std::time::Instant::now();
|
||||
// Reading signatures from signature file
|
||||
// The plugin.nu file stores the parsed signature collected from each registered plugin
|
||||
add_plugin_file(engine_state, plugin_file, storage_path);
|
||||
// Reading signatures from plugin cache file
|
||||
// The plugin.msgpackz file stores the parsed signature collected from each registered plugin
|
||||
add_plugin_file(engine_state, plugin_file.clone(), storage_path);
|
||||
perf(
|
||||
"add plugin file to engine_state",
|
||||
start_time,
|
||||
@ -38,14 +64,82 @@ pub fn read_plugin_file(
|
||||
);
|
||||
|
||||
start_time = std::time::Instant::now();
|
||||
let plugin_path = engine_state.plugin_signatures.clone();
|
||||
let plugin_path = engine_state.plugin_path.clone();
|
||||
if let Some(plugin_path) = plugin_path {
|
||||
let plugin_filename = plugin_path.to_string_lossy();
|
||||
let plug_path = plugin_filename.to_string();
|
||||
// Open the plugin file
|
||||
let mut file = match std::fs::File::open(&plugin_path) {
|
||||
Ok(file) => file,
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
log::warn!("Plugin file not found: {}", plugin_path.display());
|
||||
|
||||
// Try migration of an old plugin file if this wasn't a custom plugin file
|
||||
if plugin_file.is_none() && migrate_old_plugin_file(engine_state, storage_path)
|
||||
{
|
||||
let Ok(file) = std::fs::File::open(&plugin_path) else {
|
||||
log::warn!("Failed to load newly migrated plugin file");
|
||||
return;
|
||||
};
|
||||
file
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
report_error_new(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: format!(
|
||||
"Error while opening plugin cache file: {}",
|
||||
plugin_path.display()
|
||||
),
|
||||
msg: "plugin path defined here".into(),
|
||||
span,
|
||||
help: None,
|
||||
inner: vec![err.into()],
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Abort if the file is empty.
|
||||
if file.metadata().is_ok_and(|m| m.len() == 0) {
|
||||
log::warn!(
|
||||
"Not reading plugin file because it's empty: {}",
|
||||
plugin_path.display()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read the contents of the plugin file
|
||||
let contents = match PluginCacheFile::read_from(&mut file, span) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) => {
|
||||
log::warn!("Failed to read plugin cache file: {err:?}");
|
||||
report_error_new(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: format!(
|
||||
"Error while reading plugin cache file: {}",
|
||||
plugin_path.display()
|
||||
),
|
||||
msg: "plugin path defined here".into(),
|
||||
span,
|
||||
help: Some(
|
||||
"you might try deleting the file and registering all of your \
|
||||
plugins again"
|
||||
.into(),
|
||||
),
|
||||
inner: vec![],
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(contents) = std::fs::read(&plugin_path) {
|
||||
perf(
|
||||
&format!("read plugin file {}", &plug_path),
|
||||
&format!("read plugin file {}", plugin_path.display()),
|
||||
start_time,
|
||||
file!(),
|
||||
line!(),
|
||||
@ -53,16 +147,18 @@ pub fn read_plugin_file(
|
||||
engine_state.get_config().use_ansi_coloring,
|
||||
);
|
||||
start_time = std::time::Instant::now();
|
||||
eval_source(
|
||||
engine_state,
|
||||
stack,
|
||||
&contents,
|
||||
&plugin_filename,
|
||||
PipelineData::empty(),
|
||||
false,
|
||||
);
|
||||
|
||||
let mut working_set = StateWorkingSet::new(engine_state);
|
||||
|
||||
nu_plugin::load_plugin_file(&mut working_set, &contents, span);
|
||||
|
||||
if let Err(err) = engine_state.merge_delta(working_set.render()) {
|
||||
report_error_new(engine_state, &err);
|
||||
return;
|
||||
}
|
||||
|
||||
perf(
|
||||
&format!("eval_source plugin file {}", &plug_path),
|
||||
&format!("load plugin file {}", plugin_path.display()),
|
||||
start_time,
|
||||
file!(),
|
||||
line!(),
|
||||
@ -70,7 +166,6 @@ pub fn read_plugin_file(
|
||||
engine_state.get_config().use_ansi_coloring,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
@ -79,15 +174,30 @@ pub fn add_plugin_file(
|
||||
plugin_file: Option<Spanned<String>>,
|
||||
storage_path: &str,
|
||||
) {
|
||||
use std::path::Path;
|
||||
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
let cwd = working_set.get_cwd();
|
||||
|
||||
if let Some(plugin_file) = plugin_file {
|
||||
if let Ok(path) = canonicalize_with(&plugin_file.item, cwd) {
|
||||
engine_state.plugin_signatures = Some(path)
|
||||
let path = Path::new(&plugin_file.item);
|
||||
let path_dir = path.parent().unwrap_or(path);
|
||||
// Just try to canonicalize the directory of the plugin file first.
|
||||
if let Ok(path_dir) = canonicalize_with(path_dir, &cwd) {
|
||||
// Try to canonicalize the actual filename, but it's ok if that fails. The file doesn't
|
||||
// have to exist.
|
||||
let path = path_dir.join(path.file_name().unwrap_or(path.as_os_str()));
|
||||
let path = canonicalize_with(&path, &cwd).unwrap_or(path);
|
||||
engine_state.plugin_path = Some(path)
|
||||
} else {
|
||||
let e = ParseError::FileNotFound(plugin_file.item, plugin_file.span);
|
||||
report_error(&working_set, &e);
|
||||
// It's an error if the directory for the plugin file doesn't exist.
|
||||
report_error(
|
||||
&working_set,
|
||||
&ParseError::FileNotFound(
|
||||
path_dir.to_string_lossy().into_owned(),
|
||||
plugin_file.span,
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if let Some(mut plugin_path) = nu_path::config_dir() {
|
||||
// Path to store plugins signatures
|
||||
@ -95,7 +205,7 @@ pub fn add_plugin_file(
|
||||
let mut plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
|
||||
plugin_path.push(PLUGIN_FILE);
|
||||
let plugin_path = canonicalize_with(&plugin_path, &cwd).unwrap_or(plugin_path);
|
||||
engine_state.plugin_signatures = Some(plugin_path);
|
||||
engine_state.plugin_path = Some(plugin_path);
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,3 +261,129 @@ pub(crate) fn get_history_path(storage_path: &str, mode: HistoryFileFormat) -> O
|
||||
history_path
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -> bool {
|
||||
use nu_protocol::{
|
||||
report_error_new, PluginCacheItem, PluginCacheItemData, PluginExample, PluginIdentity,
|
||||
PluginSignature, ShellError,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let cwd = engine_state.current_work_dir();
|
||||
|
||||
let Some(config_dir) = nu_path::config_dir().and_then(|mut dir| {
|
||||
dir.push(storage_path);
|
||||
nu_path::canonicalize_with(dir, &cwd).ok()
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let Ok(old_plugin_file_path) = nu_path::canonicalize_with(OLD_PLUGIN_FILE, &config_dir) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let old_contents = match std::fs::read(&old_plugin_file_path) {
|
||||
Ok(old_contents) => old_contents,
|
||||
Err(err) => {
|
||||
report_error_new(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Can't read old plugin file to migrate".into(),
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: Some(err.to_string()),
|
||||
inner: vec![],
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Make a copy of the engine state, because we'll read the newly generated file
|
||||
let mut engine_state = engine_state.clone();
|
||||
let mut stack = Stack::new();
|
||||
|
||||
if !eval_source(
|
||||
&mut engine_state,
|
||||
&mut stack,
|
||||
&old_contents,
|
||||
&old_plugin_file_path.to_string_lossy(),
|
||||
PipelineData::Empty,
|
||||
false,
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now that the plugin commands are loaded, we just have to generate the file
|
||||
let mut contents = PluginCacheFile::new();
|
||||
|
||||
let mut groups = BTreeMap::<PluginIdentity, Vec<PluginSignature>>::new();
|
||||
|
||||
for decl in engine_state.plugin_decls() {
|
||||
if let Some(identity) = decl.plugin_identity() {
|
||||
groups
|
||||
.entry(identity.clone())
|
||||
.or_default()
|
||||
.push(PluginSignature {
|
||||
sig: decl.signature(),
|
||||
examples: decl
|
||||
.examples()
|
||||
.into_iter()
|
||||
.map(PluginExample::from)
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (identity, commands) in groups {
|
||||
contents.upsert_plugin(PluginCacheItem {
|
||||
name: identity.name().to_owned(),
|
||||
filename: identity.filename().to_owned(),
|
||||
shell: identity.shell().map(|p| p.to_owned()),
|
||||
data: PluginCacheItemData::Valid { commands },
|
||||
});
|
||||
}
|
||||
|
||||
// Write the new file
|
||||
let new_plugin_file_path = config_dir.join(PLUGIN_FILE);
|
||||
if let Err(err) = std::fs::File::create(&new_plugin_file_path)
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|file| contents.write_to(file, None))
|
||||
{
|
||||
report_error_new(
|
||||
&engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Failed to save migrated plugin file".into(),
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: Some("ensure `$nu.plugin-path` is writable".into()),
|
||||
inner: vec![err],
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if engine_state.is_interactive {
|
||||
eprintln!(
|
||||
"Your old plugin.nu file has been migrated to the new format: {}",
|
||||
new_plugin_file_path.display()
|
||||
);
|
||||
eprintln!(
|
||||
"The plugin.nu file has not been removed. If `plugin list` looks okay, \
|
||||
you may do so manually."
|
||||
);
|
||||
}
|
||||
|
||||
perf(
|
||||
"migrate old plugin file",
|
||||
start_time,
|
||||
file!(),
|
||||
line!(),
|
||||
column!(),
|
||||
engine_state.get_config().use_ansi_coloring,
|
||||
);
|
||||
true
|
||||
}
|
||||
|
@ -32,4 +32,6 @@ pub use validation::NuValidator;
|
||||
#[cfg(feature = "plugin")]
|
||||
pub use config_files::add_plugin_file;
|
||||
#[cfg(feature = "plugin")]
|
||||
pub use config_files::migrate_old_plugin_file;
|
||||
#[cfg(feature = "plugin")]
|
||||
pub use config_files::read_plugin_file;
|
||||
|
@ -71,13 +71,3 @@ pub use try_::Try;
|
||||
pub use use_::Use;
|
||||
pub use version::Version;
|
||||
pub use while_::While;
|
||||
|
||||
mod plugin;
|
||||
mod plugin_list;
|
||||
mod plugin_stop;
|
||||
mod register;
|
||||
|
||||
pub use plugin::PluginCommand;
|
||||
pub use plugin_list::PluginList;
|
||||
pub use plugin_stop::PluginStop;
|
||||
pub use register::Register;
|
||||
|
@ -63,9 +63,6 @@ pub fn create_default_context() -> EngineState {
|
||||
While,
|
||||
};
|
||||
|
||||
//#[cfg(feature = "plugin")]
|
||||
bind_command!(PluginCommand, PluginList, PluginStop, Register,);
|
||||
|
||||
working_set.render()
|
||||
};
|
||||
|
||||
|
20
crates/nu-cmd-plugin/Cargo.toml
Normal file
20
crates/nu-cmd-plugin/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
authors = ["The Nushell Project Developers"]
|
||||
description = "Commands for managing Nushell plugins."
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
name = "nu-cmd-plugin"
|
||||
repository = "https://github.com/nushell/nushell/tree/main/crates/nu-cmd-plugin"
|
||||
version = "0.92.3"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nu-engine = { path = "../nu-engine", version = "0.92.3" }
|
||||
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||
nu-protocol = { path = "../nu-protocol", version = "0.92.3", features = ["plugin"] }
|
||||
nu-plugin = { path = "../nu-plugin", version = "0.92.3" }
|
||||
|
||||
itertools = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
21
crates/nu-cmd-plugin/LICENSE
Normal file
21
crates/nu-cmd-plugin/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 - 2023 The Nushell Project Developers
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
crates/nu-cmd-plugin/README.md
Normal file
3
crates/nu-cmd-plugin/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# nu-cmd-plugin
|
||||
|
||||
This crate implements Nushell commands related to plugin management.
|
5
crates/nu-cmd-plugin/src/commands/mod.rs
Normal file
5
crates/nu-cmd-plugin/src/commands/mod.rs
Normal file
@ -0,0 +1,5 @@
|
||||
mod plugin;
|
||||
mod register;
|
||||
|
||||
pub use plugin::*;
|
||||
pub use register::Register;
|
148
crates/nu-cmd-plugin/src/commands/plugin/add.rs
Normal file
148
crates/nu-cmd-plugin/src/commands/plugin/add.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use nu_engine::{command_prelude::*, current_dir};
|
||||
use nu_plugin::{GetPlugin, PersistentPlugin};
|
||||
use nu_protocol::{PluginCacheItem, PluginGcConfig, PluginIdentity, RegisteredPlugin};
|
||||
|
||||
use crate::util::modify_plugin_file;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginAdd;
|
||||
|
||||
impl Command for PluginAdd {
|
||||
fn name(&self) -> &str {
|
||||
"plugin add"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.input_output_type(Type::Nothing, Type::Nothing)
|
||||
// This matches the option to `nu`
|
||||
.named(
|
||||
"plugin-config",
|
||||
SyntaxShape::Filepath,
|
||||
"Use a plugin cache file other than the one set in `$nu.plugin-path`",
|
||||
None,
|
||||
)
|
||||
.named(
|
||||
"shell",
|
||||
SyntaxShape::Filepath,
|
||||
"Use an additional shell program (cmd, sh, python, etc.) to run the plugin",
|
||||
Some('s'),
|
||||
)
|
||||
.required(
|
||||
"filename",
|
||||
SyntaxShape::Filepath,
|
||||
"Path to the executable for the plugin",
|
||||
)
|
||||
.category(Category::Plugin)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Add a plugin to the plugin cache file."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"
|
||||
This does not load the plugin commands into the scope - see `register` for that.
|
||||
|
||||
Instead, it runs the plugin to get its command signatures, and then edits the
|
||||
plugin cache file (by default, `$nu.plugin-path`). The changes will be
|
||||
apparent the next time `nu` is next launched with that plugin cache file.
|
||||
"#
|
||||
.trim()
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["plugin", "add", "register", "load", "signature"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "plugin add nu_plugin_inc",
|
||||
description: "Run the `nu_plugin_inc` plugin from the current directory or $env.NU_PLUGIN_DIRS and install its signatures.",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "plugin add --plugin-config polars.msgpackz nu_plugin_polars",
|
||||
description: "Run the `nu_plugin_polars` plugin from the current directory or $env.NU_PLUGIN_DIRS, and install its signatures to the \"polars.msgpackz\" plugin cache file.",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let filename: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
let shell: Option<Spanned<String>> = call.get_flag(engine_state, stack, "shell")?;
|
||||
|
||||
let cwd = current_dir(engine_state, stack)?;
|
||||
|
||||
// Check the current directory, or fall back to NU_PLUGIN_DIRS
|
||||
let filename_expanded = match nu_path::canonicalize_with(&filename.item, &cwd) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
// Try to find it in NU_PLUGIN_DIRS first, before giving up
|
||||
let mut found = None;
|
||||
if let Some(nu_plugin_dirs) = stack.get_env_var(engine_state, "NU_PLUGIN_DIRS") {
|
||||
for dir in nu_plugin_dirs.into_list().unwrap_or(vec![]) {
|
||||
if let Ok(path) = nu_path::canonicalize_with(dir.as_str()?, &cwd)
|
||||
.and_then(|dir| nu_path::canonicalize_with(&filename.item, dir))
|
||||
{
|
||||
found = Some(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
found.ok_or(err.into_spanned(filename.span))?
|
||||
}
|
||||
};
|
||||
|
||||
let shell_expanded = shell
|
||||
.as_ref()
|
||||
.map(|s| {
|
||||
nu_path::canonicalize_with(&s.item, &cwd).map_err(|err| err.into_spanned(s.span))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Parse the plugin filename so it can be used to spawn the plugin
|
||||
let identity = PluginIdentity::new(filename_expanded, shell_expanded).map_err(|_| {
|
||||
ShellError::GenericError {
|
||||
error: "Plugin filename is invalid".into(),
|
||||
msg: "plugin executable files must start with `nu_plugin_`".into(),
|
||||
span: Some(filename.span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
}
|
||||
})?;
|
||||
|
||||
let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
|
||||
|
||||
// Start the plugin manually, to get the freshest signatures and to not affect engine
|
||||
// state. Provide a GC config that will stop it ASAP
|
||||
let plugin = Arc::new(PersistentPlugin::new(
|
||||
identity,
|
||||
PluginGcConfig {
|
||||
enabled: true,
|
||||
stop_after: 0,
|
||||
},
|
||||
));
|
||||
let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
|
||||
let commands = interface.get_signature()?;
|
||||
|
||||
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
||||
// Update the file with the received signatures
|
||||
let item = PluginCacheItem::new(plugin.identity(), commands);
|
||||
contents.upsert_plugin(item);
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(Value::nothing(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
@ -22,7 +22,7 @@ impl Command for PluginList {
|
||||
("commands".into(), Type::List(Type::String.into())),
|
||||
]),
|
||||
)
|
||||
.category(Category::Core)
|
||||
.category(Category::Plugin)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
@ -1,5 +1,15 @@
|
||||
use nu_engine::{command_prelude::*, get_full_help};
|
||||
|
||||
mod add;
|
||||
mod list;
|
||||
mod rm;
|
||||
mod stop;
|
||||
|
||||
pub use add::PluginAdd;
|
||||
pub use list::PluginList;
|
||||
pub use rm::PluginRm;
|
||||
pub use stop::PluginStop;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginCommand;
|
||||
|
||||
@ -11,7 +21,7 @@ impl Command for PluginCommand {
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build("plugin")
|
||||
.input_output_types(vec![(Type::Nothing, Type::Nothing)])
|
||||
.category(Category::Core)
|
||||
.category(Category::Plugin)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
@ -54,6 +64,16 @@ impl Command for PluginCommand {
|
||||
description: "Stop the plugin named `inc`.",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "plugin add nu_plugin_inc",
|
||||
description: "Run the `nu_plugin_inc` plugin from the current directory and install its signatures.",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "plugin rm inc",
|
||||
description: "Remove the installed signatures for the `inc` plugin.",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
100
crates/nu-cmd-plugin/src/commands/plugin/rm.rs
Normal file
100
crates/nu-cmd-plugin/src/commands/plugin/rm.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use nu_engine::command_prelude::*;
|
||||
|
||||
use crate::util::modify_plugin_file;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PluginRm;
|
||||
|
||||
impl Command for PluginRm {
|
||||
fn name(&self) -> &str {
|
||||
"plugin rm"
|
||||
}
|
||||
|
||||
fn signature(&self) -> Signature {
|
||||
Signature::build(self.name())
|
||||
.input_output_type(Type::Nothing, Type::Nothing)
|
||||
// This matches the option to `nu`
|
||||
.named(
|
||||
"plugin-config",
|
||||
SyntaxShape::Filepath,
|
||||
"Use a plugin cache file other than the one set in `$nu.plugin-path`",
|
||||
None,
|
||||
)
|
||||
.switch(
|
||||
"force",
|
||||
"Don't cause an error if the plugin name wasn't found in the file",
|
||||
Some('f'),
|
||||
)
|
||||
.required(
|
||||
"name",
|
||||
SyntaxShape::String,
|
||||
"The name of the plugin to remove (not the filename)",
|
||||
)
|
||||
.category(Category::Plugin)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
||||
"Remove a plugin from the plugin cache file."
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
r#"
|
||||
This does not remove the plugin commands from the current scope or from `plugin
|
||||
list` in the current shell. It instead removes the plugin from the plugin
|
||||
cache file (by default, `$nu.plugin-path`). The changes will be apparent the
|
||||
next time `nu` is launched with that plugin cache file.
|
||||
|
||||
This can be useful for removing an invalid plugin signature, if it can't be
|
||||
fixed with `plugin add`.
|
||||
"#
|
||||
.trim()
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["plugin", "rm", "remove", "delete", "signature"]
|
||||
}
|
||||
|
||||
fn examples(&self) -> Vec<Example> {
|
||||
vec![
|
||||
Example {
|
||||
example: "plugin rm inc",
|
||||
description: "Remove the installed signatures for the `inc` plugin.",
|
||||
result: None,
|
||||
},
|
||||
Example {
|
||||
example: "plugin rm --plugin-config polars.msgpackz polars",
|
||||
description: "Remove the installed signatures for the `polars` plugin from the \"polars.msgpackz\" plugin cache file.",
|
||||
result: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn run(
|
||||
&self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
call: &Call,
|
||||
_input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let name: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||
let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
|
||||
let force = call.has_flag(engine_state, stack, "force")?;
|
||||
|
||||
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
|
||||
if !force && !contents.plugins.iter().any(|p| p.name == name.item) {
|
||||
Err(ShellError::GenericError {
|
||||
error: format!("Failed to remove the `{}` plugin", name.item),
|
||||
msg: "couldn't find a plugin with this name in the cache file".into(),
|
||||
span: Some(name.span),
|
||||
help: None,
|
||||
inner: vec![],
|
||||
})
|
||||
} else {
|
||||
contents.remove_plugin(&name.item);
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(Value::nothing(call.head).into_pipeline_data())
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ impl Command for PluginStop {
|
||||
SyntaxShape::String,
|
||||
"The name of the plugin to stop.",
|
||||
)
|
||||
.category(Category::Core)
|
||||
.category(Category::Plugin)
|
||||
}
|
||||
|
||||
fn usage(&self) -> &str {
|
@ -31,7 +31,7 @@ impl Command for Register {
|
||||
"path of shell used to run plugin (cmd, sh, python, etc)",
|
||||
Some('s'),
|
||||
)
|
||||
.category(Category::Core)
|
||||
.category(Category::Plugin)
|
||||
}
|
||||
|
||||
fn extra_usage(&self) -> &str {
|
||||
@ -39,6 +39,10 @@ impl Command for Register {
|
||||
https://www.nushell.sh/book/thinking_in_nu.html"#
|
||||
}
|
||||
|
||||
fn search_terms(&self) -> Vec<&str> {
|
||||
vec!["plugin", "add", "register"]
|
||||
}
|
||||
|
||||
fn is_parser_keyword(&self) -> bool {
|
||||
true
|
||||
}
|
31
crates/nu-cmd-plugin/src/default_context.rs
Normal file
31
crates/nu-cmd-plugin/src/default_context.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use crate::*;
|
||||
use nu_protocol::engine::{EngineState, StateWorkingSet};
|
||||
|
||||
pub fn add_plugin_command_context(mut engine_state: EngineState) -> EngineState {
|
||||
let delta = {
|
||||
let mut working_set = StateWorkingSet::new(&engine_state);
|
||||
|
||||
macro_rules! bind_command {
|
||||
( $( $command:expr ),* $(,)? ) => {
|
||||
$( working_set.add_decl(Box::new($command)); )*
|
||||
};
|
||||
}
|
||||
|
||||
bind_command!(
|
||||
PluginCommand,
|
||||
PluginAdd,
|
||||
PluginList,
|
||||
PluginRm,
|
||||
PluginStop,
|
||||
Register,
|
||||
);
|
||||
|
||||
working_set.render()
|
||||
};
|
||||
|
||||
if let Err(err) = engine_state.merge_delta(delta) {
|
||||
eprintln!("Error creating default context: {err:?}");
|
||||
}
|
||||
|
||||
engine_state
|
||||
}
|
8
crates/nu-cmd-plugin/src/lib.rs
Normal file
8
crates/nu-cmd-plugin/src/lib.rs
Normal file
@ -0,0 +1,8 @@
|
||||
//! Nushell commands for managing plugins.
|
||||
|
||||
mod commands;
|
||||
mod default_context;
|
||||
mod util;
|
||||
|
||||
pub use commands::*;
|
||||
pub use default_context::*;
|
50
crates/nu-cmd-plugin/src/util.rs
Normal file
50
crates/nu-cmd-plugin/src/util.rs
Normal file
@ -0,0 +1,50 @@
|
||||
use std::fs::{self, File};
|
||||
|
||||
use nu_engine::{command_prelude::*, current_dir};
|
||||
use nu_protocol::PluginCacheFile;
|
||||
|
||||
pub(crate) fn modify_plugin_file(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
span: Span,
|
||||
custom_path: Option<Spanned<String>>,
|
||||
operate: impl FnOnce(&mut PluginCacheFile) -> Result<(), ShellError>,
|
||||
) -> Result<(), ShellError> {
|
||||
let cwd = current_dir(engine_state, stack)?;
|
||||
|
||||
let plugin_cache_file_path = if let Some(ref custom_path) = custom_path {
|
||||
nu_path::expand_path_with(&custom_path.item, cwd, true)
|
||||
} else {
|
||||
engine_state
|
||||
.plugin_path
|
||||
.clone()
|
||||
.ok_or_else(|| ShellError::GenericError {
|
||||
error: "Plugin cache file not set".into(),
|
||||
msg: "pass --plugin-config explicitly here".into(),
|
||||
span: Some(span),
|
||||
help: Some("you may be running `nu` with --no-config-file".into()),
|
||||
inner: vec![],
|
||||
})?
|
||||
};
|
||||
|
||||
// Try to read the plugin file if it exists
|
||||
let mut contents = if fs::metadata(&plugin_cache_file_path).is_ok_and(|m| m.len() > 0) {
|
||||
PluginCacheFile::read_from(
|
||||
File::open(&plugin_cache_file_path).map_err(|err| err.into_spanned(span))?,
|
||||
Some(span),
|
||||
)?
|
||||
} else {
|
||||
PluginCacheFile::default()
|
||||
};
|
||||
|
||||
// Do the operation
|
||||
operate(&mut contents)?;
|
||||
|
||||
// Save the modified file on success
|
||||
contents.write_to(
|
||||
File::create(&plugin_cache_file_path).map_err(|err| err.into_spanned(span))?,
|
||||
Some(span),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -3550,7 +3550,8 @@ pub fn parse_where(working_set: &mut StateWorkingSet, lite_command: &LiteCommand
|
||||
pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline {
|
||||
use nu_plugin::{get_signature, PersistentPlugin, PluginDeclaration};
|
||||
use nu_protocol::{
|
||||
engine::Stack, IntoSpanned, PluginIdentity, PluginSignature, RegisteredPlugin,
|
||||
engine::Stack, IntoSpanned, PluginCacheItem, PluginIdentity, PluginSignature,
|
||||
RegisteredPlugin,
|
||||
};
|
||||
|
||||
let spans = &lite_command.parts;
|
||||
@ -3743,10 +3744,10 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm
|
||||
)
|
||||
});
|
||||
|
||||
if signatures.is_ok() {
|
||||
// mark plugins file as dirty only when the user is registering plugins
|
||||
// and not when we evaluate plugin.nu on shell startup
|
||||
working_set.mark_plugins_file_dirty();
|
||||
if let Ok(ref signatures) = signatures {
|
||||
// Add the loaded plugin to the delta
|
||||
working_set
|
||||
.update_plugin_cache(PluginCacheItem::new(&identity, signatures.clone()));
|
||||
}
|
||||
|
||||
signatures
|
||||
|
@ -78,10 +78,10 @@ pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer};
|
||||
// Used by other nu crates.
|
||||
#[doc(hidden)]
|
||||
pub use plugin::{
|
||||
create_plugin_signature, get_signature, serve_plugin_io, EngineInterfaceManager, GetPlugin,
|
||||
Interface, InterfaceManager, PersistentPlugin, PluginDeclaration,
|
||||
PluginExecutionCommandContext, PluginExecutionContext, PluginInterface, PluginInterfaceManager,
|
||||
PluginSource, ServePluginError,
|
||||
create_plugin_signature, get_signature, load_plugin_cache_item, load_plugin_file,
|
||||
serve_plugin_io, EngineInterfaceManager, GetPlugin, Interface, InterfaceManager,
|
||||
PersistentPlugin, PluginDeclaration, PluginExecutionCommandContext, PluginExecutionContext,
|
||||
PluginInterface, PluginInterfaceManager, PluginSource, ServePluginError,
|
||||
};
|
||||
#[doc(hidden)]
|
||||
pub use protocol::{PluginCustomValue, PluginInput, PluginOutput};
|
||||
|
@ -750,15 +750,7 @@ impl PluginInterface {
|
||||
help: Some(format!(
|
||||
"the plugin may have experienced an error. Try registering the plugin again \
|
||||
with `{}`",
|
||||
if let Some(shell) = self.state.source.shell() {
|
||||
format!(
|
||||
"register --shell '{}' '{}'",
|
||||
shell.display(),
|
||||
self.state.source.filename().display(),
|
||||
)
|
||||
} else {
|
||||
format!("register '{}'", self.state.source.filename().display())
|
||||
}
|
||||
self.state.source.identity.register_command(),
|
||||
)),
|
||||
inner: vec![],
|
||||
})?;
|
||||
|
@ -23,8 +23,9 @@ use std::{
|
||||
|
||||
use nu_engine::documentation::get_flags_section;
|
||||
use nu_protocol::{
|
||||
ast::Operator, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginSignature,
|
||||
ShellError, Spanned, Value,
|
||||
ast::Operator, engine::StateWorkingSet, report_error_new, CustomValue, IntoSpanned,
|
||||
LabeledError, PipelineData, PluginCacheFile, PluginCacheItem, PluginCacheItemData,
|
||||
PluginIdentity, PluginSignature, ShellError, Span, Spanned, Value,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
@ -919,3 +920,79 @@ pub fn get_plugin_encoding(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Load the definitions from the plugin file into the engine state
|
||||
#[doc(hidden)]
|
||||
pub fn load_plugin_file(
|
||||
working_set: &mut StateWorkingSet,
|
||||
plugin_cache_file: &PluginCacheFile,
|
||||
span: Option<Span>,
|
||||
) {
|
||||
for plugin in &plugin_cache_file.plugins {
|
||||
// Any errors encountered should just be logged.
|
||||
if let Err(err) = load_plugin_cache_item(working_set, plugin, span) {
|
||||
report_error_new(working_set.permanent_state, &err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a definition from the plugin file into the engine state
|
||||
#[doc(hidden)]
|
||||
pub fn load_plugin_cache_item(
|
||||
working_set: &mut StateWorkingSet,
|
||||
plugin: &PluginCacheItem,
|
||||
span: Option<Span>,
|
||||
) -> Result<(), ShellError> {
|
||||
let identity =
|
||||
PluginIdentity::new(plugin.filename.clone(), plugin.shell.clone()).map_err(|_| {
|
||||
ShellError::GenericError {
|
||||
error: "Invalid plugin filename in plugin cache file".into(),
|
||||
msg: "loaded from here".into(),
|
||||
span,
|
||||
help: Some(format!(
|
||||
"the filename for `{}` is not a valid nushell plugin: {}",
|
||||
plugin.name,
|
||||
plugin.filename.display()
|
||||
)),
|
||||
inner: vec![],
|
||||
}
|
||||
})?;
|
||||
|
||||
match &plugin.data {
|
||||
PluginCacheItemData::Valid { commands } => {
|
||||
// Find garbage collection config for the plugin
|
||||
let gc_config = working_set
|
||||
.get_config()
|
||||
.plugin_gc
|
||||
.get(identity.name())
|
||||
.clone();
|
||||
|
||||
// Add it to / get it from the working set
|
||||
let plugin = working_set.find_or_create_plugin(&identity, || {
|
||||
Arc::new(PersistentPlugin::new(identity.clone(), gc_config.clone()))
|
||||
});
|
||||
|
||||
// Downcast the plugin to `PersistentPlugin` - we generally expect this to succeed.
|
||||
// The trait object only exists so that nu-protocol can contain plugins without knowing
|
||||
// anything about their implementation, but we only use `PersistentPlugin` in practice.
|
||||
let plugin: Arc<PersistentPlugin> =
|
||||
plugin
|
||||
.as_any()
|
||||
.downcast()
|
||||
.map_err(|_| ShellError::NushellFailed {
|
||||
msg: "encountered unexpected RegisteredPlugin type".into(),
|
||||
})?;
|
||||
|
||||
// Create the declarations from the commands
|
||||
for signature in commands {
|
||||
let decl = PluginDeclaration::new(plugin.clone(), signature.clone());
|
||||
working_set.add_decl(Box::new(decl));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
PluginCacheItemData::Invalid => Err(ShellError::PluginCacheDataInvalid {
|
||||
plugin_name: identity.name().to_owned(),
|
||||
register_command: identity.register_command(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ nu-utils = { path = "../nu-utils", version = "0.92.3" }
|
||||
nu-path = { path = "../nu-path", version = "0.92.3" }
|
||||
nu-system = { path = "../nu-system", version = "0.92.3" }
|
||||
|
||||
brotli = { workspace = true, optional = true }
|
||||
byte-unit = { version = "5.1", features = [ "serde" ] }
|
||||
chrono = { workspace = true, features = [ "serde", "std", "unstable-locales" ], default-features = false }
|
||||
chrono-humanize = { workspace = true }
|
||||
@ -25,19 +26,25 @@ indexmap = { workspace = true }
|
||||
lru = { workspace = true }
|
||||
miette = { workspace = true, features = ["fancy-no-backtrace"] }
|
||||
num-format = { workspace = true }
|
||||
rmp-serde = { workspace = true, optional = true }
|
||||
serde = { workspace = true, default-features = false }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
thiserror = "1.0"
|
||||
typetag = "0.2"
|
||||
|
||||
[features]
|
||||
plugin = ["serde_json"]
|
||||
plugin = [
|
||||
"brotli",
|
||||
"rmp-serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
nu-test-support = { path = "../nu-test-support", version = "0.92.3" }
|
||||
pretty_assertions = "1.0"
|
||||
rstest = { workspace = true }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
|
@ -24,7 +24,7 @@ use std::{
|
||||
type PoisonDebuggerError<'a> = PoisonError<MutexGuard<'a, Box<dyn Debugger>>>;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::RegisteredPlugin;
|
||||
use crate::{PluginCacheFile, PluginCacheItem, RegisteredPlugin};
|
||||
|
||||
pub static PWD_ENV: &str = "PWD";
|
||||
|
||||
@ -92,7 +92,7 @@ pub struct EngineState {
|
||||
pub repl_state: Arc<Mutex<ReplState>>,
|
||||
pub table_decl_id: Option<usize>,
|
||||
#[cfg(feature = "plugin")]
|
||||
pub plugin_signatures: Option<PathBuf>,
|
||||
pub plugin_path: Option<PathBuf>,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins: Vec<Arc<dyn RegisteredPlugin>>,
|
||||
config_path: HashMap<String, PathBuf>,
|
||||
@ -155,7 +155,7 @@ impl EngineState {
|
||||
})),
|
||||
table_decl_id: None,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_signatures: None,
|
||||
plugin_path: None,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins: vec![],
|
||||
config_path: HashMap::new(),
|
||||
@ -255,7 +255,7 @@ impl EngineState {
|
||||
if let Some(existing) = self
|
||||
.plugins
|
||||
.iter_mut()
|
||||
.find(|p| p.identity() == plugin.identity())
|
||||
.find(|p| p.identity().name() == plugin.identity().name())
|
||||
{
|
||||
// Stop the existing plugin, so that the new plugin definitely takes over
|
||||
existing.stop()?;
|
||||
@ -267,10 +267,10 @@ impl EngineState {
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
if delta.plugins_changed {
|
||||
if !delta.plugin_cache_items.is_empty() {
|
||||
// Update the plugin file with the new signatures.
|
||||
if self.plugin_signatures.is_some() {
|
||||
self.update_plugin_file()?;
|
||||
if self.plugin_path.is_some() {
|
||||
self.update_plugin_file(std::mem::take(&mut delta.plugin_cache_items))?;
|
||||
}
|
||||
}
|
||||
|
||||
@ -480,94 +480,59 @@ impl EngineState {
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn update_plugin_file(&self) -> Result<(), ShellError> {
|
||||
use std::io::Write;
|
||||
|
||||
use crate::{PluginExample, PluginSignature};
|
||||
|
||||
pub fn update_plugin_file(
|
||||
&self,
|
||||
updated_items: Vec<PluginCacheItem>,
|
||||
) -> Result<(), ShellError> {
|
||||
// Updating the signatures plugin file with the added signatures
|
||||
self.plugin_signatures
|
||||
use std::fs::File;
|
||||
|
||||
let plugin_path = self
|
||||
.plugin_path
|
||||
.as_ref()
|
||||
.ok_or_else(|| ShellError::PluginFailedToLoad {
|
||||
msg: "Plugin file not found".into(),
|
||||
})
|
||||
.and_then(|plugin_path| {
|
||||
// Always create the file, which will erase previous signatures
|
||||
std::fs::File::create(plugin_path.as_path()).map_err(|err| {
|
||||
ShellError::PluginFailedToLoad {
|
||||
msg: err.to_string(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.and_then(|mut plugin_file| {
|
||||
// Plugin definitions with parsed signature
|
||||
self.plugin_decls().try_for_each(|decl| {
|
||||
// A successful plugin registration already includes the plugin filename
|
||||
// No need to check the None option
|
||||
let identity = decl.plugin_identity().expect("plugin should have identity");
|
||||
let mut file_name = identity
|
||||
.filename()
|
||||
.to_str()
|
||||
.expect("path was checked during registration as a str")
|
||||
.to_string();
|
||||
.ok_or_else(|| ShellError::GenericError {
|
||||
error: "Plugin file path not set".into(),
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: Some("you may be running nu with --no-config-file".into()),
|
||||
inner: vec![],
|
||||
})?;
|
||||
|
||||
// Fix files or folders with quotes
|
||||
if file_name.contains('\'')
|
||||
|| file_name.contains('"')
|
||||
|| file_name.contains(' ')
|
||||
{
|
||||
file_name = format!("`{file_name}`");
|
||||
}
|
||||
|
||||
let sig = decl.signature();
|
||||
let examples = decl
|
||||
.examples()
|
||||
.into_iter()
|
||||
.map(PluginExample::from)
|
||||
.collect();
|
||||
let sig_with_examples = PluginSignature::new(sig, examples);
|
||||
serde_json::to_string_pretty(&sig_with_examples)
|
||||
.map(|signature| {
|
||||
// Extracting the possible path to the shell used to load the plugin
|
||||
let shell_str = identity
|
||||
.shell()
|
||||
.map(|path| {
|
||||
format!(
|
||||
"-s {}",
|
||||
path.to_str().expect(
|
||||
"shell path was checked during registration as a str"
|
||||
)
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
// Each signature is stored in the plugin file with the shell and signature
|
||||
// This information will be used when loading the plugin
|
||||
// information when nushell starts
|
||||
format!("register {file_name} {shell_str} {signature}\n\n")
|
||||
})
|
||||
.map_err(|err| ShellError::PluginFailedToLoad {
|
||||
msg: err.to_string(),
|
||||
})
|
||||
.and_then(|line| {
|
||||
plugin_file.write_all(line.as_bytes()).map_err(|err| {
|
||||
ShellError::PluginFailedToLoad {
|
||||
msg: err.to_string(),
|
||||
}
|
||||
})
|
||||
})
|
||||
.and_then(|_| {
|
||||
plugin_file.flush().map_err(|err| ShellError::GenericError {
|
||||
error: "Error flushing plugin file".into(),
|
||||
msg: format! {"{err}"},
|
||||
// Read the current contents of the plugin file if it exists
|
||||
let mut contents = match File::open(plugin_path.as_path()) {
|
||||
Ok(mut plugin_file) => PluginCacheFile::read_from(&mut plugin_file, None),
|
||||
Err(err) => {
|
||||
if err.kind() == std::io::ErrorKind::NotFound {
|
||||
Ok(PluginCacheFile::default())
|
||||
} else {
|
||||
Err(ShellError::GenericError {
|
||||
error: "Failed to open plugin file".into(),
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![],
|
||||
})
|
||||
})
|
||||
})
|
||||
inner: vec![err.into()],
|
||||
})
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
// Update the given signatures
|
||||
for item in updated_items {
|
||||
contents.upsert_plugin(item);
|
||||
}
|
||||
|
||||
// Write it to the same path
|
||||
let plugin_file =
|
||||
File::create(plugin_path.as_path()).map_err(|err| ShellError::GenericError {
|
||||
error: "Failed to write plugin file".into(),
|
||||
msg: "".into(),
|
||||
span: None,
|
||||
help: None,
|
||||
inner: vec![err.into()],
|
||||
})?;
|
||||
|
||||
contents.write_to(plugin_file, None)
|
||||
}
|
||||
|
||||
/// Update plugins with new garbage collection config
|
||||
#[cfg(feature = "plugin")]
|
||||
|
@ -9,7 +9,7 @@ use crate::{
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::RegisteredPlugin;
|
||||
use crate::{PluginCacheItem, RegisteredPlugin};
|
||||
|
||||
/// A delta (or change set) between the current global state and a possible future global state. Deltas
|
||||
/// can be applied to the global state to update it to contain both previous state and the state held
|
||||
@ -24,9 +24,9 @@ pub struct StateDelta {
|
||||
pub(super) usage: Usage,
|
||||
pub scope: Vec<ScopeFrame>,
|
||||
#[cfg(feature = "plugin")]
|
||||
pub(super) plugins_changed: bool, // marks whether plugin file should be updated
|
||||
#[cfg(feature = "plugin")]
|
||||
pub(super) plugins: Vec<Arc<dyn RegisteredPlugin>>,
|
||||
#[cfg(feature = "plugin")]
|
||||
pub(super) plugin_cache_items: Vec<PluginCacheItem>,
|
||||
}
|
||||
|
||||
impl StateDelta {
|
||||
@ -48,9 +48,9 @@ impl StateDelta {
|
||||
scope: vec![scope_frame],
|
||||
usage: Usage::new(),
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins_changed: false,
|
||||
#[cfg(feature = "plugin")]
|
||||
plugins: vec![],
|
||||
#[cfg(feature = "plugin")]
|
||||
plugin_cache_items: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ use std::{
|
||||
};
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
use crate::{PluginIdentity, RegisteredPlugin};
|
||||
use crate::{PluginCacheItem, PluginIdentity, RegisteredPlugin};
|
||||
|
||||
/// A temporary extension to the global state. This handles bridging between the global state and the
|
||||
/// additional declarations and scope changes that are not yet part of the global scope.
|
||||
@ -159,11 +159,6 @@ impl<'a> StateWorkingSet<'a> {
|
||||
.insert(name, decl_id)
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn mark_plugins_file_dirty(&mut self) {
|
||||
self.delta.plugins_changed = true;
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn find_or_create_plugin(
|
||||
&mut self,
|
||||
@ -186,6 +181,11 @@ impl<'a> StateWorkingSet<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
pub fn update_plugin_cache(&mut self, item: PluginCacheItem) {
|
||||
self.delta.plugin_cache_items.push(item);
|
||||
}
|
||||
|
||||
pub fn merge_predecl(&mut self, name: &[u8]) -> Option<DeclId> {
|
||||
self.move_predecls_to_overlay();
|
||||
|
||||
|
@ -750,6 +750,19 @@ pub enum ShellError {
|
||||
span: Span,
|
||||
},
|
||||
|
||||
/// The cached plugin data (in `$nu.plugin-path`) for a plugin is invalid.
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// `register` the plugin again to update the data, or remove it.
|
||||
#[error("The cached plugin data for `{plugin_name}` is invalid")]
|
||||
#[diagnostic(code(nu::shell::plugin_cache_data_invalid))]
|
||||
PluginCacheDataInvalid {
|
||||
plugin_name: String,
|
||||
#[help("try registering the plugin again with `{}`")]
|
||||
register_command: String,
|
||||
},
|
||||
|
||||
/// A plugin failed to load.
|
||||
///
|
||||
/// ## Resolution
|
||||
|
@ -116,7 +116,7 @@ pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Valu
|
||||
{
|
||||
record.push(
|
||||
"plugin-path",
|
||||
if let Some(path) = &engine_state.plugin_signatures {
|
||||
if let Some(path) = &engine_state.plugin_path {
|
||||
let canon_plugin_path = canonicalize_path(engine_state, path);
|
||||
Value::string(canon_plugin_path.to_string_lossy(), span)
|
||||
} else {
|
||||
@ -124,7 +124,7 @@ pub fn create_nu_constant(engine_state: &EngineState, span: Span) -> Result<Valu
|
||||
config_path.clone().map_or_else(
|
||||
|e| e,
|
||||
|mut path| {
|
||||
path.push("plugin.nu");
|
||||
path.push("plugin.msgpackz");
|
||||
let canonical_plugin_path = canonicalize_path(engine_state, &path);
|
||||
Value::string(canonical_plugin_path.to_string_lossy(), span)
|
||||
},
|
||||
|
@ -13,7 +13,7 @@ pub struct Example<'a> {
|
||||
// and `description` fields, because these information is fetched from plugin, a third party
|
||||
// binary, nushell have no way to construct it directly.
|
||||
#[cfg(feature = "plugin")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PluginExample {
|
||||
pub example: String,
|
||||
pub description: String,
|
||||
|
175
crates/nu-protocol/src/plugin/cache_file/mod.rs
Normal file
175
crates/nu-protocol/src/plugin/cache_file/mod.rs
Normal file
@ -0,0 +1,175 @@
|
||||
use std::{
|
||||
io::{Read, Write},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{PluginIdentity, PluginSignature, ShellError, Span};
|
||||
|
||||
// This has a big impact on performance
|
||||
const BUFFER_SIZE: usize = 65536;
|
||||
|
||||
// Chose settings at the low end, because we're just trying to get the maximum speed
|
||||
const COMPRESSION_QUALITY: u32 = 1;
|
||||
const WIN_SIZE: u32 = 20; // recommended 20-22
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PluginCacheFile {
|
||||
/// The Nushell version that last updated the file.
|
||||
pub nushell_version: String,
|
||||
|
||||
/// The installed plugins.
|
||||
pub plugins: Vec<PluginCacheItem>,
|
||||
}
|
||||
|
||||
impl Default for PluginCacheFile {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl PluginCacheFile {
|
||||
/// Create a new, empty plugin cache file.
|
||||
pub fn new() -> PluginCacheFile {
|
||||
PluginCacheFile {
|
||||
nushell_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
plugins: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the plugin cache file from a reader, e.g. [`File`](std::fs::File).
|
||||
pub fn read_from(
|
||||
reader: impl Read,
|
||||
error_span: Option<Span>,
|
||||
) -> Result<PluginCacheFile, ShellError> {
|
||||
// Format is brotli compressed messagepack
|
||||
let brotli_reader = brotli::Decompressor::new(reader, BUFFER_SIZE);
|
||||
|
||||
rmp_serde::from_read(brotli_reader).map_err(|err| ShellError::GenericError {
|
||||
error: format!("Failed to load plugin file: {err}"),
|
||||
msg: "plugin file load attempted here".into(),
|
||||
span: error_span,
|
||||
help: Some(
|
||||
"it may be corrupt. Try deleting it and registering your plugins again".into(),
|
||||
),
|
||||
inner: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
/// Write the plugin cache file to a writer, e.g. [`File`](std::fs::File).
|
||||
///
|
||||
/// The `nushell_version` will be updated to the current version before writing.
|
||||
pub fn write_to(
|
||||
&mut self,
|
||||
writer: impl Write,
|
||||
error_span: Option<Span>,
|
||||
) -> Result<(), ShellError> {
|
||||
// Update the Nushell version before writing
|
||||
self.nushell_version = env!("CARGO_PKG_VERSION").to_owned();
|
||||
|
||||
// Format is brotli compressed messagepack
|
||||
let mut brotli_writer =
|
||||
brotli::CompressorWriter::new(writer, BUFFER_SIZE, COMPRESSION_QUALITY, WIN_SIZE);
|
||||
|
||||
rmp_serde::encode::write_named(&mut brotli_writer, self)
|
||||
.map_err(|err| err.to_string())
|
||||
.and_then(|_| brotli_writer.flush().map_err(|err| err.to_string()))
|
||||
.map_err(|err| ShellError::GenericError {
|
||||
error: "Failed to save plugin file".into(),
|
||||
msg: "plugin file save attempted here".into(),
|
||||
span: error_span,
|
||||
help: Some(err.to_string()),
|
||||
inner: vec![],
|
||||
})
|
||||
}
|
||||
|
||||
/// Insert or update a plugin in the plugin cache file.
|
||||
pub fn upsert_plugin(&mut self, item: PluginCacheItem) {
|
||||
if let Some(existing_item) = self.plugins.iter_mut().find(|p| p.name == item.name) {
|
||||
*existing_item = item;
|
||||
} else {
|
||||
self.plugins.push(item);
|
||||
|
||||
// Sort the plugins for consistency
|
||||
self.plugins
|
||||
.sort_by(|item1, item2| item1.name.cmp(&item2.name));
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a plugin from the plugin cache file by name.
|
||||
pub fn remove_plugin(&mut self, name: &str) {
|
||||
self.plugins.retain_mut(|item| item.name != name)
|
||||
}
|
||||
}
|
||||
|
||||
/// A single plugin definition from a [`PluginCacheFile`].
|
||||
///
|
||||
/// Contains the information necessary for the [`PluginIdentity`], as well as possibly valid data
|
||||
/// about the plugin including the cached command signatures.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PluginCacheItem {
|
||||
/// The name of the plugin, as would show in `plugin list`. This does not include the file
|
||||
/// extension or the `nu_plugin_` prefix.
|
||||
pub name: String,
|
||||
|
||||
/// The path to the file.
|
||||
pub filename: PathBuf,
|
||||
|
||||
/// The shell program used to run the plugin, if applicable.
|
||||
pub shell: Option<PathBuf>,
|
||||
|
||||
/// Additional data that might be invalid so that we don't fail to load the whole plugin file
|
||||
/// if there's a deserialization error.
|
||||
#[serde(flatten)]
|
||||
pub data: PluginCacheItemData,
|
||||
}
|
||||
|
||||
impl PluginCacheItem {
|
||||
/// Create a [`PluginCacheItem`] from an identity and signatures.
|
||||
pub fn new(identity: &PluginIdentity, mut commands: Vec<PluginSignature>) -> PluginCacheItem {
|
||||
// Sort the commands for consistency
|
||||
commands.sort_by(|cmd1, cmd2| cmd1.sig.name.cmp(&cmd2.sig.name));
|
||||
|
||||
PluginCacheItem {
|
||||
name: identity.name().to_owned(),
|
||||
filename: identity.filename().to_owned(),
|
||||
shell: identity.shell().map(|p| p.to_owned()),
|
||||
data: PluginCacheItemData::Valid { commands },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Possibly valid data about a plugin in a [`PluginCacheFile`]. If deserialization fails, it will
|
||||
/// be `Invalid`.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum PluginCacheItemData {
|
||||
Valid {
|
||||
/// Signatures and examples for each command provided by the plugin.
|
||||
commands: Vec<PluginSignature>,
|
||||
},
|
||||
#[serde(
|
||||
serialize_with = "serialize_invalid",
|
||||
deserialize_with = "deserialize_invalid"
|
||||
)]
|
||||
Invalid,
|
||||
}
|
||||
|
||||
fn serialize_invalid<S>(serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
().serialize(serializer)
|
||||
}
|
||||
|
||||
fn deserialize_invalid<'de, D>(deserializer: D) -> Result<(), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
serde::de::IgnoredAny::deserialize(deserializer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
120
crates/nu-protocol/src/plugin/cache_file/tests.rs
Normal file
120
crates/nu-protocol/src/plugin/cache_file/tests.rs
Normal file
@ -0,0 +1,120 @@
|
||||
use super::{PluginCacheFile, PluginCacheItem, PluginCacheItemData};
|
||||
use crate::{
|
||||
Category, PluginExample, PluginSignature, ShellError, Signature, SyntaxShape, Type, Value,
|
||||
};
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io::Cursor;
|
||||
|
||||
fn foo_plugin() -> PluginCacheItem {
|
||||
PluginCacheItem {
|
||||
name: "foo".into(),
|
||||
filename: "/path/to/nu_plugin_foo".into(),
|
||||
shell: None,
|
||||
data: PluginCacheItemData::Valid {
|
||||
commands: vec![PluginSignature {
|
||||
sig: Signature::new("foo")
|
||||
.input_output_type(Type::Int, Type::List(Box::new(Type::Int)))
|
||||
.category(Category::Experimental),
|
||||
examples: vec![PluginExample {
|
||||
example: "16 | foo".into(),
|
||||
description: "powers of two up to 16".into(),
|
||||
result: Some(Value::test_list(vec![
|
||||
Value::test_int(2),
|
||||
Value::test_int(4),
|
||||
Value::test_int(8),
|
||||
Value::test_int(16),
|
||||
])),
|
||||
}],
|
||||
}],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn bar_plugin() -> PluginCacheItem {
|
||||
PluginCacheItem {
|
||||
name: "bar".into(),
|
||||
filename: "/path/to/nu_plugin_bar".into(),
|
||||
shell: None,
|
||||
data: PluginCacheItemData::Valid {
|
||||
commands: vec![PluginSignature {
|
||||
sig: Signature::new("bar")
|
||||
.usage("overwrites files with random data")
|
||||
.switch("force", "ignore errors", Some('f'))
|
||||
.required(
|
||||
"path",
|
||||
SyntaxShape::Filepath,
|
||||
"file to overwrite with random data",
|
||||
)
|
||||
.category(Category::Experimental),
|
||||
examples: vec![],
|
||||
}],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() -> Result<(), ShellError> {
|
||||
let mut plugin_cache_file = PluginCacheFile {
|
||||
nushell_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
plugins: vec![foo_plugin(), bar_plugin()],
|
||||
};
|
||||
|
||||
let mut output = vec![];
|
||||
|
||||
plugin_cache_file.write_to(&mut output, None)?;
|
||||
|
||||
let read_file = PluginCacheFile::read_from(Cursor::new(&output[..]), None)?;
|
||||
|
||||
assert_eq!(plugin_cache_file, read_file);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_invalid() -> Result<(), ShellError> {
|
||||
let mut plugin_cache_file = PluginCacheFile {
|
||||
nushell_version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
plugins: vec![PluginCacheItem {
|
||||
name: "invalid".into(),
|
||||
filename: "/path/to/nu_plugin_invalid".into(),
|
||||
shell: None,
|
||||
data: PluginCacheItemData::Invalid,
|
||||
}],
|
||||
};
|
||||
|
||||
let mut output = vec![];
|
||||
|
||||
plugin_cache_file.write_to(&mut output, None)?;
|
||||
|
||||
let read_file = PluginCacheFile::read_from(Cursor::new(&output[..]), None)?;
|
||||
|
||||
assert_eq!(plugin_cache_file, read_file);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upsert_new() {
|
||||
let mut file = PluginCacheFile::new();
|
||||
|
||||
file.plugins.push(foo_plugin());
|
||||
|
||||
file.upsert_plugin(bar_plugin());
|
||||
|
||||
assert_eq!(2, file.plugins.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upsert_replace() {
|
||||
let mut file = PluginCacheFile::new();
|
||||
|
||||
file.plugins.push(foo_plugin());
|
||||
|
||||
let mut mutated_foo = foo_plugin();
|
||||
mutated_foo.shell = Some("/bin/sh".into());
|
||||
|
||||
file.upsert_plugin(mutated_foo);
|
||||
|
||||
assert_eq!(1, file.plugins.len());
|
||||
assert_eq!(Some("/bin/sh".into()), file.plugins[0].shell);
|
||||
}
|
@ -88,6 +88,19 @@ impl PluginIdentity {
|
||||
PluginIdentity::new(format!(r"/fake/path/nu_plugin_{name}"), None)
|
||||
.expect("fake plugin identity path is invalid")
|
||||
}
|
||||
|
||||
/// A command that could be used to register the plugin, for suggesting in errors.
|
||||
pub fn register_command(&self) -> String {
|
||||
if let Some(shell) = self.shell() {
|
||||
format!(
|
||||
"register --shell '{}' '{}'",
|
||||
shell.display(),
|
||||
self.filename().display(),
|
||||
)
|
||||
} else {
|
||||
format!("register '{}'", self.filename().display())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1,7 +1,9 @@
|
||||
mod cache_file;
|
||||
mod identity;
|
||||
mod registered;
|
||||
mod signature;
|
||||
|
||||
pub use cache_file::*;
|
||||
pub use identity::*;
|
||||
pub use registered::*;
|
||||
pub use signature::*;
|
||||
|
@ -2,7 +2,7 @@ use crate::{PluginExample, Signature};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A simple wrapper for Signature that includes examples.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct PluginSignature {
|
||||
pub sig: Signature,
|
||||
pub examples: Vec<PluginExample>,
|
||||
|
@ -56,6 +56,7 @@ pub enum Category {
|
||||
Network,
|
||||
Path,
|
||||
Platform,
|
||||
Plugin,
|
||||
Random,
|
||||
Shells,
|
||||
Strings,
|
||||
@ -90,6 +91,7 @@ impl std::fmt::Display for Category {
|
||||
Category::Network => "network",
|
||||
Category::Path => "path",
|
||||
Category::Platform => "platform",
|
||||
Category::Plugin => "plugin",
|
||||
Category::Random => "random",
|
||||
Category::Shells => "shells",
|
||||
Category::Strings => "strings",
|
||||
|
@ -202,11 +202,11 @@ macro_rules! nu_with_std {
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! nu_with_plugins {
|
||||
(cwd: $cwd:expr, plugins: [$(($plugin_name:expr)),+$(,)?], $command:expr) => {{
|
||||
(cwd: $cwd:expr, plugins: [$(($plugin_name:expr)),*$(,)?], $command:expr) => {{
|
||||
nu_with_plugins!(
|
||||
cwd: $cwd,
|
||||
envs: Vec::<(&str, &str)>::new(),
|
||||
plugins: [$(($plugin_name)),+],
|
||||
plugins: [$(($plugin_name)),*],
|
||||
$command
|
||||
)
|
||||
}};
|
||||
@ -222,10 +222,10 @@ macro_rules! nu_with_plugins {
|
||||
(
|
||||
cwd: $cwd:expr,
|
||||
envs: $envs:expr,
|
||||
plugins: [$(($plugin_name:expr)),+$(,)?],
|
||||
plugins: [$(($plugin_name:expr)),*$(,)?],
|
||||
$command:expr
|
||||
) => {{
|
||||
$crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$($plugin_name),+], $command)
|
||||
$crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$($plugin_name),*], $command)
|
||||
}};
|
||||
(cwd: $cwd:expr, envs: $envs:expr, plugin: ($plugin_name:expr), $command:expr) => {{
|
||||
$crate::macros::nu_with_plugin_run_test($cwd, $envs, &[$plugin_name], $command)
|
||||
@ -329,13 +329,15 @@ where
|
||||
});
|
||||
|
||||
let temp = tempdir().expect("couldn't create a temporary directory");
|
||||
let [temp_config_file, temp_env_config_file, temp_plugin_file] =
|
||||
["config.nu", "env.nu", "plugin.nu"].map(|name| {
|
||||
let [temp_config_file, temp_env_config_file] = ["config.nu", "env.nu"].map(|name| {
|
||||
let temp_file = temp.path().join(name);
|
||||
std::fs::File::create(&temp_file).expect("couldn't create temporary config file");
|
||||
temp_file
|
||||
});
|
||||
|
||||
// We don't have to write the plugin cache file, it's ok for it to not exist
|
||||
let temp_plugin_file = temp.path().join("plugin.msgpackz");
|
||||
|
||||
crate::commands::ensure_plugins_built();
|
||||
|
||||
let registrations: String = plugins
|
||||
|
@ -340,7 +340,7 @@ impl Command for Nu {
|
||||
signature = signature.named(
|
||||
"plugin-config",
|
||||
SyntaxShape::String,
|
||||
"start with an alternate plugin signature file",
|
||||
"start with an alternate plugin cache file",
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
@ -214,7 +214,7 @@ pub(crate) fn setup_config(
|
||||
);
|
||||
let result = catch_unwind(AssertUnwindSafe(|| {
|
||||
#[cfg(feature = "plugin")]
|
||||
read_plugin_file(engine_state, stack, plugin_file, NUSHELL_FOLDER);
|
||||
read_plugin_file(engine_state, plugin_file, NUSHELL_FOLDER);
|
||||
|
||||
read_config_file(engine_state, stack, env_file, true);
|
||||
read_config_file(engine_state, stack, config_file, false);
|
||||
|
@ -43,6 +43,8 @@ use std::{
|
||||
|
||||
fn get_engine_state() -> EngineState {
|
||||
let engine_state = nu_cmd_lang::create_default_context();
|
||||
#[cfg(feature = "plugin")]
|
||||
let engine_state = nu_cmd_plugin::add_plugin_command_context(engine_state);
|
||||
let engine_state = nu_command::add_shell_command_context(engine_state);
|
||||
let engine_state = nu_cmd_extra::add_extra_command_context(engine_state);
|
||||
#[cfg(feature = "dataframe")]
|
||||
|
14
src/run.rs
14
src/run.rs
@ -30,12 +30,7 @@ pub(crate) fn run_commands(
|
||||
// if the --no-config-file(-n) flag is passed, do not load plugin, env, or config files
|
||||
if parsed_nu_cli_args.no_config_file.is_none() {
|
||||
#[cfg(feature = "plugin")]
|
||||
read_plugin_file(
|
||||
engine_state,
|
||||
&mut stack,
|
||||
parsed_nu_cli_args.plugin_file,
|
||||
NUSHELL_FOLDER,
|
||||
);
|
||||
read_plugin_file(engine_state, parsed_nu_cli_args.plugin_file, NUSHELL_FOLDER);
|
||||
|
||||
perf(
|
||||
"read plugins",
|
||||
@ -155,12 +150,7 @@ pub(crate) fn run_file(
|
||||
if parsed_nu_cli_args.no_config_file.is_none() {
|
||||
let start_time = std::time::Instant::now();
|
||||
#[cfg(feature = "plugin")]
|
||||
read_plugin_file(
|
||||
engine_state,
|
||||
&mut stack,
|
||||
parsed_nu_cli_args.plugin_file,
|
||||
NUSHELL_FOLDER,
|
||||
);
|
||||
read_plugin_file(engine_state, parsed_nu_cli_args.plugin_file, NUSHELL_FOLDER);
|
||||
perf(
|
||||
"read plugins",
|
||||
start_time,
|
||||
|
@ -117,7 +117,7 @@ fn test_config_path_helper(playground: &mut Playground, config_dir_nushell: Path
|
||||
|
||||
#[cfg(feature = "plugin")]
|
||||
{
|
||||
let plugin_path = config_dir_nushell.join("plugin.nu");
|
||||
let plugin_path = config_dir_nushell.join("plugin.msgpackz");
|
||||
let canon_plugin_path =
|
||||
adjust_canonicalization(std::fs::canonicalize(&plugin_path).unwrap_or(plugin_path));
|
||||
let actual = run(playground, "$nu.plugin-path");
|
||||
@ -161,7 +161,7 @@ fn test_default_symlink_config_path_broken_symlink_config_files() {
|
||||
"history.txt",
|
||||
"history.sqlite3",
|
||||
"login.nu",
|
||||
"plugin.nu",
|
||||
"plugin.msgpackz",
|
||||
] {
|
||||
let fake_file = fake_dir.join(config_file);
|
||||
File::create(playground.cwd().join(&fake_file)).unwrap();
|
||||
@ -194,7 +194,7 @@ fn test_default_config_path_symlinked_config_files() {
|
||||
"history.txt",
|
||||
"history.sqlite3",
|
||||
"login.nu",
|
||||
"plugin.nu",
|
||||
"plugin.msgpackz",
|
||||
] {
|
||||
let empty_file = playground.cwd().join(format!("empty-{config_file}"));
|
||||
File::create(&empty_file).unwrap();
|
||||
|
211
tests/plugins/cache_file.rs
Normal file
211
tests/plugins/cache_file.rs
Normal file
@ -0,0 +1,211 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
use nu_protocol::{PluginCacheFile, PluginCacheItem, PluginCacheItemData};
|
||||
use nu_test_support::{fs::Stub, nu, nu_with_plugins, playground::Playground};
|
||||
|
||||
fn example_plugin_path() -> PathBuf {
|
||||
nu_test_support::commands::ensure_plugins_built();
|
||||
|
||||
let bins_path = nu_test_support::fs::binaries();
|
||||
nu_path::canonicalize_with(
|
||||
if cfg!(windows) {
|
||||
"nu_plugin_example.exe"
|
||||
} else {
|
||||
"nu_plugin_example"
|
||||
},
|
||||
bins_path,
|
||||
)
|
||||
.expect("nu_plugin_example not found")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_add_then_restart_nu() {
|
||||
let result = nu_with_plugins!(
|
||||
cwd: ".",
|
||||
plugins: [],
|
||||
&format!("
|
||||
plugin add '{}'
|
||||
(
|
||||
^$nu.current-exe
|
||||
--config $nu.config-path
|
||||
--env-config $nu.env-path
|
||||
--plugin-config $nu.plugin-path
|
||||
--commands 'plugin list | get name | to json --raw'
|
||||
)
|
||||
", example_plugin_path().display())
|
||||
);
|
||||
assert!(result.status.success());
|
||||
assert_eq!(r#"["example"]"#, result.out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_add_to_custom_path() {
|
||||
let example_plugin_path = example_plugin_path();
|
||||
Playground::setup("plugin add to custom path", |dirs, _playground| {
|
||||
let result = nu!(
|
||||
cwd: dirs.test(),
|
||||
&format!("
|
||||
plugin add --plugin-config test-plugin-file.msgpackz '{}'
|
||||
", example_plugin_path.display())
|
||||
);
|
||||
|
||||
assert!(result.status.success());
|
||||
|
||||
let contents = PluginCacheFile::read_from(
|
||||
File::open(dirs.test().join("test-plugin-file.msgpackz"))
|
||||
.expect("failed to open plugin file"),
|
||||
None,
|
||||
)
|
||||
.expect("failed to read plugin file");
|
||||
|
||||
assert_eq!(1, contents.plugins.len());
|
||||
assert_eq!("example", contents.plugins[0].name);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_rm_then_restart_nu() {
|
||||
let result = nu_with_plugins!(
|
||||
cwd: ".",
|
||||
plugin: ("nu_plugin_example"),
|
||||
r#"
|
||||
plugin rm example
|
||||
^$nu.current-exe --config $nu.config-path --env-config $nu.env-path --plugin-config $nu.plugin-path --commands 'plugin list | get name | to json --raw'
|
||||
"#
|
||||
);
|
||||
assert!(result.status.success());
|
||||
assert_eq!(r#"[]"#, result.out);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_rm_not_found() {
|
||||
let result = nu_with_plugins!(
|
||||
cwd: ".",
|
||||
plugins: [],
|
||||
r#"
|
||||
plugin rm example
|
||||
"#
|
||||
);
|
||||
assert!(!result.status.success());
|
||||
assert!(result.err.contains("example"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_rm_from_custom_path() {
|
||||
let example_plugin_path = example_plugin_path();
|
||||
Playground::setup("plugin rm from custom path", |dirs, _playground| {
|
||||
let file = File::create(dirs.test().join("test-plugin-file.msgpackz"))
|
||||
.expect("failed to create file");
|
||||
let mut contents = PluginCacheFile::new();
|
||||
|
||||
contents.upsert_plugin(PluginCacheItem {
|
||||
name: "example".into(),
|
||||
filename: example_plugin_path,
|
||||
shell: None,
|
||||
data: PluginCacheItemData::Valid { commands: vec![] },
|
||||
});
|
||||
|
||||
contents.upsert_plugin(PluginCacheItem {
|
||||
name: "foo".into(),
|
||||
// this doesn't exist, but it should be ok
|
||||
filename: dirs.test().join("nu_plugin_foo"),
|
||||
shell: None,
|
||||
data: PluginCacheItemData::Valid { commands: vec![] },
|
||||
});
|
||||
|
||||
contents
|
||||
.write_to(file, None)
|
||||
.expect("failed to write plugin file");
|
||||
|
||||
let result = nu!(
|
||||
cwd: dirs.test(),
|
||||
"plugin rm --plugin-config test-plugin-file.msgpackz example",
|
||||
);
|
||||
assert!(result.status.success());
|
||||
assert!(result.err.trim().is_empty());
|
||||
|
||||
// Check the contents after running
|
||||
let contents = PluginCacheFile::read_from(
|
||||
File::open(dirs.test().join("test-plugin-file.msgpackz")).expect("failed to open file"),
|
||||
None,
|
||||
)
|
||||
.expect("failed to read file");
|
||||
|
||||
assert!(!contents.plugins.iter().any(|p| p.name == "example"));
|
||||
|
||||
// Shouldn't remove anything else
|
||||
assert!(contents.plugins.iter().any(|p| p.name == "foo"));
|
||||
})
|
||||
}
|
||||
|
||||
/// Running nu with a test plugin file that fails to parse on one plugin should just cause a warning
|
||||
/// but the others should be loaded
|
||||
#[test]
|
||||
fn warning_on_invalid_plugin_item() {
|
||||
let example_plugin_path = example_plugin_path();
|
||||
Playground::setup("warning on invalid plugin item", |dirs, playground| {
|
||||
playground.with_files(vec![
|
||||
Stub::FileWithContent("config.nu", ""),
|
||||
Stub::FileWithContent("env.nu", ""),
|
||||
]);
|
||||
|
||||
let file = File::create(dirs.test().join("test-plugin-file.msgpackz"))
|
||||
.expect("failed to create file");
|
||||
let mut contents = PluginCacheFile::new();
|
||||
|
||||
contents.upsert_plugin(PluginCacheItem {
|
||||
name: "example".into(),
|
||||
filename: example_plugin_path,
|
||||
shell: None,
|
||||
data: PluginCacheItemData::Valid { commands: vec![] },
|
||||
});
|
||||
|
||||
contents.upsert_plugin(PluginCacheItem {
|
||||
name: "badtest".into(),
|
||||
// this doesn't exist, but it should be ok
|
||||
filename: dirs.test().join("nu_plugin_badtest"),
|
||||
shell: None,
|
||||
data: PluginCacheItemData::Invalid,
|
||||
});
|
||||
|
||||
contents
|
||||
.write_to(file, None)
|
||||
.expect("failed to write plugin file");
|
||||
|
||||
let result = Command::new(nu_test_support::fs::executable_path())
|
||||
.current_dir(dirs.test())
|
||||
.args([
|
||||
"--no-std-lib",
|
||||
"--config",
|
||||
"config.nu",
|
||||
"--env-config",
|
||||
"env.nu",
|
||||
"--plugin-config",
|
||||
"test-plugin-file.msgpackz",
|
||||
"--commands",
|
||||
"plugin list | get name | to json --raw",
|
||||
])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.expect("failed to run nu");
|
||||
|
||||
let out = String::from_utf8_lossy(&result.stdout).trim().to_owned();
|
||||
let err = String::from_utf8_lossy(&result.stderr).trim().to_owned();
|
||||
|
||||
println!("=== stdout\n{out}\n=== stderr\n{err}");
|
||||
|
||||
// The code should still execute successfully
|
||||
assert!(result.status.success());
|
||||
// The "example" plugin should be unaffected
|
||||
assert_eq!(r#"["example"]"#, out);
|
||||
// The warning should be in there
|
||||
assert!(err.contains("cached plugin data"));
|
||||
assert!(err.contains("badtest"));
|
||||
})
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
mod cache_file;
|
||||
mod config;
|
||||
mod core_inc;
|
||||
mod custom_values;
|
||||
|
Loading…
Reference in New Issue
Block a user